Serve P1/A1 camera frames over the uplink
A P1 or A1 has no RTSP endpoint; its camera is a TLS stream on port 6000. bambu_camera logs in and reads one JPEG frame, and the uplink answers a camera_snapshot_request with it. Telemetry advertises camera_snapshot for printers that report a camera block but no RTSP URL.
This commit is contained in:
@@ -316,6 +316,12 @@ async fn serve(
|
||||
outbound.clone(),
|
||||
);
|
||||
}
|
||||
// Spawned: a connect, a TLS handshake and a frame
|
||||
// from the printer take seconds, and awaiting that
|
||||
// here would stall heartbeats and telemetry.
|
||||
Ok(ServerMessage::CameraSnapshotRequest { request_id, printer_id }) => {
|
||||
spawn_snapshot(fleet.clone(), request_id, printer_id, outbound.clone());
|
||||
}
|
||||
// Spawned: connecting to the new printer can take
|
||||
// many seconds, and awaiting it here would stall
|
||||
// heartbeats and telemetry for the whole farm.
|
||||
@@ -401,6 +407,12 @@ async fn collect_telemetry(fleet: &SharedFleet) -> Vec<PrinterTelemetry> {
|
||||
report.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()),
|
||||
)
|
||||
.then_some(true),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -555,6 +567,48 @@ fn spawn_webrtc_negotiation(
|
||||
});
|
||||
}
|
||||
|
||||
/// Reads one camera frame off this connection's task and reports it.
|
||||
///
|
||||
/// The fleet lock is held only to copy out what the read needs (the access
|
||||
/// code never leaves this process in the message), never across the network.
|
||||
fn spawn_snapshot(
|
||||
fleet: SharedFleet,
|
||||
request_id: String,
|
||||
printer_id: String,
|
||||
outbound: tokio::sync::mpsc::Sender<ClientMessage>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let job = {
|
||||
let fleet = fleet.lock().await;
|
||||
match fleet.iter().find(|p| p.base().id == printer_id) {
|
||||
Some(printer) => printer.snapshot_job().map_err(|err| err.to_string()),
|
||||
None => Err(format!("no printer '{printer_id}' in this gateway's fleet")),
|
||||
}
|
||||
};
|
||||
let result = match job {
|
||||
Ok(job) => job.run().await.map_err(|err| err.to_string()),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
let reply = match result {
|
||||
Ok(jpeg) => {
|
||||
use base64::Engine;
|
||||
ClientMessage::CameraSnapshot {
|
||||
request_id,
|
||||
jpeg_base64: Some(base64::engine::general_purpose::STANDARD.encode(jpeg)),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(%printer_id, %err, "could not read a camera frame");
|
||||
ClientMessage::CameraSnapshot { request_id, jpeg_base64: None, error: Some(err) }
|
||||
}
|
||||
};
|
||||
if outbound.send(reply).await.is_err() {
|
||||
warn!(%printer_id, "uplink is shutting down; the viewer gets no picture");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Finds the printer by our own id and runs the command. The error is a
|
||||
/// `String` rather than a typed error because its only destination is a
|
||||
/// `CommandResult` going back to the cloud as text.
|
||||
|
||||
Reference in New Issue
Block a user