Real MQTT wiring for Bambu: connect, auto-detect v5/v3.1.1, request/reply
Removes GenericPrinter::receive (two disconnected fire-and-forget stubs that didn't actually give request/response) in favor of a real mechanism in the new src/printer/bambu_mqtt.rs: - BambuMqttSession wraps whichever rumqttc client a connection ended up using (v3.1.1's AsyncClient/EventLoop for BambuMQTT v1, v5's for BambuMQTT v2 - different types, different event/error shapes) behind one interface. `open()` respects ForceV1/ForceV2, and for Auto tries v5 first, falling back to v3.1.1 on any failure (see the module doc comment for why "any failure", not a precisely-matched version- rejection code - unverified against a live printer, no broker here to test against). - Reuses BambuTls::build_connector (now pub(crate)) for rumqttc's TLS transport via TlsConfiguration::NativeConnector, instead of a second, separate TLS trust story - hence rumqttc now pulls in use-native-tls instead of its default rustls stack, so there's one TLS implementation in the binary, not two. - request() publishes and waits (with a timeout) for a report whose sequence_id matches, correlated via a shared pending-request table that a spawned background task completes as reports arrive - the first real tokio::spawn in this crate, flagged as deferred since printer/mod.rs's very first doc comment; this is why it was deferred until now, and why it's needed now (polling the eventloop has to run continuously, independent of whatever call site is waiting). - publish()/publish_raw() are the fire-and-forget half, for commands like gcode_line that only get acknowledged implicitly. BambuPrinter::connect/send_gcode_line/publish now call the real session instead of printing what they would do. Verified against 192.0.2.1 (a reserved, never-routable test address): both AsyncClient/EventLoop pairs actually attempt a real TCP connection and fail gracefully (network timeout, not a panic) when nothing answers, and Auto genuinely attempts both while ForceV1 skips straight to v3.1.1.
This commit is contained in:
+44
-60
@@ -73,7 +73,11 @@ impl BambuTls {
|
||||
|
||||
/// Builds a `native_tls` connector configured for this trust mode:
|
||||
/// trusting only `ca_bytes()`, or accepting anything for `Insecure`.
|
||||
fn build_connector(&self) -> anyhow::Result<native_tls::TlsConnector> {
|
||||
/// `pub(crate)`, not private: `bambu_mqtt.rs` (a sibling module, not a
|
||||
/// descendant of this one) reuses this exact connector to configure
|
||||
/// rumqttc's TLS transport, instead of every consumer's TLS trust
|
||||
/// decisions living in two separate places.
|
||||
pub(crate) fn build_connector(&self) -> anyhow::Result<native_tls::TlsConnector> {
|
||||
let mut builder = native_tls::TlsConnector::builder();
|
||||
|
||||
// Bambu printer certificates don't carry their LAN IP as a Subject
|
||||
@@ -182,8 +186,10 @@ pub struct BambuPrinter {
|
||||
pub sn: String,
|
||||
tls: BambuTls,
|
||||
preference: BambuProtocolPreference,
|
||||
/// `None` until `connect()` succeeds.
|
||||
version: Option<BambuProtocolVersion>,
|
||||
/// `None` until `connect()` succeeds. Carries the live MQTT session
|
||||
/// alongside the protocol version it settled on, since the two are
|
||||
/// decided together and always used together - see `bambu_mqtt.rs`.
|
||||
connection: Option<(super::bambu_mqtt::BambuMqttSession, BambuProtocolVersion)>,
|
||||
}
|
||||
|
||||
impl BambuPrinter {
|
||||
@@ -206,7 +212,7 @@ impl BambuPrinter {
|
||||
sn: sn.into(),
|
||||
tls,
|
||||
preference,
|
||||
version: None,
|
||||
connection: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,51 +350,33 @@ impl GenericPrinter for BambuPrinter {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// Settles `self.version` - forced directly if `preference` says so,
|
||||
/// otherwise (the real version) attempting an MQTT v5 CONNECT first
|
||||
/// and falling back to v3.1.1 if the broker's CONNACK rejects that
|
||||
/// protocol version. No real MQTT client exists yet (still a stub, no
|
||||
/// different from before), so `Auto` always "detects" v2 for now -
|
||||
/// the fallback attempt itself is what's left to build.
|
||||
/// Real, not a stub: opens the MQTT session (see `bambu_mqtt.rs` for
|
||||
/// `preference`'s `Auto`-tries-v5-then-v3.1.1 fallback) and keeps it -
|
||||
/// same reasoning as `PrusaSerialPrinter::connect` holding its port
|
||||
/// open, just an MQTT session instead of a serial port.
|
||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||
let mode = match &self.tls {
|
||||
BambuTls::BundledCa => "bundled Bambu CA",
|
||||
BambuTls::Custom(_) => "custom CA cert",
|
||||
BambuTls::Insecure => "no certificate verification",
|
||||
};
|
||||
let (session, version) = super::bambu_mqtt::BambuMqttSession::open(
|
||||
&self.base.host,
|
||||
&self.access_code,
|
||||
&self.sn,
|
||||
&self.tls,
|
||||
self.preference,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let version = match self.preference {
|
||||
BambuProtocolPreference::ForceV1 => BambuProtocolVersion::V1,
|
||||
BambuProtocolPreference::ForceV2 => BambuProtocolVersion::V2,
|
||||
BambuProtocolPreference::Auto => {
|
||||
println!(
|
||||
"[{}] auto-detecting protocol: trying MQTT v5 first",
|
||||
self.base.name
|
||||
);
|
||||
BambuProtocolVersion::V2
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"[{}] connecting over MQTTS:8883 ({mode}, {version:?})",
|
||||
self.base.name
|
||||
);
|
||||
self.version = Some(version);
|
||||
println!("[{}] connected over MQTT ({version:?})", self.base.name);
|
||||
self.connection = Some((session, version));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Real: a raw pass-through publish to `device/{sn}/request`, no
|
||||
/// category envelope opinion - `send_gcode_line` below builds and
|
||||
/// sends its own properly-wrapped request instead of going through
|
||||
/// this.
|
||||
async fn publish(&mut self, payload: String) -> Result<(), PrinterError> {
|
||||
// TODO: add publishing via MQTT
|
||||
println!(
|
||||
"[{}] publishing to device/{}/request: {payload}",
|
||||
self.base.name, self.sn
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive(&mut self, payload: String) -> Result<(), PrinterError> {
|
||||
// TODO: add MQTT response handling
|
||||
Ok(())
|
||||
let (session, _) =
|
||||
self.connection.as_ref().ok_or_else(|| PrinterError(format!("{} is not connected", self.base.name)))?;
|
||||
session.publish_raw(payload).await
|
||||
}
|
||||
|
||||
/// Same MQTT gcode-injection mechanism regardless of protocol
|
||||
@@ -423,25 +411,21 @@ impl GenericPrinter for BambuPrinter {
|
||||
/// which one depends on `self.version`, settled by `connect()` above -
|
||||
/// instead of a hand-written serde struct: `sequence_id` and the
|
||||
/// `command` const get filled in by the schema itself, `param` is the
|
||||
/// one field we actually set. Only the actual MQTT publish to
|
||||
/// `device/{sn}/request` is still a stub, same as `connect` above.
|
||||
/// one field we actually set. Real, end to end: publishes over the
|
||||
/// live MQTT session `connect()` opened, no waiting for a reply -
|
||||
/// `gcode_line` is fire-and-forget, the printer just executes it.
|
||||
async fn send_gcode_line(&mut self, gcode_line: String) -> Result<(), PrinterError> {
|
||||
let version = self
|
||||
.version
|
||||
.ok_or_else(|| PrinterError(format!("{} is not connected", self.base.name)))?;
|
||||
let (session, version) =
|
||||
self.connection.as_ref().ok_or_else(|| PrinterError(format!("{} is not connected", self.base.name)))?;
|
||||
|
||||
let mut print =
|
||||
continuum_schema_instance::build_instance_from_file(
|
||||
&get_schema_for_command(
|
||||
version,
|
||||
"print".into(),
|
||||
"gcode_line".into()
|
||||
)
|
||||
)
|
||||
.map_err(|err| PrinterError(format!("failed to build gcode_line request: {err}")))?;
|
||||
print["param"] = serde_json::Value::String(gcode_line);
|
||||
let mut body = continuum_schema_instance::build_instance_from_file(&get_schema_for_command(
|
||||
*version,
|
||||
"print".into(),
|
||||
"gcode_line".into(),
|
||||
))
|
||||
.map_err(|err| PrinterError(format!("failed to build gcode_line request: {err}")))?;
|
||||
body["param"] = serde_json::Value::String(gcode_line);
|
||||
|
||||
let payload = serde_json::json!({ "print": print }).to_string();
|
||||
self.publish(payload).await
|
||||
session.publish("print", &body).await
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user