Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a8ed9f3
feat: STL slicing and human-confirmed print dispatch
cyrilcaoyang Oct 5, 2026
567eba1
fix: refuse AMS HT trays in dispatch until their tray index is verified
cyrilcaoyang Oct 5, 2026
00be09a
fix: re-read the artifact at dispatch instead of trusting intake facts
cyrilcaoyang Oct 5, 2026
3e88f7c
feat: chamber light control verb for the plate check
cyrilcaoyang Oct 5, 2026
c65d298
docs: plan the P1S move to LAN Only + Developer Mode
cyrilcaoyang Oct 5, 2026
66dbc56
docs: plan remote Bambu Studio access through Tailscale
cyrilcaoyang Oct 12, 2026
5aab464
fix: drop the placeholder chamber reading on printers without a sensor
cyrilcaoyang Oct 20, 2026
4f85bcb
feat: dispatch from the external spool on printers without an AMS
cyrilcaoyang Oct 20, 2026
ab78dce
fix: accept the stock bed-mesh M500 in Bambu's machine start gcode
cyrilcaoyang Oct 20, 2026
5d42efc
feat: view-only camera on the submission page, off until turned on
cyrilcaoyang Oct 21, 2026
6c801fe
fix: flatten slicer presets so the CLI sees the whole machine
cyrilcaoyang Oct 21, 2026
53e2789
ui: drop the signed-in banner; the read-only name field already shows…
cyrilcaoyang Oct 21, 2026
f251db3
ui: send a standalone visit behind the edge to the dashboard's panel
cyrilcaoyang Oct 21, 2026
f4e89cc
ui: fixed-size camera frame beside the machine tiles, off until started
cyrilcaoyang Oct 21, 2026
8ccc062
ui: hide the name field when signed in; camera frame 1.5x larger
cyrilcaoyang Oct 21, 2026
2d83553
feat: drop the manual snapshot step; a camera start records its own f…
cyrilcaoyang Oct 21, 2026
6cd45d2
ui: printer bar with camera and light; one Submission section with ch…
cyrilcaoyang Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,24 @@ The following documents take precedence over this file and must not be weakened:
This service is a multi-printer gateway. It translates Bambu Lab's local MQTT
telemetry into one STATUS_SPEC v1.0 HTTP surface per configured printer.

The first release is monitoring-only. Do not add control endpoints or invoke
command methods from `bambulabs_api` without an explicitly approved design that
routes execution through `lab-skills`, implements claims and interlocks, and
conforms to STATUS_SPEC v1.1 or later.
Monitoring is the default. A human approved (2026-10-04) a narrow,
human-in-the-loop control plane, enabled per deployment by `dispatch.enabled`:
STATUS_SPEC v1.1 claims plus exactly four verbs -- camera snapshot, chamber
light (added 2026-10-05), `start_print` for an approved job after a person
confirms the plate is empty, and `stop_print`. On 2026-10-20 a human approved
the external spool (`vt_tray`) as a `start_print` mapping target for printers
without an AMS, checked like an AMS tray, and the one `M500` in Bambu's stock
start gcode (saving the bed mesh right after `G29`) as accepted by the gcode
check. On 2026-10-21 a view-only camera (`GET /printers/{id}/camera.jpg`, no
claim, one shared read per printer every 2 s, off on the page until a person
turns it on, never plate-check evidence) was approved, and with it the manual
snapshot step was dropped: a camera start records its own frame under the
claim when the person presses start. Homing was considered and
deliberately not added: it moves the bed into whatever is on the plate. All printer commands go through `bambu_server.dispatch` and
the backend's control methods; nothing else may call `bambulabs_api` command
methods. Adding a verb, relaxing a gate, or letting an agent start a print
without a person needs a new explicitly approved design. Routing control
through `lab-skills` is still owed.

## Working conventions

Expand Down
30 changes: 24 additions & 6 deletions AGENT_RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,31 @@ first and in full. The authoritative device contract is
`../ac-organic-lab/docs/STATUS_SPEC.md`. This file only adds rules specific to
the Bambu printer gateway.

