From 1b1df9c438e2020a62d24b2733c863fb581a891c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Thu, 29 Oct 2026 15:53:50 +0000 Subject: [PATCH] Add camera snapshot messages and flag for P1/A1 cameras camera_snapshot_request (cloud to gateway) and camera_snapshot (gateway to cloud) carry one JPEG frame at a time; telemetry gains camera_snapshot and the app view gains cameraSnapshot. --- packages/ts-types/src/schemas/app.ts | 5 ++++ packages/ts-types/src/schemas/edge.ts | 26 +++++++++++++++++++ .../v1/app/telemetry_message.schema.json | 4 +++ .../v1/edge/client_message.schema.json | 12 +++++++++ .../v1/edge/printer_telemetry.schema.json | 6 ++++- .../v1/edge/server_message.schema.json | 12 +++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/ts-types/src/schemas/app.ts b/packages/ts-types/src/schemas/app.ts index edf3a7b..6dd22b1 100644 --- a/packages/ts-types/src/schemas/app.ts +++ b/packages/ts-types/src/schemas/app.ts @@ -62,6 +62,11 @@ export const AppPrinterStateSchema = Type.Object({ * calling; null means render no player at all. */ cameraStream: Type.Optional(Nullable(Type.String())), + /** + * True when the camera can only be read as refreshing JPEG frames (Bambu + * P1/A1); `GET /printers/:id/stream` then answers `kind: "snapshot"`. + */ + cameraSnapshot: Type.Optional(Type.Boolean()), /** * When a gateway last reported this printer; null means never. This is how * a client tells "idle" from "nobody has heard from it in an hour", which diff --git a/packages/ts-types/src/schemas/edge.ts b/packages/ts-types/src/schemas/edge.ts index a7456c3..e495c4d 100644 --- a/packages/ts-types/src/schemas/edge.ts +++ b/packages/ts-types/src/schemas/edge.ts @@ -65,6 +65,11 @@ export const PrinterTelemetrySchema = Type.Object({ * implements yet. */ camera_stream: Maybe(Type.String()), + /** + * The camera can only be read as single JPEG frames (Bambu P1/A1) and the + * gateway answers `camera_snapshot_request` for it. + */ + camera_snapshot: Maybe(Type.Boolean()), layer: Maybe(Type.Integer({ minimum: 0 })), total_layers: Maybe(Type.Integer({ minimum: 0 })), remaining_minutes: Maybe(Type.Integer({ minimum: 0 })), @@ -296,6 +301,13 @@ export const EdgeClientMessageSchema = Type.Union([ session_id: Type.String(), message: Type.String(), }), + /** One JPEG frame from a `camera_snapshot_request`, or why there is none. */ + Type.Object({ + type: Type.Literal("camera_snapshot"), + request_id: Type.String(), + jpeg_base64: Maybe(Type.String()), + error: Maybe(Type.String()), + }), Type.Object({ type: Type.Literal("frame_batch"), printer_id: Type.String(), @@ -367,6 +379,20 @@ export const EdgeServerMessageSchema = Type.Union([ /** The browser's offer SDP, passed through verbatim. */ sdp: Type.String(), }, { additionalProperties: false }), + /** + * Read one JPEG frame from a printer whose camera is not RTSP (Bambu P1/A1). + * Unlike `webrtc_offer` the picture itself crosses the cloud: frames are + * small and requested one at a time. + */ + Type.Object( + { + type: Type.Literal("camera_snapshot_request"), + request_id: Type.String(), + /** The gateway's own printer id. */ + printer_id: Type.String(), + }, + { additionalProperties: false }, + ), Type.Object({ type: Type.Literal("command"), command_id: Type.String(), diff --git a/schemas/continuum/v1/app/telemetry_message.schema.json b/schemas/continuum/v1/app/telemetry_message.schema.json index 758edbc..7157f29 100644 --- a/schemas/continuum/v1/app/telemetry_message.schema.json +++ b/schemas/continuum/v1/app/telemetry_message.schema.json @@ -62,6 +62,10 @@ "type": ["string", "null"], "description": "The go2rtc stream name, when this printer has a camera that can actually be restreamed. Null is the common case - only Bambu's X1/H2 class publishes an RTSP URL. Present means it is worth calling GET /printers/:id/stream; null means render no player." }, + "cameraSnapshot": { + "type": "boolean", + "description": "True when the camera can only be read as refreshing JPEG frames (Bambu P1/A1); GET /printers/:id/stream then answers kind 'snapshot'." + }, "lastSeenAt": { "type": ["string", "null"], "format": "date-time", diff --git a/schemas/continuum/v1/edge/client_message.schema.json b/schemas/continuum/v1/edge/client_message.schema.json index 06f610f..d495c79 100644 --- a/schemas/continuum/v1/edge/client_message.schema.json +++ b/schemas/continuum/v1/edge/client_message.schema.json @@ -183,6 +183,18 @@ "message" ] }, + { + "type": "object", + "title": "CameraSnapshot", + "description": "The answer to a CameraSnapshotRequest: one JPEG frame (base64), or the reason there is none.", + "properties": { + "type": { "const": "camera_snapshot" }, + "request_id": { "type": "string" }, + "jpeg_base64": { "type": "string" }, + "error": { "type": "string", "description": "Set instead of jpeg_base64 when no frame could be read." } + }, + "required": ["type", "request_id"] + }, { "type": "object", "title": "DiscoveredPrinters", diff --git a/schemas/continuum/v1/edge/printer_telemetry.schema.json b/schemas/continuum/v1/edge/printer_telemetry.schema.json index 0dcb351..8cf80a1 100644 --- a/schemas/continuum/v1/edge/printer_telemetry.schema.json +++ b/schemas/continuum/v1/edge/printer_telemetry.schema.json @@ -108,7 +108,11 @@ }, "camera_stream": { "type": "string", - "description": "The stream name to ask the gateway's stream_base_url for, when this printer has a camera the gateway could restream. Absent means no stream: either the printer reports no camera source, or it is a model whose camera is not reachable this way. That second case is the common one today - a Bambu P1/A1 does not publish an RTSP URL at all (confirmed: a P1P's ipcam block has ipcam_dev and ipcam_record and no rtsp_url), and only the X1/H2 class does. Those need Bambu's proprietary port-6000 JPEG stream instead, which is a separate client this gateway does not have yet." + "description": "The stream name to ask the gateway's stream_base_url for, when this printer has a camera the gateway could restream. Absent means no stream: either the printer reports no camera source, or it is a model whose camera is not reachable this way. That second case is the common one today - a Bambu P1/A1 does not publish an RTSP URL at all (confirmed: a P1P's ipcam block has ipcam_dev and ipcam_record and no rtsp_url), and only the X1/H2 class does. Those are served by camera_snapshot instead." + }, + "camera_snapshot": { + "type": "boolean", + "description": "True when this printer's camera can only be read as single JPEG frames (a Bambu P1/A1: proprietary TLS stream on port 6000, no RTSP) and the gateway will answer a CameraSnapshotRequest for it. Mutually exclusive with camera_stream in practice. Absent means false." } }, "required": [ diff --git a/schemas/continuum/v1/edge/server_message.schema.json b/schemas/continuum/v1/edge/server_message.schema.json index d5d0d47..ba5d1df 100644 --- a/schemas/continuum/v1/edge/server_message.schema.json +++ b/schemas/continuum/v1/edge/server_message.schema.json @@ -110,6 +110,18 @@ ], "additionalProperties": false }, + { + "type": "object", + "title": "CameraSnapshotRequest", + "description": "Read one JPEG frame from a printer's camera and send it back as a CameraSnapshot. For the printers whose camera is not RTSP (Bambu P1/A1, telemetry camera_snapshot true). Unlike WebrtcOffer this DOES carry the picture through the cloud, which is acceptable only because the frames are small, low-rate and requested one at a time.", + "properties": { + "type": { "const": "camera_snapshot_request" }, + "request_id": { "type": "string" }, + "printer_id": { "type": "string", "description": "The gateway's own printer id." } + }, + "required": ["type", "request_id", "printer_id"], + "additionalProperties": false + }, { "type": "object", "title": "ProvisionPrinter",