Windows discovery tool / windows (push) Successful in 57s
A P1P mid-print pushes ams objects with no unit list; every such update was dropped as "missing field ams", silencing telemetry for the rest of the print (found on the first real dispatch). The unit list now defaults to empty and a partial object does not wipe known units. CONTINUUM_ALLOW_INSECURE_LOCAL_DOWNLOADS=true lets the downloader fetch a presigned URL over plain HTTP, but only from a loopback or RFC 1918 address.
157 lines
6.2 KiB
Rust
157 lines
6.2 KiB
Rust
//! Does `PrintReport` actually parse what real printers really send?
|
|
//!
|
|
//! The fixtures in `tests/fixtures/` are verbatim `print.push_status`
|
|
//! payloads captured off real hardware on 2026-10-03 via
|
|
//! `cargo run --example live_reports` (hardware identifiers scrubbed):
|
|
//!
|
|
//! - `push_status_v1_p1p_idle.json` - a P1P on BambuMQTT v1, idle.
|
|
//! - `push_status_v2_h2c_printing.json` - an H2C on BambuMQTT v2, 13% into
|
|
//! a real job.
|
|
//!
|
|
//! These exist because the struct was originally built from a *truncated*
|
|
//! example payload, and the gap between that and reality turned out to be
|
|
//! the cause of two live misbehaviours (silently empty `print_status()`, and
|
|
//! bogus "sequence gap" warnings). A fixture is the only way to keep that
|
|
//! from regressing without a printer plugged in.
|
|
|
|
use continuum_proxy::printer::PrintReport;
|
|
|
|
fn load(name: &str) -> serde_json::Value {
|
|
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/");
|
|
let text = std::fs::read_to_string(format!("{path}{name}")).expect("fixture should exist");
|
|
serde_json::from_str(&text).expect("fixture should be valid JSON")
|
|
}
|
|
|
|
/// The failure mode that mattered: `handle_incoming_report` does
|
|
/// `if let Ok(report) = serde_json::from_value::<PrintReport>(..)`, so a
|
|
/// parse error doesn't just lose that one update - it's invisible. Assert on
|
|
/// the error text, not just `is_ok()`, so a regression says *what* broke.
|
|
fn assert_parses(name: &str) {
|
|
let value = load(name);
|
|
match serde_json::from_value::<PrintReport>(value) {
|
|
Ok(_) => {}
|
|
Err(err) => panic!("{name} failed to parse as PrintReport: {err}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_v1_idle_push_status() {
|
|
assert_parses("push_status_v1_p1p_idle.json");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_v2_printing_push_status() {
|
|
assert_parses("push_status_v2_h2c_printing.json");
|
|
}
|
|
|
|
/// The whole point of consuming these at all: a print farm needs progress
|
|
/// and temperatures. If the struct parses but drops these, it's no more
|
|
/// useful than not parsing.
|
|
#[test]
|
|
fn v2_printing_report_carries_progress_and_temperatures() {
|
|
let value = load("push_status_v2_h2c_printing.json");
|
|
let report: PrintReport = serde_json::from_value(value).expect("should parse");
|
|
|
|
assert_eq!(report.gcode_state.as_deref(), Some("RUNNING"));
|
|
assert_eq!(report.mc_percent, Some(13));
|
|
assert_eq!(report.layer_num, Some(2));
|
|
assert_eq!(report.total_layer_num, Some(50));
|
|
assert_eq!(report.mc_remaining_time, Some(102));
|
|
assert_eq!(report.nozzle_temper, Some(230.0));
|
|
assert_eq!(report.subtask_name.as_deref(), Some("\u{15b}wieczka-24.stl"));
|
|
}
|
|
|
|
/// An idle printer still has to produce a coherent report - just with
|
|
/// nothing printing.
|
|
#[test]
|
|
fn v1_idle_report_reads_as_idle() {
|
|
let value = load("push_status_v1_p1p_idle.json");
|
|
let report: PrintReport = serde_json::from_value(value).expect("should parse");
|
|
|
|
assert_eq!(report.gcode_state.as_deref(), Some("IDLE"));
|
|
assert_eq!(report.mc_percent, Some(0));
|
|
}
|
|
|
|
/// v1 and v2 disagree about tray field names (`tray_temp`/`tray_time` vs
|
|
/// `drying_temp`/`drying_time`) - this is the specific mismatch that made
|
|
/// every v1 push carrying AMS data fail. Both spellings have to work.
|
|
#[test]
|
|
fn both_protocol_generations_parse_their_ams_trays() {
|
|
for name in ["push_status_v1_p1p_idle.json", "push_status_v2_h2c_printing.json"] {
|
|
let value = load(name);
|
|
let report: PrintReport = serde_json::from_value(value).unwrap_or_else(|err| {
|
|
panic!("{name}: {err}");
|
|
});
|
|
let ams = report.ams.expect("fixture should carry AMS state");
|
|
assert!(!ams.ams.is_empty(), "{name}: expected at least one AMS unit");
|
|
}
|
|
}
|
|
|
|
fn parse(name: &str) -> PrintReport {
|
|
serde_json::from_value(load(name)).unwrap_or_else(|err| panic!("{name}: {err}"))
|
|
}
|
|
|
|
/// The P1P sends an external spool; pin the fields a farm would show.
|
|
#[test]
|
|
fn v1_report_carries_the_external_spool() {
|
|
let report = parse("push_status_v1_p1p_idle.json");
|
|
let vt = report.vt_tray.expect("v1 fixture carries vt_tray");
|
|
|
|
assert_eq!(vt.id, "254");
|
|
assert_eq!(vt.tray_info_idx.as_deref(), Some("P9599a25"));
|
|
assert_eq!(vt.tray_type.as_deref(), Some("PETG"));
|
|
assert_eq!(vt.tray_color.as_deref(), Some("D3C5A3FF"));
|
|
assert_eq!(vt.cali_idx, Some(-1));
|
|
assert_eq!(vt.n, Some(1));
|
|
}
|
|
|
|
/// The H2C has no `vt_tray` key at all. That is the observed v1/v2
|
|
/// divergence, NOT a parse failure: if a change makes `vt_tray` required,
|
|
/// `parse` panics here instead of every v2 report silently vanishing in
|
|
/// `handle_incoming_report`.
|
|
#[test]
|
|
fn v2_report_has_no_external_spool_and_still_parses() {
|
|
let report = parse("push_status_v2_h2c_printing.json");
|
|
assert!(report.vt_tray.is_none());
|
|
}
|
|
|
|
/// `stg_cur` is what tells "preparing" from "stalled" while `layer_num` is 0.
|
|
#[test]
|
|
fn stage_fields_parse_on_both_generations() {
|
|
let v1 = parse("push_status_v1_p1p_idle.json");
|
|
assert_eq!(v1.stg_cur, Some(0));
|
|
assert_eq!(v1.stg, Some(vec![]));
|
|
|
|
let v2 = parse("push_status_v2_h2c_printing.json");
|
|
assert_eq!(v2.stg_cur, Some(0));
|
|
assert_eq!(v2.stg, Some(vec![64, 29, 13, 74, 72, 4, 54, 39, 8, 14, 1, 3]));
|
|
}
|
|
|
|
/// A v1 partial push without these must not erase what a full one set.
|
|
#[test]
|
|
fn merge_keeps_stage_and_spool_when_a_partial_update_omits_them() {
|
|
let mut base = parse("push_status_v1_p1p_idle.json");
|
|
let mut partial = parse("push_status_v1_p1p_idle.json");
|
|
partial.stg_cur = None;
|
|
partial.stg = None;
|
|
partial.vt_tray = None;
|
|
base.stg_cur = Some(54);
|
|
base.stg = Some(vec![54, 14, 13]);
|
|
|
|
base.merge_from(partial);
|
|
|
|
assert_eq!(base.stg_cur, Some(54));
|
|
assert_eq!(base.stg, Some(vec![54, 14, 13]));
|
|
assert!(base.vt_tray.is_some());
|
|
}
|
|
|
|
/// A P1P mid-print pushes `ams` objects without the inner unit list. That
|
|
/// used to fail the whole report ("missing field `ams`"), silencing telemetry
|
|
/// for the rest of the print - found on the first real dispatch.
|
|
#[test]
|
|
fn a_partial_ams_object_parses_and_keeps_known_units() {
|
|
let partial = serde_json::json!({"command":"push_status","sequence_id":"1","msg":1,"gcode_state":"RUNNING","ams":{"tray_now":"2"}});
|
|
let report: PrintReport = serde_json::from_value(partial).expect("a partial ams object should parse");
|
|
assert!(report.ams.as_ref().is_some_and(|a| a.ams.is_empty()));
|
|
}
|