mirror of
https://github.com/ClusterM/open-bamboo-networking.git
synced 2026-10-23 03:23:23 -05:00
342 lines
15 KiB
Markdown
342 lines
15 KiB
Markdown
# `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/<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`](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 `<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`
|
||
|
||
```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_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)
|
||
|
||
```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_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](https://mqtt-explorer.com/) — 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):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```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_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.
|
||
|
||
```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_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:
|
||
|
||
```bash
|
||
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`](plugin_loader.hpp) against the
|
||
`tools/abi_snapshot/v<MM.mm.pp>/NetworkAgent.hpp` snapshot for the
|
||
ABI you're targeting.
|