Model vt_tray and the stage fields, and use them
## vt_tray closes a hole in the filament decision
`filament_source_for` used to refuse a project that wanted the external
spool, on a printer that had an AMS, purely because there was no way to see
what the spool held - and claiming a match without looking is how the
`use_ams: false` bug happened. Now that `vt_tray` is on `PrintReport`, the
question can be answered: `external_spool_satisfies` reuses `map_to_trays`
with the spool as the single candidate tray, so an `Ok` means every filament
matched it.
The decision is now: the AMS first when there is one, because a project
sliced for the AMS should use the AMS even if the spool happens to hold the
same filament; the spool if nothing in the AMS fits and the spool does; and
an error otherwise, reporting the AMS mismatch rather than the spool one
since that names what was wanted against what was loaded. A reported spool
that does not match is a real negative now and is refused.
One case stays unverifiable: a v2 printer reporting neither AMS trays nor
`vt_tray`. v2 omits the key entirely, so absence means "not reported", not
"empty". That one is logged as an unverified assumption instead of being
passed off as a match - and it is the same reason `EXTERNAL_SPOOLS` is still
a constant. Counting reported spools would give 0 on exactly the printer the
ams_mapping schema says has two.
Tests use the real P1P capture's own spool (PETG P9599a25 in D3C5A3FF): it
satisfies a project asking for that filament, and does not satisfy one asking
for the AMS's blue GFG99. Those two were indistinguishable before.
## stage reaches the cloud
`stg_cur`/`stg` are modelled, and `stage` is now on `PrinterTelemetry`,
through `continuum-schemas` and the ts-types mirror. Without it progress is
ambiguous in a way that reads as a fault: on the confirmed P1P run `percent`
climbed to 14% while `layer` stayed 0 for minutes, the printer walking
`stg_cur` 54 -> 14 -> 13 -> 0 before laying the first layer. A console
showing 14% at layer 0 with no stage looks stuck.
A zero stage means "printing" and has its own test on both sides, because
`skip_serializing_if = "Option::is_none"` plus one `unwrap_or_default` is all
it takes to turn "printing" into "no stage".
The backend test asserts a *wrongly typed* stage is rejected, not just that a
valid one is accepted: `PrinterTelemetrySchema` permits unknown properties -
verified - so `{ stage: 54 }` validates whether or not the field is declared.
The negative is what fails if someone removes it.
The live capture fixture is left alone. It predates the field, the field is
optional, and editing a capture by hand would defeat the point of having one.
This commit is contained in:
+198
-29
@@ -244,10 +244,16 @@ const SLOTS_PER_AMS: i32 = 4;
|
||||
/// External spools, which `ams_mapping` has to be padded for even though it
|
||||
/// cannot address them.
|
||||
///
|
||||
/// 1 is right for every printer this has been used with. A dual-nozzle H2C
|
||||
/// has two, and this should come from the report rather than a constant once
|
||||
/// `vt_tray` is modelled on `PrintReport` - the ams_mapping schema spells
|
||||
/// out the arithmetic.
|
||||
/// Still a constant now that `vt_tray` is modelled, because modelling it did
|
||||
/// not make the count derivable. A v1 P1P reports one `vt_tray` object, which
|
||||
/// confirms 1 for that printer - but a v2 H2C omits the key entirely (see
|
||||
/// `PrintReport::vt_tray`), so on v2 an absent `vt_tray` means "not reported"
|
||||
/// and not "no external spool". Counting what is reported would therefore
|
||||
/// give 0 on exactly the printer the ams_mapping schema says has *two*.
|
||||
///
|
||||
/// 1 is right for every printer this has run against. A dual-nozzle H2C has
|
||||
/// two, and getting that right needs a v2 capture that actually shows how a
|
||||
/// v2 printer describes its spools.
|
||||
const EXTERNAL_SPOOLS: usize = 1;
|
||||
|
||||
/// Flattens the AMS report into trays with global slot numbers, plus the
|
||||
@@ -291,6 +297,47 @@ fn available_trays(ams: Option<&super::AmsState>) -> (Vec<super::AvailableTray>,
|
||||
(trays, total)
|
||||
}
|
||||
|
||||
/// Whether the printer's external spool holds something every one of this
|
||||
/// project's filaments could be printed from.
|
||||
///
|
||||
/// Reuses `map_to_trays` with the spool presented as the single candidate
|
||||
/// tray, rather than reimplementing the match. With exactly one candidate,
|
||||
/// an `Ok` means every filament matched that one spool - which is the
|
||||
/// question being asked - and the slot numbers it returns are thrown away,
|
||||
/// since the external spool is not addressable by slot. Hence the `0`
|
||||
/// placeholder.
|
||||
///
|
||||
/// The point of this is to turn "a project wants the external spool" from an
|
||||
/// error into a correct answer. Before `vt_tray` was modelled there was no
|
||||
/// way to see what the spool held, and claiming a match without looking is
|
||||
/// how the `use_ams: false` bug happened in the first place.
|
||||
fn external_spool_satisfies(
|
||||
filaments: &[super::ProjectFilament],
|
||||
spool: &super::VtTray,
|
||||
) -> bool {
|
||||
let (Some(tray_info_idx), Some(filament_type), Some(color)) = (
|
||||
spool.tray_info_idx.as_ref(),
|
||||
spool.tray_type.as_ref(),
|
||||
spool.tray_color.as_ref(),
|
||||
) else {
|
||||
// An empty spool reports these as absent or blank. Nothing to match.
|
||||
return false;
|
||||
};
|
||||
|
||||
if tray_info_idx.is_empty() || filament_type.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let candidate = [super::AvailableTray {
|
||||
slot: 0,
|
||||
tray_info_idx: tray_info_idx.clone(),
|
||||
filament_type: filament_type.clone(),
|
||||
color: super::normalise_color(color),
|
||||
}];
|
||||
|
||||
super::map_to_trays(filaments, &candidate).is_ok()
|
||||
}
|
||||
|
||||
/// What a printer's light should be doing.
|
||||
///
|
||||
/// Only the two steady states. Bambu's `system.ledctrl` also has a
|
||||
@@ -770,14 +817,18 @@ impl BambuPrinter {
|
||||
/// project "work". An unmatched filament is an error, because every
|
||||
/// alternative is a guess about what someone meant to print.
|
||||
///
|
||||
/// The one case it does answer `ExternalSpool` for is a printer
|
||||
/// reporting no AMS trays at all, where there is nothing else it could
|
||||
/// mean. Note the asymmetry that leaves: a printer *with* an AMS whose
|
||||
/// project wants the external spool is an error here, not an
|
||||
/// `ExternalSpool`, because `vt_tray` is not modelled on `PrintReport` -
|
||||
/// so there is no way to check that the spool holds the right material,
|
||||
/// and claiming a match without checking is how this went wrong the
|
||||
/// first time. Modelling `vt_tray` is what fixes that.
|
||||
/// It answers `ExternalSpool` when the spool is what the project asks
|
||||
/// for - checked against `vt_tray`, not assumed. A printer with an AMS
|
||||
/// whose project wants the spool used to be an error here purely because
|
||||
/// `vt_tray` was not modelled; now that it is, the question can be
|
||||
/// answered instead of refused.
|
||||
///
|
||||
/// The one place it still answers `ExternalSpool` without being able to
|
||||
/// verify anything is a v2 printer that reports no AMS trays and no
|
||||
/// `vt_tray` either - v2 omits the key entirely (see
|
||||
/// `PrintReport::vt_tray`), so there is nothing to check against and
|
||||
/// nothing else it could mean. That one is logged as unverified rather
|
||||
/// than passed off as a match.
|
||||
pub async fn filament_source_for(
|
||||
&self,
|
||||
archive: &[u8],
|
||||
@@ -794,26 +845,68 @@ impl BambuPrinter {
|
||||
})?;
|
||||
|
||||
let (trays, tray_count) = available_trays(report.ams.as_ref());
|
||||
let spool = report.vt_tray.as_ref();
|
||||
|
||||
if trays.is_empty() {
|
||||
tracing::info!(
|
||||
printer = %self.base.name,
|
||||
"no AMS trays reported; printing from the external spool"
|
||||
);
|
||||
return Ok(FilamentSource::ExternalSpool);
|
||||
// The AMS first, when there is one: a project sliced for the AMS
|
||||
// naming a filament that also happens to be on the spool should
|
||||
// still use the AMS, because that is what it was sliced for.
|
||||
if !trays.is_empty() {
|
||||
match super::map_to_trays(&filaments, &trays) {
|
||||
Ok(mapping) => {
|
||||
tracing::info!(
|
||||
printer = %self.base.name,
|
||||
?mapping,
|
||||
tray_count,
|
||||
"mapped the project's filaments onto AMS slots"
|
||||
);
|
||||
return FilamentSource::ams(mapping, tray_count);
|
||||
}
|
||||
Err(ams_err) => {
|
||||
// Nothing in the AMS fits. The spool is the only other
|
||||
// thing it could have meant, so check it rather than
|
||||
// failing outright - but report the AMS error if the
|
||||
// spool does not fit either, since that one names what
|
||||
// was wanted and what was loaded.
|
||||
if spool.is_some_and(|spool| external_spool_satisfies(&filaments, spool)) {
|
||||
tracing::info!(
|
||||
printer = %self.base.name,
|
||||
"no AMS tray fits this project, but the external spool does"
|
||||
);
|
||||
return Ok(FilamentSource::ExternalSpool);
|
||||
}
|
||||
return Err(PrinterError(format!("{}: {ams_err}", self.base.name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mapping = super::map_to_trays(&filaments, &trays)
|
||||
.map_err(|err| PrinterError(format!("{}: {err}", self.base.name)))?;
|
||||
|
||||
tracing::info!(
|
||||
printer = %self.base.name,
|
||||
?mapping,
|
||||
tray_count,
|
||||
"mapped the project's filaments onto AMS slots"
|
||||
);
|
||||
|
||||
FilamentSource::ams(mapping, tray_count)
|
||||
match spool {
|
||||
Some(spool) if external_spool_satisfies(&filaments, spool) => {
|
||||
tracing::info!(
|
||||
printer = %self.base.name,
|
||||
"no AMS; the external spool matches this project"
|
||||
);
|
||||
Ok(FilamentSource::ExternalSpool)
|
||||
}
|
||||
// A spool is reported and does not match. Now that `vt_tray`
|
||||
// exists this is a real negative rather than an unknown, so it
|
||||
// is worth refusing on.
|
||||
Some(spool) => Err(PrinterError(format!(
|
||||
"{}: no AMS trays, and the external spool holds {} {} ({}), \
|
||||
which does not match this project",
|
||||
self.base.name,
|
||||
spool.tray_type.clone().unwrap_or_else(|| "nothing".into()),
|
||||
spool.tray_info_idx.clone().unwrap_or_default(),
|
||||
spool.tray_color.clone().unwrap_or_default(),
|
||||
))),
|
||||
// v2 omits `vt_tray`, so this is "not reported", not "empty".
|
||||
None => {
|
||||
tracing::warn!(
|
||||
printer = %self.base.name,
|
||||
"no AMS trays and no vt_tray reported; assuming the external spool, unverified"
|
||||
);
|
||||
Ok(FilamentSource::ExternalSpool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads one file back off the printer's SD card.
|
||||
@@ -1312,6 +1405,82 @@ mod tests {
|
||||
assert_eq!(body["ams_mapping"], serde_json::json!(""));
|
||||
}
|
||||
|
||||
fn real_report() -> super::super::PrintReport {
|
||||
let raw = include_str!("../../tests/fixtures/push_status_v1_p1p_idle.json");
|
||||
let value: serde_json::Value = serde_json::from_str(raw).expect("fixture is json");
|
||||
let body = value.get("print").unwrap_or(&value);
|
||||
serde_json::from_value(body.clone()).expect("fixture should parse")
|
||||
}
|
||||
|
||||
fn filament(tray_info_idx: &str, filament_type: &str, color: &str) -> super::super::ProjectFilament {
|
||||
super::super::ProjectFilament {
|
||||
index: 0,
|
||||
tray_info_idx: tray_info_idx.into(),
|
||||
filament_type: filament_type.into(),
|
||||
color: color.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The real P1P's external spool: PETG `P9599a25` in `D3C5A3FF`.
|
||||
#[test]
|
||||
fn the_real_external_spool_satisfies_a_project_asking_for_it() {
|
||||
let report = real_report();
|
||||
let spool = report.vt_tray.as_ref().expect("the v1 fixture has a vt_tray");
|
||||
|
||||
assert!(external_spool_satisfies(
|
||||
&[filament("P9599a25", "PETG", "D3C5A3")],
|
||||
spool
|
||||
));
|
||||
}
|
||||
|
||||
/// The case that used to be indistinguishable from the one above: the
|
||||
/// project wants a filament the spool does not hold. Before `vt_tray`
|
||||
/// was modelled there was no way to tell these apart, so the code
|
||||
/// assumed a match - which is the whole `use_ams: false` bug.
|
||||
#[test]
|
||||
fn the_real_external_spool_does_not_satisfy_a_different_filament() {
|
||||
let report = real_report();
|
||||
let spool = report.vt_tray.as_ref().expect("the v1 fixture has a vt_tray");
|
||||
|
||||
// Right material, wrong product and colour - the AMS's blue GFG99.
|
||||
assert!(!external_spool_satisfies(
|
||||
&[filament("GFG99", "PETG", "2850E0")],
|
||||
spool
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_external_spool_satisfies_nothing() {
|
||||
let empty = super::super::VtTray {
|
||||
id: "254".into(),
|
||||
tray_info_idx: Some(String::new()),
|
||||
tray_type: Some(String::new()),
|
||||
tray_color: Some("00000000".into()),
|
||||
bed_temp: None,
|
||||
bed_temp_type: None,
|
||||
nozzle_temp_max: None,
|
||||
nozzle_temp_min: None,
|
||||
remain: None,
|
||||
tag_uid: None,
|
||||
tray_diameter: None,
|
||||
tray_id_name: None,
|
||||
tray_sub_brands: None,
|
||||
tray_uuid: None,
|
||||
tray_weight: None,
|
||||
xcam_info: None,
|
||||
tray_temp: None,
|
||||
tray_time: None,
|
||||
cali_idx: None,
|
||||
k: None,
|
||||
n: None,
|
||||
};
|
||||
|
||||
assert!(!external_spool_satisfies(
|
||||
&[filament("P9599a25", "PETG", "D3C5A3")],
|
||||
&empty
|
||||
));
|
||||
}
|
||||
|
||||
/// The AMS half of the mapping, against the real P1P report in
|
||||
/// `tests/fixtures/push_status_v1_p1p_idle.json`: one unit, slots 0 and
|
||||
/// 1 empty, slots 2 and 3 holding PETG in two different colours.
|
||||
|
||||
Reference in New Issue
Block a user