1. The service is read-only until a human explicitly approves a control-plane
design integrated with `lab-skills`, cooperative claims, preconditions, and
audited plan execution.
1. Control is limited to what a human approved on 2026-10-04: cooperative
claims, a camera snapshot, the chamber light (approved 2026-10-05),
`start_print` of an approved job (from an AMS tray or, approved 2026-10-20,
the external spool, checked the same way; and, approved the same day, with
the stock start gcode's bed-mesh `M500` right after `G29` accepted -- every
other refused command still refused), and
`stop_print`, all behind `dispatch.enabled` and the gates in
`bambu_server.dispatch`. Any other verb, a relaxed gate, or integration with
`lab-skills` plan execution needs a new, explicitly approved design.
2. Monitoring must never start, pause, resume, stop, heat, home, move, calibrate,
upload to, or otherwise control a printer.
3. The service may request MQTT state refreshes in its background monitoring
loop. A dashboard request must never cause printer I/O.
4. Printer access codes, serial numbers, addresses, and raw MQTT payloads are
loop. A status, profile, or queue request must never cause printer I/O; only
a claimed `/control/*` request may, with one exception approved 2026-10-21:
the view-only `GET /printers/{id}/camera.jpg`, present only with
`dispatch.enabled`, which reads the camera port (never publishes a command),
at most once per printer every 2 s shared by all viewers, and whose frames
are never plate-check evidence. The page loads with it off; a person turns
it on.
4. A print is started only by a person who has confirmed the build plate is
empty. (Since 2026-10-21, approved, a camera confirmation needs no separate
snapshot step: the person checks the live view and the gateway records a
frame under the claim when they press start, refusing if it cannot.) An agent must never set `plate_confirmed_empty`, start a print, or
approve a job on its own initiative, and must never retry a failed or
uncertain dispatch -- a person checks the printer first.
5. Printer access codes, serial numbers, addresses, and raw MQTT payloads are
local secrets and must not be committed, returned by HTTP, or logged.
5. Tests and development defaults must not contact lab hardware.
6. Tests and development defaults must not contact lab hardware.
115 changes: 85 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
# AC Bambu Server

