Replace proto-based bambu_protocol with plain-Rust bambu_commands/

One file per command (get_version.rs, pushall.rs), each holding its
request AND response together. CommandEnvelope (sequence_id + command) is
the shared-fields piece every command embeds via #[serde(flatten)] — the
'inheritance' replacement for these structs, same composition idea as
BambuGenericPrinter in bambu.rs, just expressed as a struct field instead
of impl-block delegation.

examples/bambu_commands_demo.rs round-trips both commands against Bambu's
exact real JSON (from the original conversation) and confirms byte-for-
byte matches, same as the removed proto version did — this replacement is
behaviorally identical, just without protoc/build.rs in the loop.
This commit is contained in:
2026-08-28 22:49:06 +00:00
parent 8e5d85202f
commit 166761479b
8 changed files with 145 additions and 78 deletions
+2 -2
View File
@@ -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"] }
+35
View File
@@ -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);
}
-24
View File
@@ -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);
}
+28
View File
@@ -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<T>` 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<String>,
pub reason: Option<String>,
pub module: Vec<ModuleInfo>,
}
#[derive(serde::Deserialize, Debug)]
pub struct ModuleInfo {
pub hw_ver: String,
pub name: String,
pub sn: String,
pub sw_ver: String,
}
+56
View File
@@ -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),
}
+20
View File
@@ -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<String>,
pub reason: Option<String>,
// 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<T>` for anything that isn't always present.
}
-50
View File
@@ -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.
+4 -2
View File
@@ -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};