# `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`](../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: ```bash 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`](../plugin_runner.sh) | Bash wrapper, the only user-facing entry point | | [`CMakeLists.txt`](CMakeLists.txt) | Standalone CMake project (independent of the top-level build) | | [`main.cpp`](main.cpp) | CLI parsing, init order, callback wiring, action dispatch, completion wait | | [`plugin_loader.{hpp,cpp}`](plugin_loader.hpp) | `dlopen` + `dlsym` for the ~30 entry points we use | | [`print_params_io.{hpp,cpp}`](print_params_io.hpp) | JSON ↔ `BBL::PrintParams` (the `--params-json` schema) | | [`plugin_downloader.{hpp,cpp}`](plugin_downloader.hpp) | `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//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`](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`](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`: ```json { "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 `` (15-character > serial from the printer's *Settings → About* screen), `` > (LAN IPv4 of the printer), and `` (the 8-digit code > under *Settings → Network → LAN-only mode*). Substitute your own > values before running anything. ### Auto-download for ABI `02.05.03` ```bash 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-ip \ --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) ```bash 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-ip --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://:8883`, authenticate as `bblp` / ``, 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](https://mqtt-explorer.com/) — open the app, add a connection with `Protocol: mqtts`, `Host: `, `Port: 8883`, `Username: bblp`, `Password: `, 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): ```bash socat -d TCP-LISTEN:18883,bind=127.0.0.1,reuseaddr,fork \ OPENSSL::8883,verify=0,commonname=ignore & mosquitto_sub -h 127.0.0.1 -p 18883 \ -u bblp -P -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: ```bash 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-ip --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. ```bash # 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-ip --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: ```bash tools/plugin_runner.sh --abi 02.05.03 \ --action none --timeout 6 \ --dev-id --dev-ip --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`](plugin_loader.hpp) against the `tools/abi_snapshot/v/NetworkAgent.hpp` snapshot for the ABI you're targeting.