Files
open-bamboo-networking/tools/plugin_runner
Alexey Cluster 304fa24777 Clean up
Signed-off-by: Alexey Cluster <cluster@cluster.wtf>
2026-05-20 16:56:16 +02:00
..
2026-05-08 14:34:33 +02:00
2026-05-20 16:56:16 +02:00
2026-05-20 16:56:16 +02:00
2026-05-08 14:34:33 +02:00
2026-05-08 14:34:33 +02:00
2026-05-08 14:34:33 +02:00
2026-05-08 14:34:33 +02:00
2026-05-08 14:34:33 +02:00
2026-05-20 16:40:03 +02:00

plugin_runner — stock-plugin shim for MQTT reverse engineering

plugin_runner is a tiny standalone CLI that pretends to be Bambu Studio just enough to load any libbambu_networking.so (a specific upstream release), drive its print entry points with a user-supplied BBL::PrintParams, and let the real MQTT publish hit your printer. It exists so we can diff the wire output of the stock plugin against our own open-bamboo-networking build without firing up the whole IDE.

The wrapper script (tools/plugin_runner.sh) is the only thing humans should call. It picks the right ABI, builds the C++ bridge against it, downloads the matching plugin from Bambu's CDN (or reuses a cached copy), and exec's the binary with all your other flags forwarded.


1. Install dependencies

Debian/Ubuntu:

sudo apt install build-essential cmake ninja-build pkg-config \
                 libcurl4-openssl-dev libminizip-dev libssl-dev \
                 nlohmann-json3-dev python3

python3 is only used by the wrapper to parse the downloader's JSON output (one tiny python3 -c); the C++ binary itself has no Python dependency.

2. Layout

File Role
../plugin_runner.sh Bash wrapper, the only user-facing entry point
CMakeLists.txt Standalone CMake project (independent of the top-level build)
main.cpp CLI parsing, init order, callback wiring, action dispatch, completion wait
plugin_loader.{hpp,cpp} dlopen + dlsym for the ~30 entry points we use
print_params_io.{hpp,cpp} JSON ↔ BBL::PrintParams (the --params-json schema)
plugin_downloader.{hpp,cpp} libcurl + minizip against Bambu's /slicer/resource API

Build artefacts land in tools/plugin_runner/build-0xMMmmpp/ (one dir per ABI). Plugin downloads are cached in ~/.cache/obn-plugin-runner/<full-version>/libbambu_networking.so (override with --cache-dir).

3. CLI reference

Wrapper-only flags

--abi MM.mm.pp            target ABI version          (required)
--plugin-path PATH        use this .so, skip download
--use-backup-fallback     also try ~/.config/BambuStudio/plugins/backup/
--force-download          ignore cache, refetch from CDN
--cache-dir DIR           override ~/.cache/obn-plugin-runner

Forwarded to the C++ binary

--params-json FILE        BBL::PrintParams JSON (see §4 for the schema)
                          required for the print actions; ignored for
                          send_raw / none
--action ACTION           send_gcode_to_sdcard | local_print
                          | sdcard_print | local_print_with_record
                          | send_raw | none
--gcode-3mf PATH          path to the *.gcode.3mf file Studio would have
                          generated; used as `filename` and `ftp_file`
                          fallbacks if your params JSON omits them
--dev-id ID               printer serial                                    (required)
--dev-ip IP               printer LAN IP                                    (required)
--access-code CODE        printer LAN access code                           (required)
--country US              countries influence routing in some plugins
--use-ssl-mqtt 0|1        default 1; matches Studio's LAN flow
--cert-file PATH          override slicer_base64.cer location (best-effort)
--timeout SECONDS         max wait for PrintingStageFinished/ERROR (default 90)
--connect-settle-ms MS    *cap* on the wait for set_on_local_connect_fn
                          (status=ConnectStatusOk) before the publish
                          channel is considered ready. Default 800; bump
                          to 15000+ on slow handshakes (see §7).
--log-out PATH            also mirror every JSON event line to this file
--keep-tmpdir             leave the per-run /tmp/obn-plugin-runner-* alone
--fast-exit               flush logs then `_Exit(0)` instead of letting
                          destroy_agent drain its worker pool. Default ON
                          for `--action none`, OFF otherwise. Stock plugins
                          keep boost::asio / mqtt-cpp threads alive past
                          destroy_agent so a graceful shutdown blocks ~60s;
                          fast exit lets the kernel reap the mapping.
--no-fast-exit            opposite, useful if you want destroy_agent to
                          flush a final status/event after the action.

# --action send_raw flags
--raw-json FILE                JSON payload to publish verbatim       (required)
--raw-qos N                    MQTT QoS (default 1)
--raw-flag N                   plugin-specific publish flag (default 0)
--raw-settle SECONDS           sleep after the last publish to let
                               local_message replies land (default 5)
--raw-repeat N                 publish the payload N times (default 1)
--raw-repeat-interval-s S      gap between repeats (default 5)

plugin_runner --help prints the same reference at any time.

