diff --git a/src/discovery/mod.rs b/src/discovery/mod.rs index b1d5aee..15d35cf 100644 --- a/src/discovery/mod.rs +++ b/src/discovery/mod.rs @@ -409,10 +409,10 @@ pub(crate) fn valid_serial(value: String) -> Option { /// one thing in a datagram that cannot be mistyped. /// /// Tolerant on purpose: it extracts whatever headers are present rather than -/// requiring a full set. The exact header spelling is from Bambu's published -/// discovery format and has not been checked against a real announcement -/// here, so a missing vendor header has to mean "one fewer field filled in", -/// not "no printer found". +/// requiring a full set. The vendor header spelling was checked against a +/// real H2C's `200 OK` reply to a unicast M-SEARCH (2026-10-29); an +/// unsolicited NOTIFY has not been seen, so a missing vendor header has to +/// mean "one fewer field filled in", not "no printer found". pub fn parse_ssdp(message: &str, from: Ipv4Addr) -> Option { let mut lines = message.lines(); let start = lines.next()?.trim(); @@ -585,6 +585,37 @@ mod tests { DevConnect.bambu.com: lan\r\n\ DevBind.bambu.com: free\r\n"; + /// The reply a real H2C sent to a unicast M-SEARCH on 1990 and on 2021 + /// (2026-10-29), byte for byte in shape; the serial, name and addresses + /// are made up. It is a `200 OK`, not a NOTIFY, and carries + /// `Devseclink`/`DevInf`/`DevCap`, which `NOTIFY` above does not. + #[test] + fn a_real_printers_reply_to_an_msearch_parses() { + let reply = "HTTP/1.1 200 OK\r\n\ + Server: UPnP/1.0\r\n\ + Date: Mon, 26 Oct 2026 17:21:14 GMT\r\n\ + Location: 192.168.0.99\r\n\ + ST: urn:bambulab-com:device:3dprinter:1\r\n\ + EXT: \r\n\ + USN: 31B8BP5A0000000\r\n\ + Cache-Control: max-age=1800\r\n\ + DevModel.bambu.com: O1C2\r\n\ + DevName.bambu.com: Workshop\r\n\ + DevConnect.bambu.com: lan\r\n\ + DevBind.bambu.com: free\r\n\ + Devseclink.bambu.com: secure\r\n\ + DevInf.bambu.com: wlan0\r\n\ + DevVersion.bambu.com: 01.08.00.00\r\n\ + DevCap.bambu.com: 1\r\n\r\n"; + let found = parse_ssdp(reply, Ipv4Addr::new(10, 0, 0, 1)).expect("a Bambu reply"); + assert_eq!(found.host, "192.168.0.99"); + assert_eq!(found.serial.as_deref(), Some("31B8BP5A0000000")); + assert_eq!(found.model.as_deref(), Some("O1C2")); + assert_eq!(found.name.as_deref(), Some("Workshop")); + assert_eq!(found.bind_state.as_deref(), Some("free")); + assert_eq!(found.detail.as_deref(), Some("Bambu firmware 01.08.00.00")); + } + #[test] fn an_announcement_yields_every_field_it_carries() { let found = parse_ssdp(NOTIFY, Ipv4Addr::new(10, 0, 0, 1)).expect("a Bambu announcement");