Drive Klipper, OctoPrint and PrusaLink for real
Windows discovery tool / windows (push) Successful in 1m2s

The three were println stubs. Each is now an HTTP client (Moonraker,
OctoPrint's REST API, PrusaLink's v1 API) behind a small shared helper:
status polled in the background, pause/resume/cancel, G-code lines, fan
speed, and upload-and-start of a sliced file. A slicer project (.3mf) is
refused for these, and so is an upload while a print is running.

Telemetry and the completion watcher now read a vendor-neutral
PrinterStatus instead of Bambu's PrintReport, so these printers report
progress, temperatures and job state to the cloud.

Written from the public API docs and tested against mock servers only; no
real Klipper, OctoPrint or PrusaLink machine has been on the bench.
This commit is contained in:
2026-10-30 07:49:31 +00:00
parent b1e733a5fe
commit 9359add6aa
10 changed files with 1141 additions and 166 deletions
+19 -20
View File
@@ -374,29 +374,28 @@ async fn collect_telemetry(fleet: &SharedFleet) -> Vec<PrinterTelemetry> {
for printer in fleet.iter() {
let base = printer.base();
let Some(report) = printer.print_status().await else {
let Some(status) = printer.status().await else {
continue;
};
// Only Bambu has a camera report to derive these from.
let report = printer.print_status().await;
out.push(PrinterTelemetry {
printer_id: base.id.clone(),
name: base.name.clone(),
state: report.gcode_state.clone(),
percent: report.mc_percent,
stage: report.stg_cur,
layer: report.layer_num,
total_layers: report.total_layer_num,
remaining_minutes: report.mc_remaining_time,
// An idle printer reports `""` rather than omitting the field;
// empty-string-as-absent is worth normalising here so the cloud
// doesn't have to know that.
job_name: report.subtask_name.clone().filter(|s| !s.is_empty()),
nozzle_temp: report.nozzle_temper,
nozzle_target: report.nozzle_target_temper,
bed_temp: report.bed_temper,
bed_target: report.bed_target_temper,
print_error: report.print_error,
active_alerts: report.hms.as_ref().map(|h| h.len()),
state: status.state,
percent: status.percent,
stage: status.stage,
layer: status.layer,
total_layers: status.total_layers,
remaining_minutes: status.remaining_minutes,
job_name: status.job_name,
nozzle_temp: status.nozzle_temp,
nozzle_target: status.nozzle_target,
bed_temp: status.bed_temp,
bed_target: status.bed_target,
print_error: status.print_error,
active_alerts: status.active_alerts,
// Derived from the same reported URL `camera::sources` builds
// the go2rtc config from, so a printer is advertised as
// streamable here only if it is also actually being served
@@ -404,13 +403,13 @@ async fn collect_telemetry(fleet: &SharedFleet) -> Vec<PrinterTelemetry> {
// the rule means the two cannot drift apart.
camera_stream: crate::camera::source_for(
printer,
report.ipcam.as_ref().and_then(|c| c.rtsp_url.as_deref()),
report.as_ref().and_then(|r| r.ipcam.as_ref()).and_then(|c| c.rtsp_url.as_deref()),
)
.map(|source| source.stream_name),
camera_snapshot: crate::camera::wants_snapshot(
printer,
report.ipcam.is_some(),
report.ipcam.as_ref().and_then(|c| c.rtsp_url.as_deref()),
report.as_ref().is_some_and(|r| r.ipcam.is_some()),
report.as_ref().and_then(|r| r.ipcam.as_ref()).and_then(|c| c.rtsp_url.as_deref()),
)
.then_some(true),
});