4. PrintParams JSON

The flat JSON object is fed straight into BBL::PrintParams. The full list of accepted keys (and their types) is the source of truth: tools/plugin_runner/print_params_io.cpp — load_print_params_overrides() walks PrintParams field-by-field with typed setters. Missing keys keep BBL::PrintParams{} defaults; unknown or wrong-typed keys log a params_warning event and are ignored.

A ready-to-edit reference covering every field this loader knows about — including ams_mapping* JSON-array-as-string conventions, default values from a real Studio session, and the task_* toggles that map straight into the MQTT project_file payload — lives at tools/plugin_runner/example.params.json. Copy it, point filename at your local 3mf, drop fields you don't want to override, and pass the result via --params-json.

Minimal experiment that toggles just task_timelapse_use_internal:

{
    "filename":                    "/path/to/your-job.gcode.3mf",
    "task_name":                   "obn-runner-test",
    "task_record_timelapse":       true,
    "task_timelapse_use_internal": true,
    "task_bed_type":               "auto"
}

dev_id, dev_ip, username (bblp), password (your --access-code) and use_ssl_for_mqtt only fall back to their CLI values when the JSON leaves them blank — encode them only if you want to override the wrapper.

5. Worked examples

The examples below use the placeholders <DEV_ID> (15-character serial from the printer's Settings → About screen), <PRINTER_IP> (LAN IPv4 of the printer), and <ACCESS_CODE> (the 8-digit code under Settings → Network → LAN-only mode). Substitute your own values before running anything.

Auto-download for ABI 02.05.03

tools/plugin_runner.sh \
    --abi 02.05.03 \
    --params-json experiment.json \
    --action send_gcode_to_sdcard \
    --gcode-3mf /tmp/job.gcode.3mf \
    --dev-id  <DEV_ID> \
    --dev-ip  <PRINTER_IP> \
    --access-code <ACCESS_CODE> \
    --log-out /tmp/runner.jsonl

First call configures + builds tools/plugin_runner/build-0x020503/ (~10 s on a cold machine), fetches https://public-cdn.bblmw.com/.../linux_02.05.03.63.zip, and caches the extracted .so under ~/.cache/obn-plugin-runner/02.05.03.63/. Subsequent runs against the same ABI start in well under a second.

Custom .so (e.g. an old build you have backed up)

tools/plugin_runner.sh \
    --abi 02.05.02 \
    --plugin-path ~/.config/BambuStudio/plugins/backup/libbambu_networking.so \
    --params-json experiment.json \
    --action local_print \
    --gcode-3mf /tmp/job.gcode.3mf \
    --dev-id <DEV_ID> --dev-ip <PRINTER_IP> --access-code <ACCESS_CODE>

--plugin-path skips the CDN; the wrapper still rebuilds the bridge against --abi 02.05.02 so the PrintParams layout matches the .so.

6. Diff workflow with an MQTT sniffer

The point of this tool is wire-level diffs, not "does it print". The recommended pairing is one terminal sniffing MQTT against the printer while another runs plugin_runner.sh:

Terminal A — subscribe to the printer's MQTT broker. Any MQTT client with TLS support works; just point it at mqtts://<PRINTER_IP>:8883, authenticate as bblp / <ACCESS_CODE>, and disable certificate validation (the printer presents a self-signed cert with its serial as CN, so chain validation will fail in any client that checks). A few options:

  • GUI: MQTT Explorer — open the app, add a connection with Protocol: mqtts, Host: <PRINTER_IP>, Port: 8883, Username: bblp, Password: <ACCESS_CODE>, and under Advanced → Certificate validation turn off "Validate certificate". Topic tree and per-topic diff are very handy for spotting which field actually changed between two runs.

  • CLI (mosquitto_sub doesn't talk to a self-signed broker out of the box, so bridge the TLS with socat first):

    socat -d TCP-LISTEN:18883,bind=127.0.0.1,reuseaddr,fork \
             OPENSSL:<PRINTER_IP>:8883,verify=0,commonname=ignore &
    mosquitto_sub -h 127.0.0.1 -p 18883 \
        -u bblp -P <ACCESS_CODE> -t '#' -v -F '%I  %t  %p' \
      | tee /tmp/mqtt-stock.log
    

    Best for scripted diffs since the output is plain text.

Terminal B — run the stock plugin once with your override:

tools/plugin_runner.sh --abi 02.05.03 \
    --params-json experiment.json \
    --action send_gcode_to_sdcard --gcode-3mf job.gcode.3mf \
    --dev-id <DEV_ID> --dev-ip <PRINTER_IP> --access-code <ACCESS_CODE>

Then re-run the same job using our build in place of the stock plugin and diff the captures. Anything that differs in the project_file publish is something the override / experiment.json actually changed; anything else is a wire bug in our plugin to investigate.

7. Operational notes

  • plugin_runner writes one JSON line per plugin event (local_message, update_status, local_connect, …) to stdout; copy with --log-out.
  • The bridge spawns no threads itself — every callback invocation happens on whichever worker thread the plugin called us back on. set_queue_on_main_fn is wired to inline-execute, which is fine for this LAN-only scope but would deadlock under a real UI loop.
  • Cloud printing (bambu_network_start_print) is deliberately out-of-scope: it requires the bambu-cloud auth dance and OSS upload, neither of which is needed for diffing the LAN command set.
  • A --timeout exceeded run returns exit code 75 (EX_TEMPFAIL); a finished-but-failed action returns 1; a clean success returns 0.

7. Connect timing (--connect-settle-ms) and -4030

connect_printer is asynchronous: it returns rc=0 immediately and the actual TLS handshake + first MQTT subscribe happens on a worker thread several seconds later (typically 5–6 s on a quiet network). The set_on_local_connect_fn callback firing with status=ConnectStatusOk is the only reliable signal that the LAN publish channel is ready. Publishing earlier deterministically returns -4 (paho's MQTTASYNC_DISCONNECTED); the print actions surface that as update_status code=-4030 msg="send msg failed" at the Sending stage.

plugin_runner waits for that callback automatically and only then proceeds. --connect-settle-ms is a cap on that wait, not a fixed sleep — bump it (e.g. --connect-settle-ms 20000) only if your printer is slow to handshake, otherwise the default 800 ms is fine once the wait turns into an event-driven block. The matching log event is session_ready {got: true|false, cap_ms: N} followed by a kickstart_pushall (verbatim copy of Studio's MachineObject::command_request_push_all payload, used both to mimic Studio's keep_alive loop and as a smoke test for the publish channel).

If your action still fails with -4030 after kickstart_pushall rc=0, the publish channel was healthy at action time — the failure is happening inside the plugin's print state machine, not in the MQTT layer. Use --action send_raw (§8) to bisect which payload the printer actually rejects.

8. Raw publish (--action send_raw)

Direct call into bambu_network_send_message_to_printer with a verbatim JSON file. Useful for replicating Studio's exact pre-print command sequence, probing what the printer accepts, or stress-testing the LAN MQTT session.

# pushall-as-a-keepalive: mimics Studio's MachineObject::command_request_push_all
echo '{"pushing":{"sequence_id":"0","command":"pushall","version":1,"push_target":1}}' \
    > /tmp/pushall.json

tools/plugin_runner.sh --abi 02.05.03 \
    --action send_raw --raw-json /tmp/pushall.json \
    --raw-repeat 6 --raw-repeat-interval-s 5 --raw-settle 2 \
    --dev-id <DEV_ID> --dev-ip <PRINTER_IP> --access-code <ACCESS_CODE> \
    --connect-settle-ms 15000 --fast-exit \
    --log-out /tmp/runner-raw.jsonl

Watch the send_raw_rc events: rc=0 means the publish hit the wire (the printer's reply, if any, lands as a local_message); rc=-4 means the LAN MQTT session is not actually up yet — increase --connect-settle-ms or check certs. --raw-repeat plus a few-second interval is the easiest way to confirm the session stays alive over the lifetime of an action (e.g. across an FTPS upload).

9. Diagnostics: --action none

Use this to sanity-check the init/connect path without actually printing:

tools/plugin_runner.sh --abi 02.05.03 \
    --action none --timeout 6 \
    --dev-id <DEV_ID> --dev-ip <PRINTER_IP> --access-code <ACCESS_CODE> \
    --log-out /tmp/runner-diag.jsonl

A healthy run looks like (event kinds in order): startup → plugin_loaded → data_dir → cert_resolved → extra_http_header → agent_start (rc=0) → post_start_init → change_user_clear (rc=0) → ssdp_msg → bind_detect → connect_printer_call (rc=0) → local_connect (status=0) → session_ready (got=true) → start_subscribe → kickstart_pushall (rc=0) → idle_done → shutdown.

Things to watch for:

  • local_connect status=1 means the plugin's MQTT client failed to connect (printer rejected, TLS error, busy session). The most common cause is a missing slicer_base64.cer — the runner searches $HOME/.config/BambuStudio/cert/, $HOME/BambuStudio/resources/cert/ and /usr/share/BambuStudio/resources/cert/; pass --cert-file PATH if yours lives elsewhere.
  • session_ready got=false means the cap fired before the local_connect callback. Bump --connect-settle-ms.
  • kickstart_pushall rc!=0 after session_ready got=true is pathological — the publish channel reports ready but rejects the first message. Re-check the cert and that no other LAN client is holding the access code session.
  • agent_start taking ~10 s on a cold cache is normal: the plugin initialises bambu-cloud TLS material even on the LAN-only path. Subsequent runs in the same process group are a few hundred ms.
  • A missing local_connect event entirely after connect_printer_call generally means the plugin was loaded but never spun up its worker — most often because set_on_local_connect_fn was wired against the wrong typedef. Cross-check the typedefs in plugin_loader.hpp against the tools/abi_snapshot/v<MM.mm.pp>/NetworkAgent.hpp snapshot for the ABI you're targeting.