A monitoring-only FastAPI gateway for the Bambu Lab printers in the lab. It
uses [`bambulabs_api`](https://github.com/BambuTools/bambulabs_api) for local
MQTT telemetry and publishes one
A FastAPI gateway for the Bambu Lab printers in the lab. It uses
[`bambulabs_api`](https://github.com/BambuTools/bambulabs_api) for local MQTT
telemetry and publishes one
[AC lab STATUS_SPEC v1.2](../ac-organic-lab/docs/STATUS_SPEC.md) surface per
printer. This repo conforms to lab status spec v1.2 on its per-printer
surfaces; the aggregate gateway envelope stays on v1.0 (it fronts printers and
has no primary operation of its own).

The service deliberately exposes **no control endpoints**. The third-party
package supports commands, but those methods are isolated behind a narrow
monitoring adapter and are not reachable from HTTP. Future control work must go
through `lab-skills`, claims, preconditions, audited plans, and the
human-approval rules in the lab contract.
It runs a **submission pipeline**: remote users upload a print artifact (or an
`.stl` the gateway slices with Bambu Studio), the gateway validates it against
the target machine's profile, and valid jobs wait in a per-machine queue with
expected finish times. That path writes to the gateway's own disk and never to
a printer.

It also runs a **submission pipeline**: remote users upload a print artifact,
the gateway validates it against the target machine's profile, and valid jobs
wait in a per-machine queue with expected finish times. That whole path is
read-and-analysis — it writes to the gateway's own disk and never to a printer.
The one printer-touching step, dispatch, is not implemented; see
[Submission pipeline](#submission-pipeline).
By default the service is **monitoring-only** and exposes no control endpoints.
A deployment that sets `dispatch.enabled` gets a narrow, human-in-the-loop
control plane: claims plus four verbs (camera snapshot, chamber light, start an
approved job, stop), behind the gates described in [Control plane](#control-plane). The
third-party library's other command methods remain unreachable. Control is not
yet exposed as `lab-skills` skills; the only caller is a person on the `/ui`
page or a direct HTTP client holding a claim.

## Architecture

Expand All @@ -28,19 +29,24 @@ Bambu printers -- local MQTT/TLS --> background monitors --> cached status
|
Lab dashboard ---------------- HTTP GET /printers/{id}/status -+
|
Remote user -- POST /submissions --> validate against ------- + (reads the cache)
the machine profile
Remote user -- POST /submissions --> (.stl: Bambu Studio CLI slice)
validate against the ------+ (reads the cache)
machine profile
|
v
per-machine queue --> GET /printers/{id}/queue
| person approves
v
Person on /ui -- claim, snapshot, confirm plate, start_print (dispatch.enabled only)
|
x dispatch: not implemented
v
FTPS upload (size-verified) + MQTT start
```

Dashboard requests only read the cache. They never connect to a printer or
request a telemetry refresh. The background monitor starts only the MQTT client;
camera and FTP clients are not started. The submission pipeline reads that same
cache and writes only to the gateway's own disk.
Status, profile and queue requests only read the cache. They never connect to a
printer or request a telemetry refresh. The background monitor starts only the
MQTT client. The camera and FTP connections are opened only by the claimed
control routes, one transfer or one frame at a time.

## Install

Expand Down Expand Up @@ -205,16 +211,61 @@ wait in a per-machine queue with expected finish times. The design contract is
[`docs/SUBMISSION_PIPELINE_DESIGN.md`](docs/SUBMISSION_PIPELINE_DESIGN.md).

```text
submitted -> validating -> validated -> queued -> approved -> | dispatch
\-> rejected (terminal) | not implemented
\--------\-> cancelled (terminal)
submitted -> validating -> validated -> queued -> approved -> dispatching -> running -> finished
\-> rejected (terminal) | \-> failed \-> failed
\-----------\-> cancelled (terminal)
```

Everything up to and including approval is analysis and bookkeeping. Approval is
a *record*, not an action: it marks the job `approved` and sets
`verdict.dispatch_ready`, and moves nothing. `dispatching`, `running` and
`finished` are declared by the contract but unreachable — no route in this
service can enter them.
`verdict.dispatch_ready`, and moves nothing. Only the control plane
(`dispatch.enabled: true`, see *Control plane* below) moves a job past
`approved`, and only when a person starts it.

### STL slicing

With `slicer.enabled`, `POST /submissions` also takes an `.stl` for any printer
listed under `slicer.machine_profiles`, plus a `material` listed under
`slicer.filament_profiles`. The gateway runs the installed Bambu Studio CLI
headlessly in a scratch directory, forcing two machine facts the stock presets
get wrong: the nozzle type the printer reports and the build plate declared in
its profile. The resulting `.3mf` is then validated and queued like any other
upload, and the job carries `provenance` (source mesh hash, presets, slicer
version). Slicing runs inline in the request.

### Control plane

Off unless `dispatch.enabled` is set. When on, each printer gets STATUS_SPEC
v1.1 claims and three verbs under `/printers/{id}/control/`:

| Route | What it does |
|---|---|
| `claim` / `heartbeat` / `release` | Cooperative claim (§5); every other control route needs `X-Claim-Token` or returns 423. |
| `snapshot` | One JPEG frame from the printer's chamber camera, for the plate check. Kept 5 minutes; the frame used to start a job is kept beside the job as `GET /submissions/{id}/plate.jpg`. |
| `light` | Switch the chamber light (`{"on": true}`), e.g. before a snapshot. No motion, no heat. |
| `start_print` | Upload an approved job's `.3mf` and start it. |
| `stop_print` | Stop the job in flight. |

`start_print` is refused (412, a body naming the failed gate) unless the printer
is observable, idle, not in a failed state and cold (`safe_bed_c`,
`safe_nozzle_c`); a build plate is declared and matches the plate the job was
sliced for; the job revalidates against the printer's current state; every
filament is mapped to a loaded tray holding that material (an AMS tray, or the
external spool as `ams_id` 254 / `tray_id` 0 for a single-filament job on a
printer without an AMS); and the request says
`plate_confirmed_empty: true`, by camera or in person. A camera confirmation
records a frame under this claim: the gateway captures it when start is pressed
(or uses a `/control/snapshot` token sent with the request), and refuses with
502 if the camera delivers none. With `require_verified_identity` (the default) both the approver and the
person starting it must be edge-verified identities (403 otherwise).
`allowed_actions` on `/status` is computed by the same gate, so the two never
disagree.

Dispatch is attempted once per job and never retried. The upload is verified by
size on the printer; the job becomes `running` only when the printer reports
it. If the printer never does, or the service restarts mid-dispatch, the job is
`failed` with `dispatch.uncertainty` saying what is not known. Look at the
printer before resubmitting.

### Machine profile

Expand All @@ -231,11 +282,11 @@ whose live field is blank — a dual-nozzle H2D reports no parsable nozzle type.
|---|---|
| `machine_compatible` | sliced for another printer, or a nozzle diameter/type the machine does not have |
| `material_allowed` | the filament is on the machine's forbidden list, or the request's declared material contradicts the sliced one |
| `material_filament_match` | any required material has no matching loaded AMS tray |
| `material_filament_match` | any required material has no matching loaded AMS tray or external spool |
| `nozzle_temp_in_band` | the configured **or commanded** nozzle temperature is outside the machine's limit or the loaded filament's window |
| `bed_chamber_temp_in_band` | the bed temperature is out of band, or a heated chamber is requested on a machine without one |
| `build_fits_plate` | the model's XY footprint exceeds the declared plate |
| `gcode_sanity` | the toolpath contains a refused command (firmware update, EEPROM write, PID retune, cold-extrude override, …) |
| `gcode_sanity` | the toolpath contains a refused command (firmware update, EEPROM write, PID retune, cold-extrude override, …). One `M500` is accepted: Bambu's stock bed-mesh save directly after `G29` in the machine start gcode (approved 2026-10-20) |
| `params_present` | filament type, nozzle temperature or bed temperature is missing — or a `.3mf` carries no sliced plate, so no printer could run it |

One failing check rejects the submission, and rejection is terminal.
Expand Down Expand Up @@ -287,7 +338,11 @@ observes.
`GET /ui` serves a submission page: pick a machine (its plate size, nozzle,
chamber and limits are shown so you know what you are targeting), upload a
file, and read the per-check verdict. It also lists that machine's queue with
finish times, and offers Approve / Cancel.
finish times, and offers Approve / Cancel. With the control plane on, an
approved job gets **Start…**: the page claims the printer, shows a fresh frame
from its camera, asks for a tray per filament and for the empty-plate
confirmation, then starts the job and reports what the printer says. A running
job gets **Stop print**. The page never fetches a camera frame on its own.

It is one static file with **no build step and no external resources** — no
CDN, no npm, no bundler — served from the same origin as the API it calls, so
Expand Down
38 changes: 32 additions & 6 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,28 @@ viewer at `/api/equipment/bambu_gateway/documentation/docs`.
`GET /printers` discovers configured printers. For each ID:

- `GET /printers/{id}/status` is the STATUS_SPEC v1.2 envelope; inventory is
under `details.ams_trays` and `details.ams_unit_ids`.
under `details.ams_trays` and `details.ams_unit_ids`, and the external spool,
when a filament is selected for it, under `details.external_spool` (same tray
fields, `ams_id` 254, `tray_id` 0; never counted in `ams_trays`).
- `GET /printers/{id}/profile` exposes typed `observed.loaded_trays` and
`observed.ams_unit_ids`. Stale/unavailable observations are withheld.
- `GET /printers/{id}/queue` shows the observed running job and waiting jobs.

All paths above are relative to the gateway prefix. Reads use the background
monitor's cache and never initiate printer I/O.

With `dispatch.enabled`, `GET /printers/{id}/camera.jpg` returns the latest
chamber camera frame (`image/jpeg`, `Cache-Control: no-store`, capture time in
`X-Captured-At`) without a claim. It is the one read that touches the printer:
the camera is read at most once per printer every 2 s and the frame is shared by
all callers; 503 when the camera does not deliver one. A frame from here is
never plate-check evidence: a camera `start_print` records its own frame under
the claim (a fresh capture when no `snapshot_token` is sent, or a
`/control/snapshot` taken under the claim within 5 minutes).

| Tray field | Meaning |
|---|---|
| `ams_id`, `tray_id` | Printer-reported unit ID and zero-based slot; together they identify a tray. HT unit IDs are preserved. |
| `ams_id`, `tray_id` | Printer-reported unit ID and zero-based slot; together they identify a tray. HT unit IDs are preserved; 254/0 is the external spool, which `observed.loaded_trays` includes. |
| `tray_index` | Legacy nullable field. Do not use MQTT `n` as a slot index. |
| `tray_type` | Reported material, such as PLA, PETG, PP, or PC. |
| `tray_color` | Reported RGB/RGBA hex, retained even when a display label is declared. |
Expand All @@ -41,10 +52,25 @@ does not affect validation or establish physical spool identity after a swap.

## Submission boundary and authentication

`POST /submissions` accepts an artifact and metadata, validates it, and queues
passing jobs. Approval and cancellation update gateway records only. There is
no dispatch endpoint, no hardware control route, and approval alone does not
establish a complete filament-to-slot mapping or sufficient remaining quantity.
`POST /submissions` accepts an artifact (`.3mf`, `.gcode`, or `.stl` where
slicing is enabled for the target; `.stl` requires `material`) and metadata,
validates it, and queues passing jobs. A sliced job carries `provenance`.
Approval and cancellation update gateway records only, and approval alone does
not establish a filament-to-slot mapping or sufficient remaining quantity.

## Control (only when the deployment enables dispatch)

Per printer, under `/printers/{id}/control/`: `claim`, `heartbeat`, `release`
(STATUS_SPEC §5), then `snapshot`, `light` (`{"on": true|false}`),
`start_print`, `stop_print`, each requiring
`X-Claim-Token` (423 without it). `start_print` takes the approved
`submission_id`, `plate_confirmed_empty: true`, `plate_check_method`
(`printer_camera` with a `snapshot_token`, or `in_person`), and an
`ams_mapping` of `{filament_id, ams_id, tray_id}`. Refusals are 412 with a body
naming the failed gate; `allowed_actions` on `/status` mirrors them, and
`details.claimed_by` names the holder. `GET /submissions/{id}/plate.jpg` returns
the camera frame a start was confirmed against. Without dispatch enabled none
of these paths exist.

The gateway trusts edge identity only with its configured shared edge secret.
Names supplied directly by clients are unverified labels. Dashboard links use
Expand Down
Loading
Loading