diff --git a/Cargo.toml b/Cargo.toml index 43ca50f..7fcbf8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ name = "auto_connect_bambu" path = "examples/auto_connect_bambu.rs" [[example]] -name = "bambu_protocol_demo" -path = "examples/bambu_protocol_demo.rs" +name = "bambu_commands_demo" +path = "examples/bambu_commands_demo.rs" [dependencies] tokio = { version = "1.40", features = ["full"] } diff --git a/examples/bambu_commands_demo.rs b/examples/bambu_commands_demo.rs new file mode 100644 index 0000000..791e8d1 --- /dev/null +++ b/examples/bambu_commands_demo.rs @@ -0,0 +1,35 @@ +//! Run with: cargo run --example bambu_commands_demo +//! +//! Round-trips both commands against the exact JSON from Bambu's real +//! protocol — proves the plain hand-written struct + serde approach +//! matches byte for byte, same as the proto-based version did, with none +//! of the build.rs ceremony. + +use continuum_proxy::printer::{CommandEnvelope, GetVersionRequest, GetVersionResponse, PushallRequest}; + +fn main() { + // --- get_version request --- + let req = GetVersionRequest { + envelope: CommandEnvelope { sequence_id: "0".to_string(), command: "get_version".to_string() }, + }; + let actual = serde_json::to_string(&req).unwrap(); + let expected = r#"{"sequence_id":"0","command":"get_version"}"#; + println!("get_version request: {actual}"); + println!("matches Bambu's shape: {}\n", actual == expected); + + // --- get_version response, deserialized from Bambu's real payload --- + let real_response = r#"{"command":"get_version","module":[{"hw_ver":"","name":"ota","sn":"","sw_ver":"01.01.01.00"},{"hw_ver":"","name":"xm","sn":"","sw_ver":"00.00.00.00"}],"sequence_id":"0"}"#; + let parsed: GetVersionResponse = serde_json::from_str(real_response).unwrap(); + println!("get_version response parsed fine, no result/reason present: {parsed:#?}\n"); + + // --- pushall request --- + let pushall = PushallRequest { + envelope: CommandEnvelope { sequence_id: "0".to_string(), command: "pushall".to_string() }, + version: 1, + push_target: 1, + }; + let actual = serde_json::to_string(&pushall).unwrap(); + let expected = r#"{"sequence_id":"0","command":"pushall","version":1,"push_target":1}"#; + println!("pushall request: {actual}"); + println!("matches Bambu's shape: {}", actual == expected); +} diff --git a/examples/bambu_protocol_demo.rs b/examples/bambu_protocol_demo.rs deleted file mode 100644 index 4a37bd9..0000000 --- a/examples/bambu_protocol_demo.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Run with: cargo run --example bambu_protocol_demo -//! -//! Proves the whole pipeline end to end: a message written in -//! continuum-schemas' proto/bambulab/v1/request.proto, compiled by prost -//! into a real Rust struct, wrapped in the hand-written BambuRequest enum, -//! and serialized by plain serde — no protobuf binary encoding involved, -//! since Bambu's actual wire format is JSON over MQTT. - -use continuum_proxy::printer::BambuRequest; -use continuum_types::bambulab::v1::InfoCommand; - -fn main() { - let request = BambuRequest::Info(InfoCommand { - sequence_id: "0".to_string(), - command: "get_version".to_string(), - }); - - let actual = serde_json::to_string(&request).unwrap(); - let expected = r#"{"info":{"sequence_id":"0","command":"get_version"}}"#; - - println!("generated: {actual}"); - println!("Bambu's real wire format: {expected}"); - println!("match: {}", actual == expected); -} diff --git a/src/printer/bambu_commands/get_version.rs b/src/printer/bambu_commands/get_version.rs new file mode 100644 index 0000000..239555d --- /dev/null +++ b/src/printer/bambu_commands/get_version.rs @@ -0,0 +1,28 @@ +use super::CommandEnvelope; + +#[derive(serde::Serialize, Debug)] +pub struct GetVersionRequest { + #[serde(flatten)] + pub envelope: CommandEnvelope, +} + +#[derive(serde::Deserialize, Debug)] +pub struct GetVersionResponse { + #[serde(flatten)] + pub envelope: CommandEnvelope, + /// Not always present — `Option` handles that natively, no + /// `#[serde(default)]` needed (unlike the proto-generated version this + /// replaced, which needed one explicit `field_attribute` call per + /// field like this). + pub result: Option, + pub reason: Option, + pub module: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct ModuleInfo { + pub hw_ver: String, + pub name: String, + pub sn: String, + pub sw_ver: String, +} diff --git a/src/printer/bambu_commands/mod.rs b/src/printer/bambu_commands/mod.rs new file mode 100644 index 0000000..3041892 --- /dev/null +++ b/src/printer/bambu_commands/mod.rs @@ -0,0 +1,56 @@ +//! Bambu's raw MQTT command/report structs — plain hand-written Rust + +//! serde, no protobuf. This data only ever needs to be understood by this +//! crate (raw vendor wire format, never crosses into TypeScript), and the +//! actual wire format is JSON over MQTT, not binary protobuf — protobuf's +//! codegen tooling kept fighting us for a payoff (cross-language codegen, +//! binary wire efficiency) that doesn't apply here at all. Proto stays in +//! continuum-schemas for continuum.v1, which is a schema we designed +//! ourselves and that both TS and Rust genuinely need. +//! +//! One file per command (`get_version.rs`, `pushall.rs`, ...) — each holds +//! its request AND response together, since they're conceptually one unit. +//! Copy `get_version.rs`'s shape for your next command. + +mod get_version; +mod pushall; + +pub use get_version::{GetVersionRequest, GetVersionResponse, ModuleInfo}; +pub use pushall::{PushallRequest, PushallResponse}; + +/// Fields every Bambu command/response shares. This is the "inheritance" +/// replacement for these structs — composition, not extension: every +/// command below *has* one of these, embedded via `#[serde(flatten)]` +/// (which unwraps it onto the parent object at serialize/deserialize +/// time, so the JSON stays flat — no extra nesting), rather than +/// *extending* a base class. Exactly the same idea as `BambuGenericPrinter` +/// in `../bambu.rs`, just expressed with a struct field instead of a +/// struct-holding-a-struct in `impl` blocks. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct CommandEnvelope { + pub sequence_id: String, + pub command: String, +} + +/// One request you can send to a Bambu printer, keyed by Bambu's top-level +/// JSON category ("info", "pushing", ...). NOTE: that top-level key is a +/// *category*, not the specific command — `CommandEnvelope::command` is +/// what actually says which one ("get_version", "pushall", ...). Right +/// now each category maps to exactly one known command, one-to-one. If a +/// category ever needs a second command (e.g. "info" gaining something +/// besides get_version), that variant's payload becomes its own small +/// `#[serde(tag = "command")]` enum instead of a single struct — ask if +/// you get there and want a worked example. +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum BambuRequest { + Info(GetVersionRequest), + Pushing(PushallRequest), +} + +/// Same idea, the response direction. +#[derive(serde::Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum BambuReport { + Info(GetVersionResponse), + Pushing(PushallResponse), +} diff --git a/src/printer/bambu_commands/pushall.rs b/src/printer/bambu_commands/pushall.rs new file mode 100644 index 0000000..7c25a1b --- /dev/null +++ b/src/printer/bambu_commands/pushall.rs @@ -0,0 +1,20 @@ +use super::CommandEnvelope; + +#[derive(serde::Serialize, Debug)] +pub struct PushallRequest { + #[serde(flatten)] + pub envelope: CommandEnvelope, + pub version: i32, + pub push_target: i32, +} + +#[derive(serde::Deserialize, Debug)] +pub struct PushallResponse { + #[serde(flatten)] + pub envelope: CommandEnvelope, + pub result: Option, + pub reason: Option, + // The real pushall/print report has a lot more fields (ams, xcam, + // tray_exist_bits, ...) — add them here the same way, one field per + // line, `Option` for anything that isn't always present. +} diff --git a/src/printer/bambu_protocol.rs b/src/printer/bambu_protocol.rs deleted file mode 100644 index 284dc45..0000000 --- a/src/printer/bambu_protocol.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Bambu's real MQTT wire format: `{"info": {...}}`, `{"pushing": {...}}`, -//! `{"print": {...}}` — one JSON object with a single key naming which -//! command/report it is. -//! -//! Deliberately NOT modeled as a proto `oneof` for the top level — see the -//! long comment in continuum-schemas/packages/rust-types/build.rs for why -//! (short version: prost-build's field/variant attribute matching can't -//! keep "the oneof field" and "the variants inside it" apart, so -//! `#[serde(flatten)]` can't be scoped correctly and won't compile). This -//! plain Rust enum sidesteps the problem entirely: serde's *default* enum -//! representation already produces exactly `{"info": {...}}` with zero -//! attributes needed. -//! -//! `Info` below is the one fully-wired example — run -//! `cargo run --example bambu_protocol_demo` to see it actually produce -//! Bambu's real wire JSON. Copy this exact shape for every other command: -//! 1. Write the message in continuum-schemas' bambulab/v1/request.proto -//! (or report.proto for a response). -//! 2. `use` it up top, same as `InfoCommand` below. -//! 3. Add one variant here, named to match Bambu's JSON key exactly -//! (`#[serde(rename_all = "snake_case")]` on the enum handles simple -//! cases automatically — a Rust variant `Pushing` becomes JSON key -//! `"pushing"` for free; only reach for `#[serde(rename = "...")]` on -//! a specific variant if the key doesn't snake_case cleanly from the -//! Rust name). - -use continuum_types::bambulab::v1::InfoCommand; - -/// One request you can send to a Bambu printer. Grows by one variant per -/// message you define in bambulab/v1/request.proto. -#[derive(serde::Serialize, Debug)] -#[serde(rename_all = "snake_case")] -pub enum BambuRequest { - Info(InfoCommand), - // Pushing(PushingCommand), -} - -/// One report/response received from a Bambu printer. Same idea, the other -/// direction — grows by one variant per message in bambulab/v1/report.proto. -/// Empty for now: no response messages defined yet (InfoResponse is the -/// natural first one — see the JSON example in bambu_protocol_demo.rs). -#[derive(serde::Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub enum BambuReport {} - -// V1/V2 fork here once you know how much of the wire format actually -// differs: if it's mostly the same shape, one BambuRequest/BambuReport -// covering fields from both packages may be enough (some variants simply -// unused on one generation); if the reports genuinely diverge, mirror this -// file as bambu_protocol_v2.rs against continuum_types::bambulab::v2. diff --git a/src/printer/mod.rs b/src/printer/mod.rs index f806d0a..142a21f 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -29,12 +29,14 @@ //! `tokio::spawn`, not yet). mod bambu; -mod bambu_protocol; +mod bambu_commands; mod klipper; mod prusa; pub use bambu::{BambuGenericPrinter, BambuTls, BambuV1Printer, BambuV2Printer}; -pub use bambu_protocol::{BambuReport, BambuRequest}; +pub use bambu_commands::{ + BambuReport, BambuRequest, CommandEnvelope, GetVersionRequest, GetVersionResponse, PushallRequest, PushallResponse, +}; pub use klipper::KlipperPrinter; pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};