7.3 KiB
Cloud print job: MQTT-native design (not yet implemented)
OrcaPrinterAgent::start_print currently finalizes a cloud print job over HTTP
(OrcaCloudServiceAgent::start_cloud_print_job, POST /api/v1/printers/<dev_id>/print-jobs/<job_id>/start). This document records
the MQTT-native alternative that was designed as the intended replacement, the
gap that blocks it today, and why it should not be built by adding fields to
print.gcode_file.
Current (implemented) flow
OrcaCloudServiceAgent::upload_gcode_via_cloudPOST print-jobs/uploads->{job_id, upload_url, expires_at, max_bytes}PUT upload_url-> raw G-code straight to R2 (presigned, PUT-only, no bearer token; must not go through thehttp_puthelper, which always prefixesapi_base_urland attaches the cloud session's Authorization header).
OrcaCloudServiceAgent::start_cloud_print_jobPOST print-jobs/<job_id>/startwith{filename, start}.- The gateway HEAD-verifies the R2 object landed, mints a short-lived
signed download URL (
createPrintJobDownloadTokeninapps/gateway/src/services/printer-print-jobs.ts), and relays aprint.project_filecommand carrying that URL to the printer - over the gateway's own relay connection to OrcaSonar, not OrcaSlicer's MQTT session. OrcaSonar'sexecuteProjectFile(internal/cloud/printfile.go) downloads from that URL and starts the print.
This works today and requires no changes to OrcaCloud or OrcaSonar.
Why an MQTT-native version is desirable
The HTTP finalize call means start_print's cloud path depends on the
client's REST session (auth token, network path to the gateway's HTTP API)
in addition to its MQTT session. An MQTT-native version would let OrcaSlicer
trigger the download-and-start entirely over the connection it already
maintains for every other printer command.
The key finding: OrcaSonar already supports this, transport-agnostically
print.project_file is not tied to the HTTP /start route. OrcaSonar's
cloud MQTT message handler intercepts it purely by command name, before
routing to the generic per-namespace dispatcher:
// internal/cloud/client.go, makeRequestHandler
if cmd.Namespace == "print" && cmd.Name == "project_file" {
go c.handleProjectFile(ctx, cmd) // downloads `url`, then starts if start=true
return
}
This fires for any message that reaches OrcaSonar's own outbound cloud
MQTT session on device/<dev_id>/request - regardless of whether it was
published there by the gateway's internal relay (today's /start path) or
by a client publishing directly. OrcaSlicer's cloud MQTT connection already
publishes to that same topic for every other cloud command
(OrcaPrinterAgent::route_send(is_lan=false, ...) - set_bed_temp,
ams_change_filament, etc.), via the same relay-shard mechanism
(apps/gateway/src/lib/relay-router.ts). So OrcaSlicer publishing
print.project_file itself, over its existing cloud MQTT connection, would
already reach handleProjectFile and trigger the identical download-then-
start behavior - with zero new code on OrcaSonar.
(This only works over the cloud relay session. OrcaSonar's LAN-side/generic
dispatcher, internal/bridge/klipper/adapter.go, treats print.project_file
as an unmapped macro call - a no-op in practice. That's fine: R2 upload is a
cloud-only feature to begin with.)
The one real gap: no GET-signed download URL is exposed to the client
handleProjectFile needs a URL it can GET. The upload_url returned by
POST print-jobs/uploads is presigned for PUT only - S3 SigV4 signatures
are bound to the HTTP method, so it cannot be reused for a download.
Minting a download URL/token already exists as a function
(createPrintJobDownloadToken in
apps/gateway/src/services/printer-print-jobs.ts) and the exact URL
template is already built in dispatchPrintFileCommand
(apps/gateway/src/routes/printer.ts) - it is simply never returned to the
API caller today, only used server-side when /start builds the relay
payload itself.
Required OrcaCloud change: have POST print-jobs/uploads (or a small
follow-up call) also mint and return a signed download URL alongside
upload_url, reusing createPrintJobDownloadToken + the existing URL
template. This is on the order of ~10 lines in an existing handler, not a new
permission model - the caller is already an authenticated, authorized user of
that printer, identically to who is authorized to call /start today.
No OrcaSonar change is required at all.
Why this should NOT be built into print.gcode_file / start_sdcard_print
print.gcode_file (sent by OrcaPrinterAgent::start_sdcard_print) is the
generic "start this file that is already on the printer" primitive. It is
used by the LAN start_local_print path today, is meant to stay usable for
starting any file already on the SD card by filename alone, and is expected
to grow parameters unrelated to cloud upload (e.g. filament mapping) over
time.
Making gcode_file download-aware would require either:
- adding cloud-specific fields (
job_id, a downloadurl, ...) to a command that has nothing to do with cloud jobs in the LAN case, forcing all of them to be optional/unused most of the time, or - giving OrcaSonar a side-channel registry of "filenames currently being
downloaded" that
gcode_file's handler consults - solvable, but an orthogonal change with its own design questions (see "decoupled two-step option" below).
Neither is necessary: print.project_file already exists as a fully
separate, fully-working command for exactly the "not yet on the printer,
fetch it first" case, so the cloud upload flow does not need to touch
gcode_file at all.
Intended MQTT-native design, once the gap above is closed
Replace the HTTP finalize step (start_cloud_print_job) with: build and
publish, over the cloud MQTT connection (route_send(is_lan=false, ...)),
{"print": {"command": "project_file", "sequence_id": "...",
"url": "<signed download URL from the uploads response>",
"param": "<remote_gcode_name(params)>",
"start": true}}
start_sdcard_print / print.gcode_file remains untouched and fully
decoupled.
Decoupled two-step option
If a use case ever needs "download now, start later" as an explicit user
action (rather than upload-and-immediately-print), the same
print.project_file command already supports it via start: false (download
and store only - see executeProjectFile's start handling in
internal/cloud/printfile.go). The later "start" action would then be a
perfectly ordinary print.gcode_file with param: <filename>, going through
the existing, generic start_sdcard_print unmodified. This still requires no
protocol changes beyond the download-URL gap above.
Summary of gaps
| Component | Change needed |
|---|---|
| OrcaCloud (gateway) | Return a signed download URL from POST print-jobs/uploads (or a small sibling endpoint), reusing existing createPrintJobDownloadToken logic. |
| OrcaSonar | None. print.project_file handling already does exactly what's needed, transport-agnostically, on the cloud MQTT session. |
| OrcaSlicer (this repo) | Once the above lands: replace start_cloud_print_job's HTTP call with a print.project_file publish over the cloud MQTT connection. start_sdcard_print stays untouched either way. |