Add BambuPrinter::get_version, the first real request/response round trip

Documents request()'s real constraint on BambuMqttSession directly (not
all Bambu commands map 1:1 to a reply, and even where one exists its
shape can differ by protocol version - e.g. pushing.pushall triggers an
ongoing print.push_status stream rather than a direct reply, and
print.calibration data lives inside push_status on BambuMQTT v1 but gets
its own report on v2 - so request() is only correct for genuinely
one-shot, sequence_id-correlated commands like info.get_version).

get_version itself is Bambu-only (no GenericPrinter equivalent, matching
test_tls_handshake/fetch_certificate/ensure_trusted's existing pattern):
builds the request via continuum-schema-instance, sends it through
BambuMqttSession::request, and parses the reply into the existing
hand-written GetVersionResponse - reusing the struct already validated
against real captured Bambu JSON in bambu_commands_demo.rs, rather than
introducing a second response representation.

Verified structurally (no real broker reachable from here - confirmed
this sandbox has no route into the printer's LAN at all, not just a
blocked port): connect fails gracefully against an unreachable host, and
get_version correctly reports "not connected" rather than panicking when
called without a live session.
This commit is contained in:
2026-09-05 21:27:59 +00:00
parent 9643c8b685
commit 416ef6db07
2 changed files with 48 additions and 0 deletions
+27
View File
@@ -343,6 +343,33 @@ impl BambuPrinter {
Err(err) => Err(err), Err(err) => Err(err),
} }
} }
/// Bambu-only - no `GenericPrinter` equivalent, since every vendor's
/// way of reporting firmware info (if it has one at all) is shaped
/// completely differently. Same "vendor-only extension" pattern as
/// `test_tls_handshake`/`fetch_certificate`/`ensure_trusted` above:
/// reached by matching `PrinterHandle::Bambu` back out of the enum,
/// not through the shared trait.
///
/// The first real use of `BambuMqttSession::request` -
/// `info.get_version` is a genuine one-shot, `sequence_id`-correlated
/// reply (unlike `pushing.pushall`, see `request`'s doc comment), so
/// this is exactly the case it's for.
pub async fn get_version(&self) -> Result<super::GetVersionResponse, PrinterError> {
let (session, version) =
self.connection.as_ref().ok_or_else(|| PrinterError(format!("{} is not connected", self.base.name)))?;
let body = continuum_schema_instance::build_instance_from_file(&get_schema_for_command(
*version,
"info".into(),
"get_version".into(),
))
.map_err(|err| PrinterError(format!("failed to build get_version request: {err}")))?;
let response = session.request("info", body).await?;
serde_json::from_value(response)
.map_err(|err| PrinterError(format!("failed to parse get_version response: {err}")))
}
} }
impl GenericPrinter for BambuPrinter { impl GenericPrinter for BambuPrinter {
+21
View File
@@ -162,6 +162,27 @@ impl BambuMqttSession {
/// `body` already had (continuum-schema-instance fills it with an /// `body` already had (continuum-schema-instance fills it with an
/// empty placeholder, since it has no way to know a real one) with a /// empty placeholder, since it has no way to know a real one) with a
/// fresh one from this session's own counter. /// fresh one from this session's own counter.
///
/// Only correct for commands that genuinely get *one direct,
/// sequence_id-correlated reply* - `info.get_version` is the
/// canonical example (the envelope schema says as much: "Sequence ID
/// matching the original request"). It is NOT a general "send a
/// command, get the response" primitive - Bambu's own mapping isn't
/// 1:1:
/// - `pushing.pushall` doesn't reply at all - it triggers an *ongoing*
/// `print.push_status` stream, unrelated to this request's
/// `sequence_id`. Use `publish` (fire-and-forget) for it, the same
/// way `send_gcode_line` already does for `gcode_line`.
/// - Even where a field genuinely exists, which report carries it can
/// differ *by protocol version*: `print.calibration` data is bundled
/// inside `print.push_status` on BambuMQTT v1, but v2 gives it its
/// own dedicated report. Calling `request` for something like that
/// would just hang until `REQUEST_TIMEOUT` waiting for a reply that
/// was never going to arrive correlated by `sequence_id` - not a
/// crash, but a silent design bug if used on the wrong kind of
/// command. An ongoing-stream subscription (register interest in a
/// *category*, get every report under it as it arrives, not a single
/// `oneshot`) is a separate primitive this doesn't provide yet.
pub async fn request(&self, category: &str, mut body: Value) -> Result<Value, PrinterError> { pub async fn request(&self, category: &str, mut body: Value) -> Result<Value, PrinterError> {
let sequence_id = self.next_sequence_id.fetch_add(1, Ordering::Relaxed).to_string(); let sequence_id = self.next_sequence_id.fetch_add(1, Ordering::Relaxed).to_string();
body["sequence_id"] = Value::String(sequence_id.clone()); body["sequence_id"] = Value::String(sequence_id.clone());