Compare commits

..
Author SHA1 Message Date
octoturgeandClaude Sonnet 5 5cf7621b13 Fix print.calibration docs and add v1's missing report schema
Confirmed live: both BambuMQTT versions reply to a calibration request
with a dedicated print.calibration report, not just push_status - the
v1 request schema's own description said the opposite ("printer responds
with push_status instead of calibration"), and v2's report schema framed
this as a v1-vs-v2 contrast that no longer holds. Both descriptions
corrected; the printer does still send ordinary push_status updates
alongside the dedicated report as calibration progresses, same as any
other print job, not instead of it.

Added v1/report/print/calibration.schema.json (only existed for v2
before) and wired it into v1/report.schema.json's print category
dispatcher, which never listed it since the file didn't exist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
2026-09-07 14:47:07 +00:00
octoturgeandClaude Sonnet 5 8e36f39609 Remove superseded Material/PrintParameters/PrintJob/Template mirrors
They described the old flat continuum/v1 shape, replaced by the
material/printer/vendor restructure. Nothing outside this repo imported
them (checked continuum-proxy, continuum-ai-worker, continuum-backend,
continuum-app). Left both packages in place as empty shells - add a
hand-written mirror back per new schema file once something actually
consumes it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
2026-09-05 20:54:30 +00:00
octoturgeandClaude Sonnet 5 1bfd883a9b Restructure continuum/v1 domain schemas: material/printer/vendor model
Replaces the old flat material/print_job/print_params/template schemas
with a richer structure: schemas/continuum/common/ for shared envelope +
enums (material_type taxonomy, printer_type), schemas/continuum/v1/
material/ (material + spool) and printer/ (printer_info/firmware_info/
module_info), plus a new vendor.schema.json.

Fixes found while verifying (all files added in this pass, so nothing
downstream depended on the broken paths yet):
- 5 broken $refs to common/envelope.schema.json - files under the new
  v1/material/ and v1/printer/ subdirectories need ../../common/, not
  ../common/ (one more level of nesting than vendor.schema.json, which
  sits directly under v1/).
- $id in 5 files (material.schema.json, spool.schema.json,
  firmware_info/module_info/printer_info.schema.json) didn't match
  their real path - mostly copy-paste from before the material/printer/
  subdirectories existed.
- $id in all 13 files under common/material_type/ and
  common/printer_type.enum.json said v1/ instead of common/.
- material.schema.json's hardness_value used minItems (an array-only
  keyword, silently ignored on an integer) where minimum: 0 was
  evidently intended, matching the maximum: 100 next to it.

Not fixed, needs a decision rather than a mechanical correction:
- spool.schema.json's `required` list is material.schema.json's
  fields (material_type, min_nozzle_temp, ...), none of which exist in
  spool's own properties (material, empty_spool_weight, ...) - copy-paste
  left over from material.schema.json. continuum-schema-instance's error
  message points right at it: "'material_type' is required but has no
  schema in properties".
- envelope.schema.json's created_by/updated_by are typed
  format: date-time (copy-pasted from created_at/updated_at) - almost
  certainly meant to identify an actor, not a timestamp.
- printer_info.schema.json requires `host` via a oneOf (ipv4-or-ipv6
  format) nested inside an allOf branch - continuum-schema-instance
  doesn't merge a oneOf nested inside allOf (only plain
  properties/required), so it currently builds an empty {} for this
  schema; also worth confirming ipv4/ipv6-only is intended, since it'd
  reject a DNS hostname.
- print_job.schema.json and print_params.schema.json were removed with
  no replacement under the new structure - confirm that's intentional.

Also: the hand-written TypeBox schemas (ts-types) and serde structs
(rust-types) for Material/Template/PrintJob/PrintParameters now describe
the OLD flat shape and are out of sync with this restructure - per this
repo's hand-sync convention (see README), they need updating to match,
or the old ones should be removed if these new schemas supersede them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
2026-09-05 20:40:56 +00:00
octoturgeandClaude Sonnet 5 9d995e8e03 Drop Protobuf; move continuum/v1 domain model to JSON Schema
Nothing imported the proto-generated code (zero references to
continuum-rust-types or @continuum/ts-types's proto export in any
consumer repo), and protobuf's real value - one schema compiled to a
compact binary wire format for both languages - never applied: BE<->proxy
and FE<->BE are both JSON. Also fixes 8 broken $ref paths in the bambulab
schemas and a root tsconfig.json that was sweeping the gitignored
examples/ directory into typecheck.

- proto/continuum/v1/*.proto -> schemas/continuum/v1/*.schema.json
- packages/rust-types: hand-written serde structs instead of
  prost-build codegen
- packages/ts-types: drop the generated/ proto barrel export, keep the
  hand-written TypeBox schemas as the TS source of truth
- root package.json: drop build:ts, protoc, ts-proto

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
2026-09-05 14:56:24 +00:00
octoturge f7efe0a273 finished BambuMQTT schemas 2026-09-05 14:34:23 +00:00
octoturge e49e72a97b feat: (almost) all schemas done 2026-09-02 20:38:36 +00:00
octoturge 2458798880 feat: most of the schemas done 2026-09-02 18:05:49 +00:00
octoturge da6281f5d8 feat: added schemas for print.stop, print.pause and print.resume for BambuMQTT v1 and v2 2026-09-02 15:17:50 +00:00
octoturge de5aa86a2b feat: added upgrade schemas vor BambuMQTT v1 and v2 2026-09-02 15:08:27 +00:00
octoturge 053f1d3ec4 feat: Added unload filament schema 2026-08-31 21:42:43 +00:00
octoturge d4b9f62de0 feat: Added new schemas 2026-08-31 21:18:49 +00:00
octoturge 51dba8e896 Fix broken v2 bambulab schema refs, wire up v2 dispatchers, fix PrintJobStatus JSON mismatch
- v2/common/device/device.schema.json: stray backtick in bed $ref broke resolution
- v2/report/print/print.schema.json: ams $ref pointed at wrong path (missing ams/ subdir)
- v2/common/device/plate.schema.json: $id was copy-pasted from bed.schema.json
- Add v2/request.schema.json and v2/report.schema.json dispatchers - v2's leaf
  schemas were unreachable without them, same oneOf convention as v1
- Rewrite schemas/bambulab/README.md layout section, which still described
  v1's pre-rename paths and claimed v2 was removed
- PrintJob.status serialized as a raw i32 in Rust (prost stores proto3 enums
  as i32) while ts-types' TypeBox schema validates full enum name strings -
  add serde with= on the field so Rust JSON matches TS and protobuf's own
  canonical JSON enum mapping, plus a regression test
2026-08-31 13:50:51 +00:00
octoturge 1b23c58eb6 feat: push_status report for Bambu MQTT v2 2026-08-31 13:41:56 +00:00
octoturge 20ecc1e8bd chore: schema reorganization 2026-08-31 11:02:55 +00:00
octoturge fe735c310b Added v1 schema for PushStatusReport 2026-08-31 07:03:43 +00:00
octoturge 4cc40c913c Fix broken $refs from the bambulab schema underscore-rename, remove stale v2/
- print.schema.json, _ams.schema.json, _ams_unit.schema.json still
  pointed at the pre-rename filenames (ams.schema.json, ams_unit.schema.json,
  ams_tray.schema.json) after the underscore-prefix split - fixed all
  three, verified with a full jsonschema + referencing validation run
  against real captures (get_version, pushall, a full print/ams/xcam
  report with both empty and loaded trays) plus negative tests.
- _upgrade_state.schema.json: fixed its $id (pointed at report/info/,
  file lives under report/print/) and its title (said "ModuleInfo",
  clearly copy-pasted from _module_info.schema.json). Wired it into
  print.schema.json as the print report's `upgrade_state` property -
  the file existed but nothing referenced it yet.
- Deleted schemas/bambulab/v2/, which had been recreated as stale,
  broken copies of an older v1 (missing the envelope allOf, missing
  the underscore renames, $ids still pointing at v1/ paths) - the
  repo's own README documents v2 as intentionally removed until V2's
  wire format is known to differ from V1's.
- Updated schemas/bambulab/README.md's layout tree and inheritance-
  example reference, both stale after the underscore rename.
- Removed stray tsc -b build output (packages/ts-types/src/*.d.ts/js)
  that isn't meant to be committed (outDir is dist/, gitignored).

Verified: cargo check clean (continuum-proxy, continuum-ai-worker,
rust-types), tsc clean (continuum-backend, continuum-common ts-core),
10/10 JSON Schema validation cases passing.
2026-08-29 08:13:49 +00:00
octoturge cd18337e12 Category subdirectories for request/report; ams/xcam split; print vs pushing fix
Each root JSON key ('info', 'pushing', 'print') now gets its own category
folder under request/ and report/, so a category can hold multiple command
schemas without flattening them into one directory - v1/request.schema.json
and v1/report.schema.json route into these via oneOf per category (also
future-proofs for a category ever needing more than one shape).

Split pushall's ams/xcam fields the same way module_info was already
split: ams.schema.json/ams_unit.schema.json/ams_tray.schema.json/
xcam.schema.json/print.schema.json, each referencing the next by relative
$ref. ams_tray.schema.json uses oneOf for the empty-vs-loaded tray shapes
- verified (and initially got wrong, then fixed) that LoadedTray needs
every field required, not just id, or a bare {"id":"0"} validates
against both branches and oneOf fails, since oneOf demands exactly one
match.

Real correction: the report/ category for what a pushall request
triggers is "print", not "pushing" - re-reading the original examples,
pushall's REQUEST goes to "pushing", but the resulting state-push stream
arrives under "print", a different root key. report/pushing/ (my
original mistake) is removed; report/print/ holds the corrected content.
request/pushing/ is untouched - that side was correct.

Also: v2/ removed (found as broken copy-pasted stubs still pointing at
v1 $ids - no real V2-specific content yet, better to have nothing than
broken duplicates); all $ids switched to real, verified Gitea raw-file
URLs (raw/branch/main/<path> - confirmed this is the correct pattern,
not raw/<path>) instead of a placeholder domain; fixed a broken relative
$ref in the request-side files left over from an earlier move (still
pointing 2 levels up instead of 3 after nesting one level deeper).

Re-validated everything against real examples after restructuring,
including the full ams/xcam print report and two negative cases -
see schemas/bambulab/README.md for the validation script.
2026-08-28 23:17:32 +00:00
octoturge 8d4a08e36d Move bambulab JSON Schemas here from continuum-proxy; restructure
Schemas (any notation - proto, JSON Schema) belong in the schemas repo
regardless of how many languages currently consume them; that's what
makes this 'the schemas repo'. Rust-only code (the hand-written structs
in continuum-proxy's bambu_commands/) is a different category and stays
where it was.

Also restructured per readability feedback:
- envelope.schema.json moved out of v1/ to the bambulab/ root - it's
  shared across protocol generations, not v1-specific, same principle
  already applied to the (now-removed) proto/bambulab/common.proto.
- v1/ split into v1/request/ and v1/report/, one file per command in
  each, instead of *.request.schema.json / *.response.schema.json
  filename suffixes doing the same job less clearly. The two top-level
  dispatcher schemas (matching BambuRequest/BambuReport) stay at the v1/
  root since they aren't themselves a single command's schema.

Re-verified after restructuring, not just moved and assumed still
correct:  resolution now has to handle a real basename collision
(get_version.schema.json exists in both request/ and report/) — switched
the validation approach to register schemas by  (their actual base
URI) rather than by filename, which is what makes relative  resolve
correctly here. All three real examples plus the negative tests from the
original version still pass.
2026-08-28 22:58:18 +00:00
octoturge bc4ee8690a Drop proto/bambulab/ — plain hand-written Rust structs instead
Reconsidered after actually feeling the friction: this data is JSON over
MQTT (not binary protobuf), only continuum-proxy ever parses it (raw
vendor wire format, never crosses into TS), so none of protobuf's actual
value (cross-language codegen, binary wire efficiency) applied here.
Every gotcha hit while building this (field_attribute's oneof/variant
path collision, needing .bambulab-scoped type_attribute calls, explicit
#[serde(default)] per not-always-present field) came from forcing
prost-build's codegen model onto a plain-JSON, Rust-only use case.

Verified the replacement is strictly simpler, not just smaller: a
hand-written struct's Option<T> field needs zero extra attributes to
handle a missing JSON key (prost's generated String needed an explicit
field_attribute call per field), and #[serde(flatten)] on a hand-written
field has no ambiguity at all (prost-build's oneof/variant path collision
doesn't exist without prost-build's codegen in the picture).

The replacement lives in continuum-proxy: src/printer/bambu_commands/.
continuum.v1 is unaffected — that schema is genuinely cross-language and
proto still earns its keep there.
2026-08-28 22:48:48 +00:00
octoturge 99c1d038f9 Add InfoCommand as one fully-wired real example in bambulab.v1
The rest of proto/bambulab/ was pure boilerplate/comments with nothing
concrete to copy from. This one message (sequence_id + command, matching
Bambu's real get_version request) is wired all the way through: real
proto, real generated Rust struct, lib.rs actually include!'d instead of
commented out. continuum-proxy's bambu_protocol_demo example serializes it
and diffs the output against Bambu's actual wire JSON byte for byte.

Verified with cargo build (this crate) and cargo run --example
bambu_protocol_demo (continuum-proxy) - output matches exactly.
2026-08-28 22:41:42 +00:00
octoturge f0bb8714a3 Document how to wire up bambulab modules in lib.rs once messages exist 2026-08-28 22:39:55 +00:00
octoturge 7505e759e1 Wire up proto/bambulab/ build pipeline (boilerplate — message bodies TBD)
Adds bambulab/v1 and bambulab/v2 to the prost-build pipeline, scoped
separately from continuum.v1's camelCase convention: bambulab.* uses
snake_case, matching Bambu's actual MQTT wire JSON exactly, since this is
edge-only traffic (continuum-proxy) that has to match the printer's real
format rather than a schema we get to design.

Verified the scoping doesn't collide: type_attribute calls are cumulative,
so the existing '.' blanket selector had to become '.continuum' specifically
- keeping it global would stack a second, conflicting #[serde(rename_all)]
onto every bambulab type too and fail to compile.

No message bodies added - proto/bambulab/{common,v1/*,v2/*}.proto are
intentionally just syntax+package+guidance comments (matching what was
already started for v1). build.rs has inline notes on the two real gotchas
already hit while prototyping this: the field_attribute path-matching
oneof/variant collision (solution: hand-write the envelope enum in Rust
instead of modeling it as a proto oneof), and serde needing #[serde(default)]
on fields that aren't always present.
2026-08-28 22:38:20 +00:00
octoturge e806a13c70 Gitignore examples/ for personal study files (not part of the real schema build) 2026-08-28 21:53:12 +00:00
octoturge 34c7c97d4f Fix real codegen bugs: unused proto import, missing runtime deps, no barrel index
protoc warned that print_job.proto imported template.proto without using it
- removed. ts-types/package.json was missing protobufjs and long, which the
ts-proto-generated code imports at runtime - tsc failed with 'cannot find
module'. src/index.ts also re-exports './generated' as a namespace, but
ts-proto wasn't configured to emit an index barrel file for it - added
outputIndex=true to the protoc invocation.

Verified with a clean 'bun run build' (both TS and Rust codegen) and
'tsc -b'.
2026-08-28 18:36:23 +00:00
octoturgeandClaude Sonnet 5 660855ecd6 Initial boilerplate scaffold for continuum-schemas
Protobuf data model (Template/PrintJob/MaterialProfile/PrintParameters)
with codegen targets for TypeBox+TS types (packages/ts-types) and
prost-derived Rust structs (packages/rust-types).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 16:12:03 +00:00
28 changed files with 30 additions and 1464 deletions
-4
View File
@@ -10,7 +10,3 @@ Cargo.lock
# Personal study files, not part of the real schema build.
/examples/
# Emitted by `tsc -b` into each package's dist/ - never commit build output.
*.js.map
*.d.ts.map
+6 -26
View File
@@ -13,39 +13,19 @@ JSON Schema, compiled into two consumer packages:
## Layout
```
schemas/continuum/v1/ Continuum's own model
ai/ continuum-backend <-> continuum-ai-worker: the
stream:camera_frames work queue and the
channel:print_alerts notification channel
edge/ continuum-backend <-> continuum-proxy: the
/ws/edge/v1 gateway uplink protocol
material/ printer/ domain objects
vendor.schema.json
schemas/continuum/v1/ Continuum's own domain model (PrintJob, Template, ...)
schemas/bambulab/ Bambu Lab MQTT wire schemas (v1/v2, report + request)
packages/ts-types/ TypeBox schemas (hand-written, mirrors schemas/)
packages/rust-types/ serde structs (hand-written, mirrors schemas/)
```
The two inter-service protocols - `edge/` and `ai/` - are here for a
specific reason: each has two independent implementations, one per repo, and
when there was nothing written down between them they drifted completely
while every test passed. `/ws/edge/v1` had an `auth` handshake the gateway
never sent; `stream:camera_frames` had a producer writing `printerId` and a
consumer reading `printer_id`, so every frame was attributed to a printer
called "unknown". Neither failure was visible from either side alone, which
is what a canonical schema plus a contract test on each end is for.
`schemas/continuum/v1/*.schema.json` is the canonical shape for each
domain object; `packages/ts-types/src/schemas/` and
`packages/rust-types/src/lib.rs` both mirror it by hand. A mirror is added
per schema only once something actually consumes it, so not every file here
has one, and the two inter-service protocols keep their Rust mirror in the
single repo that consumes it rather than in `packages/rust-types`:
`edge/` in `continuum-proxy/src/uplink/protocol.rs`, `ai/` in
`continuum-ai-worker/src/task.rs`. The set is small and changes slowly,
which is why keeping hand-synced copies in sync is less overhead than a
codegen pipeline (`protoc`/`ts-proto`/`prost-build`) nothing actually
consumed - see git history for the
`packages/rust-types/src/lib.rs` both mirror it by hand. There are only
four small, stable DTOs here (`PrintJob`, `MaterialProfile`,
`PrintParameters`, `Template`), so keeping two hand-synced copies in sync
is less overhead than running a codegen pipeline (`protoc`/`ts-proto`/
`prost-build`) nothing actually consumed - see git history for the
protobuf version this replaced, and why it was dropped: every service in
this system talks JSON (JSON Schema for the wire, TypeBox/serde for
runtime validation), so protobuf's actual value - a single schema compiled
-36
View File
@@ -1,36 +0,0 @@
# Licence policy for a closed-source commercial product.
# See docs/LICENSES.md in continuum-common for the rationale and the
# non-cargo dependencies (external binaries, ML models) this file cannot see.
#
# Run: cargo deny check licenses
[licenses]
# Permissive only. Anything copyleft reaching a shipped binary would make
# the product undistributable without releasing our source.
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Zlib",
"Unicode-3.0",
"Unlicense",
"MPL-2.0",
"CC0-1.0",
"BlueOak-1.0.0",
]
confidence-threshold = 0.9
# Our own crates declare `LicenseRef-Proprietary`, which is deliberately not
# an SPDX identifier and must never appear in the allow list above - a
# third-party crate claiming it would be a real finding. They are recognised
# as first-party by `publish = false` in their manifests.
private = { ignore = true }
[bans]
deny = []
[advisories]
yanked = "deny"
-1
View File
@@ -1,6 +1,5 @@
{
"name": "continuum-schemas",
"license": "UNLICENSED",
"version": "0.1.0",
"private": true,
"workspaces": [
-4
View File
@@ -2,10 +2,6 @@
name = "continuum-rust-types"
version = "0.1.0"
edition = "2021"
# Proprietary: deliberately not an SPDX id, so cargo-deny treats it as
# first-party. See docs/LICENSES.md in continuum-common.
license = "LicenseRef-Proprietary"
publish = false
[lib]
name = "continuum_types"
+1 -2
View File
@@ -1,6 +1,5 @@
{
"name": "@continuum/ts-types",
"license": "UNLICENSED",
"version": "0.1.0",
"private": true,
"type": "module",
@@ -10,6 +9,6 @@
".": "./src/index.ts"
},
"dependencies": {
"@sinclair/typebox": "0.34.52"
"@sinclair/typebox": "^0.32.0"
}
}
+1
View File
@@ -0,0 +1 @@
export * from "./schemas";
+1
View File
@@ -0,0 +1 @@
export * from "./schemas";
-96
View File
@@ -1,96 +0,0 @@
import { Type, type Static } from "@sinclair/typebox";
/**
* The `stream:camera_frames` work queue and the `channel:print_alerts`
* notification channel, mirroring `schemas/continuum/v1/ai/*.schema.json`
* by hand - the JSON Schema files are canonical, and the Rust mirror lives
* in `continuum-ai-worker/src/task.rs`.
*
* These two are the only interface between `continuum-backend` and
* `continuum-ai-worker`. They are written down here because the previous
* arrangement - a producer and a consumer each naming their own fields,
* with nothing in between - produced exactly the drift it sounds like: the
* backend wrote `printerId` and the worker read `printer_id`, so every
* frame the worker ever handled was attributed to a printer called
* "unknown", and nothing failed.
*/
/**
* One entry on `stream:camera_frames`.
*
* ## Every value is a string
*
* A Valkey stream entry is a flat field/value map of strings. There is no
* nesting and no numeric type, which is why `FrameTaskSchema` has no
* numbers and no nested objects - a producer that needs one has to encode
* it, and then both sides have to agree on the encoding, which is the thing
* this file exists to avoid.
*
* ## An absent field is absent
*
* `print_job_id` is omitted from the XADD when the frame belongs to no
* tracked job - not written as `""`. An empty string is a value: a consumer
* that passes it along ends up looking up job id `""`, and one that writes
* it to Postgres violates a uuid column. So the producer omits the field and
* the consumer treats absent and empty alike.
*/
export const FrameTaskSchema = Type.Object({
/** `farms.id`. */
farm_id: Type.String({ format: "uuid" }),
/**
* `printers.id` - the backend's UUID, already resolved from the gateway's
* own printer id, which means nothing outside its farm.
*/
printer_id: Type.String({ format: "uuid" }),
/** `print_jobs.id`, or omitted. Never `""`. */
print_job_id: Type.Optional(Type.String({ format: "uuid" })),
/** Where the frame is in R2. The bytes are never in the stream. */
r2_object_key: Type.String({ minLength: 1 }),
/**
* The gateway's clock, at capture - not enqueue time. A gateway that
* buffered frames through an uplink outage produces tasks whose
* `captured_at` is minutes old.
*/
captured_at: Type.String({ format: "date-time" }),
});
export type FrameTask = Static<typeof FrameTaskSchema>;
/**
* One message on `channel:print_alerts`, as JSON.
*
* Unlike a stream entry this one is a document, because PUBLISH carries a
* single opaque string. It is also fire-and-forget: an alert published with
* no subscriber is gone, so this channel is a live-console feed and not the
* record of a detection.
*/
export const PrintAlertSchema = Type.Object({
printer_id: Type.String({ format: "uuid" }),
print_job_id: Type.Optional(Type.String({ format: "uuid" })),
/**
* Free string, not an enum: the label set belongs to the model file and
* changes without this schema changing.
*/
label: Type.String(),
/**
* The model's own score. Thresholding is the consumer's policy - an
* operator may well want to see near-misses.
*/
confidence: Type.Number({ minimum: 0, maximum: 1 }),
/** From the frame, so a console shows when the printer looked like this. */
captured_at: Type.String({ format: "date-time" }),
/** So a console can show the picture, not just the label. */
r2_object_key: Type.String(),
});
export type PrintAlert = Static<typeof PrintAlertSchema>;
/** The stream the tasks go on, and the group the worker reads as. */
export const CAMERA_FRAMES_STREAM = "stream:camera_frames";
export const AI_WORKER_CONSUMER_GROUP = "ai-worker";
/**
* Where entries go that cannot be processed and must not be retried - a
* malformed task, or one that has been redelivered too many times. Separate
* from the main stream so a poison message cannot block the queue and
* cannot be lost silently either.
*/
export const CAMERA_FRAMES_DEAD_LETTER_STREAM = "stream:camera_frames:dead";
export const PRINT_ALERTS_CHANNEL = "channel:print_alerts";
-98
View File
@@ -1,98 +0,0 @@
import { Type, type Static, type TSchema } from "@sinclair/typebox";
/** Present but possibly null - a column that exists and has no value yet. */
const Nullable = <T extends TSchema>(schema: T) => Type.Union([schema, Type.Null()]);
/**
* What continuum-backend pushes down `/ws/telemetry` to an operator console,
* mirroring `schemas/continuum/v1/app/telemetry_message.schema.json` by hand
* - the JSON Schema is canonical, change it first.
*
* ## Why this is camelCase when everything else here is snake_case
*
* Because this one is not shared with the Rust gateway. `continuum/v1/edge/`
* is snake_case so serde derives those names directly; this is the backend's
* own HTTP vocabulary, and the browser already consumes camelCase from
* `GET /printers`. Giving it a second model of a printer to reconcile would
* be the cost of a consistency nobody benefits from.
*
* This file exists because a previous frontend invented a shape against
* nothing - `printerId`, `nozzleTempC`, `progressPct`, `updatedAt` and a
* closed `idle|printing|paused|error|offline` enum, inside an envelope no
* backend ever sent. Both ends now mirror something.
*/
/**
* A printer as the console displays it: the same shape `GET /printers/:id`
* returns, so a client can apply a push to the row it already holds.
*
* Every telemetry field is nullable, and that is not laziness - a printer
* that has never reported has no temperature, and "no data yet" is a
* different thing from 0 degrees. A client that renders `?? 0` here will
* show a cold printer as a real reading.
*/
export const AppPrinterStateSchema = Type.Object({
id: Type.String({ format: "uuid" }),
farmId: Type.String({ format: "uuid" }),
name: Type.String(),
vendor: Type.String(),
status: Type.Optional(Nullable(Type.String())),
/**
* The printer's own word for what it is doing - a Bambu's `RUNNING`,
* `PREPARE`, `FINISH`. Deliberately not an enum: an unrecognised state
* must reach the screen as itself rather than being dropped on the way.
*/
vendorState: Type.Optional(Nullable(Type.String())),
vendorErrorCode: Type.Optional(Nullable(Type.Integer())),
nozzleTempC: Type.Optional(Nullable(Type.Number())),
nozzleTargetTempC: Type.Optional(Nullable(Type.Number())),
bedTempC: Type.Optional(Nullable(Type.Number())),
bedTargetTempC: Type.Optional(Nullable(Type.Number())),
progressPercent: Type.Optional(Nullable(Type.Integer())),
currentLayer: Type.Optional(Nullable(Type.Integer())),
totalLayers: Type.Optional(Nullable(Type.Integer())),
remainingMinutes: Type.Optional(Nullable(Type.Integer())),
currentJobName: Type.Optional(Nullable(Type.String())),
/** A count of active HMS entries, not messages - we lack Bambu's tables. */
activeAlerts: Type.Optional(Nullable(Type.Integer())),
/**
* 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 `GET /printers/:id/stream` is worth
* calling; null means render no player at all.
*/
cameraStream: Type.Optional(Nullable(Type.String())),
/**
* 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
* the status on its own cannot say.
*/
lastSeenAt: Type.Optional(Nullable(Type.String())),
});
export type AppPrinterState = Static<typeof AppPrinterStateSchema>;
export const AppTelemetryMessageSchema = Type.Union([
Type.Object(
{
type: Type.Literal("printer.telemetry"),
printer: AppPrinterStateSchema,
},
{ additionalProperties: false },
),
/**
* Sent once, right after the socket authenticates.
*
* Worth a message of its own rather than letting the client infer
* readiness from silence: an operator with no printers claimed and an
* operator whose gateway is offline both see nothing arrive, and those two
* need different things said to them.
*/
Type.Object(
{
type: Type.Literal("ready"),
visiblePrinters: Type.Integer({ minimum: 0 }),
},
{ additionalProperties: false },
),
]);
export type AppTelemetryMessage = Static<typeof AppTelemetryMessageSchema>;
-378
View File
@@ -1,378 +0,0 @@
import { Type, type Static, type TSchema } from "@sinclair/typebox";
/**
* Optional *and* nullable: the key may be missing, or present as `null`.
*
* Strict in what we send, liberal in what we accept. A gateway omits fields
* it has no value for, which is what the JSON Schema describes - but a
* serde-derived Rust struct emits `null` by default unless every field is
* annotated, and one such field reaching here would otherwise fail
* validation and make Elysia drop an entire telemetry batch over a printer
* that merely had no job name. Not worth risking a farm's telemetry on.
*/
const Maybe = <T extends TSchema>(schema: T) => Type.Optional(Type.Union([schema, Type.Null()]));
/**
* The /ws/edge/v1 uplink protocol, mirroring
* `schemas/continuum/v1/edge/*.schema.json` by hand - the same convention
* every other schema in this package follows. The JSON Schema files are
* canonical; change them first, then this, then the Rust mirror (which for
* this one protocol lives in `continuum-proxy/src/uplink/protocol.rs` rather
* than in `packages/rust-types`, since the gateway is its only Rust
* consumer).
*
* Field names are snake_case, unlike the backend's HTTP routes. That is not
* an oversight: the other end of this wire is serde-derived Rust, and
* snake_case is what those structs produce without per-field rename
* attributes. It is also what the rest of this repository uses.
*/
export const PrinterTelemetrySchema = Type.Object({
/**
* The *gateway's* own id from its printers.toml - not a vendor serial and
* not `printers.id`. The backend resolves it through
* `printers.gateway_printer_id`, scoped to the authenticated farm.
*/
printer_id: Type.String(),
name: Type.String(),
/**
* The printer's own state string rather than an enum, so an unrecognised
* state survives the trip instead of being dropped at the edge. Bambu
* emits IDLE, RUNNING, PAUSE, FINISH, FAILED.
*/
state: Maybe(Type.String()),
percent: Maybe(Type.Integer({ minimum: 0, maximum: 100 })),
/**
* What the printer is doing when it is not laying down plastic - bed
* levelling, nozzle cleaning, extrusion calibration. Bambu's `stg_cur`,
* passed through as its own number because we do not have the
* code-to-name table. 0 means printing.
*
* Without it, progress is ambiguous in a way that reads as a fault: a P1P
* was observed at 14% with `layer` still 0 for several minutes while it
* worked through stages 54, 14 and 13. A console showing that with no
* stage looks stuck.
*/
stage: Maybe(Type.Integer()),
/**
* The stream name to ask the gateway's `stream_base_url` for, when this
* printer has a camera the gateway can restream. Absent means no stream.
*
* Absent is the common answer today: a Bambu P1/A1 publishes no RTSP URL
* at all - a real P1P's ipcam block has `ipcam_dev` and `ipcam_record` and
* nothing to connect to - so only the X1/H2 class works this way. The rest
* need Bambu's proprietary port-6000 JPEG stream, which no gateway
* implements yet.
*/
camera_stream: Maybe(Type.String()),
layer: Maybe(Type.Integer({ minimum: 0 })),
total_layers: Maybe(Type.Integer({ minimum: 0 })),
remaining_minutes: Maybe(Type.Integer({ minimum: 0 })),
job_name: Maybe(Type.String()),
nozzle_temp: Maybe(Type.Number()),
nozzle_target: Maybe(Type.Number()),
bed_temp: Maybe(Type.Number()),
bed_target: Maybe(Type.Number()),
/** Vendor error code; non-zero means the printer is reporting a fault. */
print_error: Maybe(Type.Integer()),
/** Count of active Health-Management-System entries, not decoded messages. */
active_alerts: Maybe(Type.Integer({ minimum: 0 })),
});
export type PrinterTelemetry = Static<typeof PrinterTelemetrySchema>;
/**
* Deliberately small, and deliberately missing "start a print" - see the
* JSON Schema for why that variant is absent until the gateway's
* start-an-uploaded-file path has been run against real hardware by hand.
*/
/**
* Every variant is closed (`additionalProperties: false`), unlike
* `PrinterTelemetrySchema` which is deliberately open. The asymmetry is the
* point, and it follows from who writes each message.
*
* Telemetry is written by gateways in the field, so it is read liberally: an
* unknown field must not cost a whole farm's batch, and a gateway that is
* newer than the backend should still be understood.
*
* Commands are written by this backend, are a handful of fields, and move
* physical machines. Here an unrecognised field is a bug on the sending side,
* and silently dropping it is the worst available outcome - a caller that
* misspells a field would watch its command succeed and do something other
* than what it asked for. `ams_mapping` is the concrete case: it is
* deliberately not part of `start_print`, and a caller that passes one must
* be told, not ignored.
*/
export const PrinterCommandSchema = Type.Union([
Type.Object({
action: Type.Literal("start_print"),
/**
* The cloud's `print_jobs.id`. The gateway does not need it to do the
* work - it is here so a gateway's logs line up with a job.
*/
job_id: Type.String({ format: "uuid" }),
/**
* What to call the file on the printer's own storage. A `.3mf` is a
* sliced project and its filament mapping is read out of the archive;
* anything else is bare gcode and carries no mapping.
*/
file_name: Type.String({ minLength: 1, maxLength: 500 }),
/**
* A presigned, expiring GET for the job file - a URL rather than the
* bytes, because the file is tens of megabytes and this is a JSON
* control channel, and because it keeps object-store credentials out of
* every gateway on every farm.
*
* Presign this at *dispatch* time, never at job creation: a queued job
* may wait days and the URL would be long expired.
*/
url: Type.String({ format: "uri" }),
}, { additionalProperties: false }),
Type.Object({
action: Type.Literal("set_fan_speed"),
/**
* A percentage on this wire whatever the vendor wants. A Bambu turns it
* into an M106 PWM value and reports it back as one of 16 gears, so what
* is set here does not read back identically.
*/
percent: Type.Integer({ minimum: 0, maximum: 100 }),
}, { additionalProperties: false }),
Type.Object({
action: Type.Literal("control_print"),
/**
* Confirmed live on a P1P (2026-10-11). Beware what the printer's
* acceptance means: all three answered `success` while it was IDLE with
* nothing to pause, so confirmation that the state actually changed has
* to come from telemetry, not from the command result.
*
* `stop` is a cancel and cannot be undone - a Bambu reports the
* cancelled print as `FAILED`.
*/
control: Type.Union([
Type.Literal("pause"),
Type.Literal("resume"),
Type.Literal("stop"),
]),
}, { additionalProperties: false }),
Type.Object({
action: Type.Literal("send_gcode_line"),
/** The most powerful variant by far; be careful about exposing it to a UI. */
line: Type.String(),
}, { additionalProperties: false }),
]);
export type PrinterCommand = Static<typeof PrinterCommandSchema>;
const DiscoveredVendor = Type.Union([
Type.Literal("bambu"),
Type.Literal("klipper"),
Type.Literal("prusa_link"),
Type.Literal("octoprint"),
Type.Literal("prusa_serial"),
]);
/**
* A printer a gateway found but cannot drive, because it is not in that
* gateway's fleet file. Carries no credential and cannot: a Bambu access
* code is on the printer's screen and a PrusaLink API key is in its web UI,
* and no probe learns either. What it buys is that an operator adding a
* printer in the web app does not have to go and read an IP address or a
* serial number off a machine.
*
* Every string here was chosen by a device on the customer's network -
* anything that answers an SSDP query or an HTTP probe names itself - so
* these are untrusted display strings. Hence the length caps, which are the
* reason this one object is closed while its sibling variants are open: an
* unbounded `name` from the LAN should not reach a database column or a
* React tree, and a gateway has no legitimate reason to send extra fields
* here that this backend would have to guess the meaning of.
*/
export const DiscoveredPrinterSchema = Type.Object({
/**
* The `vendor` value this candidate would need in a fleet file. Note
* `octoprint` where a probe saw only the OctoPrint API: PrusaLink serves
* that same API, so claiming PrusaLink would assert more than was
* observed - see `ambiguous_with`.
*/
vendor: DiscoveredVendor,
/**
* An IP or hostname for a network printer, or a device path such as
* `/dev/ttyACM0` for one on a serial port. An address on the *gateway's*
* network: not a URL, and not reachable from the cloud.
*/
address: Type.String({ minLength: 1, maxLength: 255 }),
/** Only where a probe could learn it without a credential. */
serial: Maybe(Type.String({ maxLength: 64 })),
model: Maybe(Type.String({ maxLength: 128 })),
/** Whatever the operator typed into their slicer. Display only. */
name: Maybe(Type.String({ maxLength: 128 })),
/** Human detail to recognise the machine by. Never parsed. */
detail: Maybe(Type.String({ maxLength: 256 })),
/**
* A vendor this could equally be. Today only `prusa_link` versus
* `octoprint`, which nothing on the network distinguishes. Present means
* the operator must choose, and the web app should offer both rather than
* quietly picking one.
*/
ambiguous_with: Maybe(DiscoveredVendor),
/** Which probe found it, which bounds how much it can know. */
source: Type.Union([
Type.Literal("ssdp"),
Type.Literal("certificate"),
Type.Literal("http_api"),
Type.Literal("usb_descriptor"),
Type.Literal("serial_handshake"),
]),
/**
* Whether this gateway build can actually drive this vendor. False means
* the client is still a stub, so adding the printer would produce one that
* connects and does nothing - worth saying in the UI rather than offering
* it as ready.
*/
driver_ready: Type.Boolean(),
}, { additionalProperties: false });
export type DiscoveredPrinter = Static<typeof DiscoveredPrinterSchema>;
export const EdgeClientMessageSchema = Type.Union([
Type.Object({
type: Type.Literal("hello"),
gateway_id: Type.String(),
farm_id: Type.String(),
/*
* No credential here, deliberately. The edge JWT travels in an
* `Authorization: Bearer` header on the upgrade request and is verified
* before the socket exists. A `token` field used to live on this
* message; it was removed, because a token in the first frame means a
* live socket and a parsed message for a peer that has proved nothing.
*
* `farm_id` is redundant with the token's own claim, and is checked
* against it rather than trusted.
*/
/**
* Where this gateway serves camera restreams, e.g.
* `http://gateway.local:1984`. Omitted by a gateway with no cameras or
* no go2rtc.
*
* Reported rather than configured, because only the gateway knows the
* address it is reachable at - and note what that address is: a LAN one.
* A browser on the same network can play the stream; one outside it
* cannot, and a relay for that does not exist yet.
*/
stream_base_url: Maybe(Type.String()),
}),
Type.Object({
type: Type.Literal("heartbeat"),
/** Monotonic per connection, restarting at 1 on reconnect. */
seq: Type.Integer({ minimum: 0 }),
}),
Type.Object({
type: Type.Literal("telemetry"),
/**
* A batch, not a message per printer: a farm has one uplink and many
* printers, and a coherent picture of the farm at an instant is worth
* more here than the lowest possible per-machine latency.
*/
printers: Type.Array(PrinterTelemetrySchema),
}),
Type.Object({
type: Type.Literal("command_result"),
command_id: Type.String(),
ok: Type.Boolean(),
/** Only when `ok` is false. A message for an operator, not a code. */
error: Maybe(Type.String()),
}),
/** go2rtc's answer SDP for a `webrtc_offer`, passed through verbatim. */
Type.Object({
type: Type.Literal("webrtc_answer"),
session_id: Type.String(),
sdp: Type.String(),
}),
/**
* No answer was obtainable: no such camera, go2rtc not running, or it
* refused the offer. A variant of its own rather than an empty answer, so
* a viewer is told something instead of watching a `<video>` that never
* loads.
*/
Type.Object({
type: Type.Literal("webrtc_error"),
session_id: Type.String(),
message: Type.String(),
}),
Type.Object({
type: Type.Literal("frame_batch"),
printer_id: Type.String(),
print_job_id: Maybe(Type.String({ format: "uuid" })),
frames: Type.Array(
Type.Object({
object_key: Type.String(),
captured_at: Type.String({ format: "date-time" }),
}),
),
}),
/**
* Printers the gateway found on its own network or USB ports that are not
* in its fleet file, so an operator can add one in the web app without
* hunting for an IP address.
*
* A whole batch that replaces whatever this gateway reported before, not a
* set of additions: a printer that has gone is reported by its absence,
* and a gateway that sees nothing sends an empty list. Only sent at all
* when discovery is enabled in the gateway's own config, which it is not
* by default - scanning a customer's network is something they ask for.
*/
Type.Object({
type: Type.Literal("discovered_printers"),
printers: Type.Array(DiscoveredPrinterSchema, { maxItems: 256 }),
/**
* When the scan finished, so the cloud can show the age of the list
* rather than implying it is live.
*/
scanned_at: Maybe(Type.String({ format: "date-time" })),
}),
]);
export type EdgeClientMessage = Static<typeof EdgeClientMessageSchema>;
export const EdgeServerMessageSchema = Type.Union([
Type.Object({ type: Type.Literal("hello_ack"), session_id: Type.String() }),
Type.Object({ type: Type.Literal("hello_error"), message: Type.String() }),
Type.Object({ type: Type.Literal("heartbeat_ack"), seq: Type.Integer({ minimum: 0 }) }),
/**
* Relay a browser's WebRTC offer to this gateway's go2rtc and send the
* answer back. **Signalling only - no video crosses this socket.** The
* gateway POSTs the offer to its own go2rtc over loopback, which go2rtc
* exempts from its API password, so no credential is needed or sent;
* browser and go2rtc then negotiate media directly over ICE.
*
* It exists because a browser cannot signal to a gateway itself. Measured
* against go2rtc 1.9.14: it sends no `Access-Control-Allow-Origin` on
* anything, and checks its password before CORS, so the unauthenticated
* preflight a browser always sends gets a 401.
*
* Relaying only the signalling is the point. The media stays
* peer-to-peer - on a workshop LAN it never leaves the building and costs
* no cloud egress - and go2rtc's answer carries a server-reflexive
* candidate as well as its host ones, so a remote viewer can often
* hole-punch straight to the gateway. A TURN server in go2rtc's
* `ice_servers` covers the NATs where that fails, and that is
* configuration rather than a change to this protocol.
*/
Type.Object({
type: Type.Literal("webrtc_offer"),
/** Correlates the answer with this offer. One negotiation, not a subscription. */
session_id: Type.String(),
/**
* The go2rtc stream name, which is what `PrinterTelemetry.camera_stream`
* reported. Deliberately not called a printer id: the two are equal
* today, but one is the gateway's own naming and the other is ours.
*/
camera_stream: Type.String(),
/** The browser's offer SDP, passed through verbatim. */
sdp: Type.String(),
}, { additionalProperties: false }),
Type.Object({
type: Type.Literal("command"),
command_id: Type.String(),
/** The gateway's own printer id - the backend translates on the way out. */
printer_id: Type.String(),
command: PrinterCommandSchema,
}),
]);
export type EdgeServerMessage = Static<typeof EdgeServerMessageSchema>;
+1
View File
@@ -0,0 +1 @@
export {};
+1
View File
@@ -0,0 +1 @@
export {};
+1 -15
View File
@@ -4,18 +4,4 @@
// hand-written TypeBox schema here per file under schemas/continuum/v1/
// once something actually consumes it (continuum-backend's validators,
// most likely), same convention as before.
// The /ws/edge/v1 gateway uplink protocol - the first thing to actually need
// a TypeBox mirror here, consumed by continuum-backend's edge WebSocket
// route. Mirrors schemas/continuum/v1/edge/.
export * from "./edge";
// The continuum-backend -> continuum-ai-worker interface: the
// stream:camera_frames work queue and the channel:print_alerts notification
// channel. Mirrors schemas/continuum/v1/ai/.
export * from "./ai";
// The continuum-backend -> operator console push protocol: /ws/telemetry.
// Mirrors schemas/continuum/v1/app/. camelCase, unlike the rest - see the
// file for why.
export * from "./app";
export {};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
@@ -10,11 +10,6 @@
"properties": {
"command": { "const": "push_status" },
"msg": {
"type": "integer",
"description": "Whether this push is the whole object or only what changed: 0 forced/full (what a pushing.pushall triggers), 1 automatic/partial. Confirmed against a real printer. A v1 consumer MUST read this - merging a partial push as if it were full leaves the picture complete, but replacing the cached object with a partial one silently unsets every field the push did not mention. It cannot be inferred any other way, because push_status does not echo sequence_id (see the module description), so there is no correlation back to the pushall that asked for it. v2 sends the whole object every time and this field is informational there."
},
"upgrade_state": { "$ref": "../../common/upgrade_state.schema.json" },
"ipcam": { "$ref": "../../common/ipcam.schema.json" },
"upload": { "$ref": "../../common/upload.schema.json" },
@@ -7,11 +7,9 @@
{ "$ref": "../../../common/envelope.schema.json" },
{
"type": "object",
"$comment": "There is deliberately no vt_tray property here. A real v2 printer omits the key entirely rather than sending an empty object, confirmed live, so absence means 'not reported' and not 'no external spool' - do not add it to make this symmetrical with v1. It is why the external-spool count cannot be derived from a report: counting reported spools would give 0 on exactly the dual-nozzle H2C that has two (see common/ams/ams_mapping.schema.json).",
"properties": {
"command": { "const": "push_status" },
"2D": { "$ref": "./_2d_report.schema.json" },
"3D": { "$ref": "./_3d_report.schema.json" },
"ams": { "$ref": "../../common/ams/ams.schema.json" },
@@ -87,10 +85,7 @@
"mc_stage": { "type": "integer" },
"model_id": { "type": "string" },
"msg": {
"type": "integer",
"description": "Full-vs-partial indicator, as in v1 (0 forced/full, 1 automatic/partial) - but informational here, because v2 sends the entire object on every push. The v1 schema carries the full note on why a v1 consumer must read it."
},
"msg": { "type": "integer" },
"percent": { "type": "integer" },
"plate_cnt": { "type": "integer" },
@@ -2,13 +2,12 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/common/envelope.schema.json",
"title": "MessageEnvelope",
"description": "Row provenance shared by the continuum/v1 domain types that $ref it: vendor, material, spool, printer_info, firmware_info and module_info. The _by fields are the actor's id, not a timestamp - they were typed as date-time here, copied from the _at lines above them, which would have rejected every real value the moment anything started validating against this.",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"created_at": { "type": "string", "format": "date-time" },
"created_by": { "type": "string", "format": "uuid" },
"created_by": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" },
"updated_by": { "type": "string", "format": "uuid" }
"updated_by": { "type": "string", "format": "date-time" }
}
}
@@ -1,43 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/ai/frame_task.schema.json",
"title": "FrameTask",
"description": "One camera still, handed from continuum-backend to continuum-ai-worker for print-failure scoring. Produced by XADD onto the stream named below; consumed by XREADGROUP.\n\nThis is a Valkey stream entry, not a JSON document: the wire form is a flat field/value map and EVERY value is a string. That is why there are no numeric or boolean properties here and why a field with no value is omitted from the XADD rather than written as an empty string - an empty string is a value, and a consumer that treats it as one ends up looking up the object key \"\".\n\nField names are snake_case, matching every other schema in this repository. The backend's HTTP routes are camelCase; this wire is not an HTTP route, and the consumer on the other end is serde-derived Rust.",
"x-stream": "stream:camera_frames",
"x-consumer-group": "ai-worker",
"type": "object",
"properties": {
"farm_id": {
"type": "string",
"format": "uuid",
"description": "`farms.id`. Carried so a consumer can attribute load and alerts per farm without a database round trip."
},
"printer_id": {
"type": "string",
"format": "uuid",
"description": "`printers.id` - the backend's own UUID, already resolved from the gateway's printer id. NOT the gateway's `printers.toml` id, which means nothing outside its own farm."
},
"print_job_id": {
"type": "string",
"format": "uuid",
"description": "`print_jobs.id`, when the frame belongs to a tracked job. Omitted entirely when it does not - a printer may be streaming while idle, or running something queued outside Continuum. Consumers must treat an absent key and an empty value alike, and must not fabricate a job id."
},
"r2_object_key": {
"type": "string",
"minLength": 1,
"description": "Where the frame actually is, in the R2 bucket. The frame bytes are deliberately NOT in the stream: a stream entry is held in memory by the server and replayed to every consumer group, and a JPEG per printer per second would make the stream the largest thing in the deployment."
},
"captured_at": {
"type": "string",
"format": "date-time",
"description": "When the camera took the frame, from the gateway's clock - not when the backend enqueued it. These can differ by a lot: a gateway buffers frames while its uplink is down, so a task may arrive minutes after the moment it describes."
}
},
"required": [
"farm_id",
"printer_id",
"r2_object_key",
"captured_at"
],
"additionalProperties": false
}
@@ -1,47 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/ai/print_alert.schema.json",
"title": "PrintAlert",
"description": "A print-failure detection, published by continuum-ai-worker for live operator consoles.\n\nUnlike FrameTask this one IS a JSON document: it travels as the payload of a Valkey PUBLISH, which carries a single opaque string, so the structure has to be inside it.\n\nPUBLISH is fire-and-forget with no persistence and no delivery guarantee: an alert published while nothing is subscribed is gone. That is acceptable for the live-console path it feeds and is NOT acceptable as the record of a detection, which belongs in Postgres. As of 2026-10-11 nothing subscribes to this channel and nothing persists it - see the README section on the AI path.",
"x-channel": "channel:print_alerts",
"type": "object",
"properties": {
"printer_id": {
"type": "string",
"format": "uuid",
"description": "`printers.id`, taken from the frame task that produced the detection."
},
"print_job_id": {
"type": "string",
"format": "uuid",
"description": "Omitted when the frame was not attributed to a tracked job."
},
"label": {
"type": "string",
"description": "What the model thinks it saw, e.g. \"spaghetti\", \"layer_shift\", \"detachment\". A free string rather than an enum on purpose: the label set is a property of the model file, which changes without this schema changing."
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "The model's own score. Whether it is high enough to act on is the consumer's policy decision, not the worker's - the worker publishes what it found and does not threshold on behalf of an operator who may want to see near-misses."
},
"captured_at": {
"type": "string",
"format": "date-time",
"description": "From the frame, not from the detection. An operator needs to know when the printer looked like this, not when a GPU got round to looking."
},
"r2_object_key": {
"type": "string",
"description": "The frame the detection came from, so a console can show the operator the picture rather than only a label."
}
},
"required": [
"printer_id",
"label",
"confidence",
"captured_at",
"r2_object_key"
],
"additionalProperties": false
}
@@ -1,74 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/app/telemetry_message.schema.json",
"title": "AppTelemetryMessage",
"description": "What continuum-backend pushes down /ws/telemetry to an operator console. Discriminated on 'type'. NOTE the casing: camelCase, unlike continuum/v1/edge/ and every other schema here, because this is the backend's own HTTP vocabulary rather than a format shared with the Rust gateway - the browser already consumes camelCase from GET /printers and should not have to hold two models of a printer. A previous frontend invented its own shape against nothing (printerId, nozzleTempC, progressPct, a closed state enum) and no backend ever sent it; this file exists so both ends have something to mirror.",
"oneOf": [
{
"type": "object",
"title": "PrinterTelemetryPush",
"description": "One printer's current state, sent whenever its gateway reports a change. The payload is deliberately the same shape GET /printers/:id returns, so a client can apply it to the row it already has rather than reconciling two vocabularies. Only printers in an organisation the authenticated user belongs to are ever sent.",
"properties": {
"type": { "const": "printer.telemetry" },
"printer": { "$ref": "#/$defs/PrinterState" }
},
"required": ["type", "printer"],
"additionalProperties": false
},
{
"type": "object",
"title": "Ready",
"description": "Sent once, immediately after the socket authenticates, naming how many printers the user can see. Worth its own message rather than leaving the client to infer readiness from silence: an operator with no printers claimed and an operator whose gateway is offline both see nothing arrive, and those need different things said to them.",
"properties": {
"type": { "const": "ready" },
"visiblePrinters": { "type": "integer", "minimum": 0 }
},
"required": ["type", "visiblePrinters"],
"additionalProperties": false
}
],
"$defs": {
"PrinterState": {
"type": "object",
"description": "A printer as the console displays it. Every telemetry field is nullable because a printer that has never reported has no temperature, and 'no data yet' is not 0 degrees.",
"properties": {
"id": { "type": "string", "format": "uuid" },
"farmId": { "type": "string", "format": "uuid" },
"name": { "type": "string" },
"vendor": { "type": "string" },
"status": {
"type": ["string", "null"],
"description": "The backend's own narrowed status, which the UI filters and groups on."
},
"vendorState": {
"type": ["string", "null"],
"description": "The printer's own word for what it is doing - a Bambu's 'RUNNING', 'PREPARE', 'FINISH'. Kept alongside 'status' rather than replaced by it, because the narrowed enum necessarily loses detail an operator sometimes needs. Not an enum here on purpose: an unrecognised state must reach the screen as itself rather than being dropped."
},
"vendorErrorCode": { "type": ["integer", "null"] },
"nozzleTempC": { "type": ["number", "null"] },
"nozzleTargetTempC": { "type": ["number", "null"] },
"bedTempC": { "type": ["number", "null"] },
"bedTargetTempC": { "type": ["number", "null"] },
"progressPercent": { "type": ["integer", "null"] },
"currentLayer": { "type": ["integer", "null"] },
"totalLayers": { "type": ["integer", "null"] },
"remainingMinutes": { "type": ["integer", "null"] },
"currentJobName": { "type": ["string", "null"] },
"activeAlerts": {
"type": ["integer", "null"],
"description": "How many Health-Management-System entries the printer currently has active. A count, not messages: decoding them needs Bambu's HMS tables, which Continuum does not have."
},
"cameraStream": {
"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."
},
"lastSeenAt": {
"type": ["string", "null"],
"format": "date-time",
"description": "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 the status alone cannot say."
}
},
"required": ["id", "farmId", "name", "vendor"]
}
}
}
@@ -1,213 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/edge/client_message.schema.json",
"title": "EdgeClientMessage",
"description": "What a continuum-proxy gateway sends up to continuum-backend's /ws/edge/v1 route. Discriminated on 'type'. Field names are snake_case, matching every other schema in this repository - note that this differs from the camelCase the backend's HTTP routes use, because this wire format is shared with a Rust consumer whose serde structs derive these names directly.",
"oneOf": [
{
"type": "object",
"title": "Hello",
"description": "First message on every connection, before anything else is accepted. Carries NO credential: the edge JWT minted by POST /auth/edge-token goes in an Authorization: Bearer header on the WebSocket upgrade request, and is verified before the socket exists, so an unauthenticated peer gets an HTTP 401 and no socket at all. This message used to carry a 'token' field; it was removed, because a token in the first frame means a live socket and a parsed message for a peer that has proved nothing. farm_id here is redundant with the token's own claim and is checked against it rather than trusted - a gateway naming a farm its token was not minted for is refused.",
"properties": {
"type": {
"const": "hello"
},
"gateway_id": {
"type": "string"
},
"farm_id": {
"type": "string"
},
"stream_base_url": {
"type": "string",
"format": "uri",
"description": "Where this gateway serves camera restreams, e.g. http://gateway.local:1984 - the base of a go2rtc instance the gateway supervises. Optional: a gateway with no cameras configured, or with go2rtc not installed, omits it. Reported by the gateway rather than configured in the cloud because only the gateway knows the address it is actually reachable at. NOTE the consequence, which is a real limit and not a bug: this is a LAN address. A browser on the same network as the gateway can play the stream directly; one outside it cannot, and making that work needs a relay that does not exist yet. Anything surfacing this to a user has to cope with it being unreachable."
}
},
"required": [
"type",
"gateway_id",
"farm_id"
]
},
{
"type": "object",
"title": "Heartbeat",
"properties": {
"type": {
"const": "heartbeat"
},
"seq": {
"type": "integer",
"minimum": 0,
"description": "Monotonic per connection, restarting at 1 on reconnect. Echoed back in heartbeat_ack so the gateway can tell a stalled connection from a slow one."
}
},
"required": [
"type",
"seq"
]
},
{
"type": "object",
"title": "Telemetry",
"description": "One snapshot per printer that has something to report, on a timer. Sent as a batch rather than a message per printer: a farm has one uplink and many printers, and the backend wants a coherent picture of the farm at an instant more than it wants the lowest possible latency per machine.",
"properties": {
"type": {
"const": "telemetry"
},
"printers": {
"type": "array",
"items": {
"$ref": "./printer_telemetry.schema.json"
}
}
},
"required": [
"type",
"printers"
]
},
{
"type": "object",
"title": "CommandResult",
"description": "The reply to a server 'command'. Correlated by command_id, which the backend generates - unlike Bambu's sequence_id, Continuum owns both ends of this protocol, so the side that needs the correlation gets to choose it.",
"properties": {
"type": {
"const": "command_result"
},
"command_id": {
"type": "string"
},
"ok": {
"type": "boolean"
},
"error": {
"type": [
"string",
"null"
],
"description": "Present only when ok is false. A human-readable message, not a code: its only destination is an operator looking at why a command did not take. Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
}
},
"required": [
"type",
"command_id",
"ok"
]
},
{
"type": "object",
"title": "FrameBatch",
"description": "Camera stills already uploaded to object storage, handed over for the AI worker to score. Not yet sent by any gateway - continuum-proxy restreams cameras through go2rtc and has no frame-capture path yet - but the backend consumes it, so it is part of the contract.",
"properties": {
"type": {
"const": "frame_batch"
},
"printer_id": {
"type": "string"
},
"print_job_id": {
"type": [
"string",
"null"
],
"format": "uuid",
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"frames": {
"type": "array",
"items": {
"type": "object",
"properties": {
"object_key": {
"type": "string"
},
"captured_at": {
"type": "string",
"format": "date-time"
}
},
"required": [
"object_key",
"captured_at"
]
}
}
},
"required": [
"type",
"printer_id",
"frames"
]
},
{
"type": "object",
"title": "WebrtcAnswer",
"description": "go2rtc's answer SDP for a WebrtcOffer, passed through verbatim.",
"properties": {
"type": {
"const": "webrtc_answer"
},
"session_id": {
"type": "string"
},
"sdp": {
"type": "string"
}
},
"required": [
"type",
"session_id",
"sdp"
]
},
{
"type": "object",
"title": "WebrtcError",
"description": "The gateway could not get an answer: no such camera, go2rtc not running, or it refused the offer. A variant of its own rather than an empty answer, so the viewer is told something instead of watching a video element that never loads.",
"properties": {
"type": {
"const": "webrtc_error"
},
"session_id": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"type",
"session_id",
"message"
]
},
{
"type": "object",
"title": "DiscoveredPrinters",
"description": "Printers the gateway found on its own network or USB ports that are not in its fleet file, so an operator can add one in the web app without first reading an IP address or a serial off a machine. Sent as a whole batch and treated as a replacement for what this gateway previously reported, not as additions: a printer that has gone is reported by its absence, and a gateway that can see nothing sends an empty list. Only sent when discovery is enabled in the gateway's own config, which it is NOT by default - probing a customer's network is something they have to ask for.",
"properties": {
"type": {
"const": "discovered_printers"
},
"printers": {
"type": "array",
"maxItems": 256,
"items": {
"$ref": "./discovered_printer.schema.json"
}
},
"scanned_at": {
"description": "When the gateway finished the scan these results came from, so the cloud can show the age of the list rather than implying it is live.",
"type": "string",
"format": "date-time"
}
},
"required": [
"type",
"printers"
]
}
]
}
@@ -1,55 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://schemas.continuum.dev/continuum/v1/edge/discovered_printer.schema.json",
"title": "DiscoveredPrinter",
"description": "A printer a gateway found on its own network or USB ports that is NOT in its fleet file - so it is a candidate for an operator to add, not a printer the gateway can drive. It deliberately carries no credential: a Bambu access code is on the printer's screen and a PrusaLink API key is in its web UI, and no probe can learn either. The point of sending these to the cloud is that an operator adding a printer in the web app should not have to go and read an IP address or a 15-character serial off a machine. Every string here is reported by a device on the customer's network and must be treated as untrusted display text - hence the length caps - because anything that answers an SSDP query or an HTTP probe chooses its own name.",
"type": "object",
"properties": {
"vendor": {
"description": "The `vendor` value this candidate would need in the gateway's fleet file. `octoprint` rather than `prusa_link` is the honest answer when a probe saw only the OctoPrint API, which PrusaLink also serves - see `ambiguous_with`.",
"type": "string",
"enum": ["bambu", "klipper", "prusa_link", "octoprint", "prusa_serial"]
},
"address": {
"description": "Where the gateway found it: an IP or hostname for a network printer, or a device path like `/dev/ttyACM0` for one on a serial port. Not a URL, and not reachable from the cloud - it is an address on the gateway's network, shown so an operator can recognise which machine this is.",
"type": "string",
"minLength": 1,
"maxLength": 255
},
"serial": {
"description": "The printer's serial number where the probe could learn it without a credential - which today means a Bambu printer's TLS certificate CN, or a Marlin board's `UUID`. Absent is normal and not an error.",
"type": "string",
"maxLength": 64
},
"model": {
"description": "The vendor's own model code or machine type, e.g. Bambu's `O1C2` or Marlin's `MACHINE_TYPE`.",
"type": "string",
"maxLength": 128
},
"name": {
"description": "The name the printer reports for itself, which is usually whatever the operator typed into their slicer or set as the host's hostname. Display only.",
"type": "string",
"maxLength": 128
},
"detail": {
"description": "Free-form human detail to recognise the machine by - a firmware version, a USB vendor string, or why a probe could not say more. Never parsed, and never written into a config file.",
"type": "string",
"maxLength": 256
},
"ambiguous_with": {
"description": "A vendor this could equally be, when the probe genuinely cannot tell. Today only `prusa_link` versus `octoprint`: PrusaLink implements OctoPrint's API, so without a credential nothing on the network distinguishes them. Present means the operator has to choose; the web app should offer both rather than picking one.",
"type": "string",
"enum": ["bambu", "klipper", "prusa_link", "octoprint", "prusa_serial"]
},
"source": {
"description": "Which probe found it, which bounds how much it can know: `certificate` and `usb_descriptor` are passive reads, `ssdp` is an unsolicited announcement, `http_api` an identity endpoint, and `serial_handshake` an `M115` that reset the board to ask.",
"type": "string",
"enum": ["ssdp", "certificate", "http_api", "usb_descriptor", "serial_handshake"]
},
"driver_ready": {
"description": "Whether this gateway build can actually drive this vendor. False means the printer was found and reported but its client is still a stub, so adding it would produce a printer that connects and does nothing - the web app should say so rather than offering it as ready to use.",
"type": "boolean"
}
},
"required": ["vendor", "address", "source", "driver_ready"]
}
@@ -1,102 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/edge/printer_command.schema.json",
"title": "PrinterCommand",
"description": "The closed set of things the cloud may ask a printer to do. Deliberately small. It now includes start_print, which was held back until there was a job path to drive it; the thing to understand about that variant is that its acceptance says almost nothing - see its own description. Every variant is closed (additionalProperties: false), unlike printer_telemetry which is deliberately open: telemetry is written by gateways in the field and read liberally, while a command is written by the backend, is a handful of fields, and moves a physical machine - an unrecognised field there is a bug on the sending side, and silently dropping it would let a caller misspell a field and watch its command succeed while doing something else.",
"oneOf": [
{
"type": "object",
"title": "StartPrint",
"description": "Print a job's file. The gateway downloads it from `url`, transfers it to the printer, and starts it. Three things about this variant that a caller must not get wrong. (1) Its result means the gateway accepted the job, not that anything is printing: a real end-to-end run on a P1P (2026-10-15) was accepted, uploaded byte-perfect, reached RUNNING, and then the printer paused itself on a physical filament fault. Job state has to be driven by the telemetry that follows, never by this reply. (2) There is no filament or AMS field on purpose. The gateway reads the mapping out of the project archive's own Metadata/slice_info.config, because the archive is the only thing that knows what each plate was sliced for - guessing it instead is what made that same printer heat to 250C and pause itself at 1%. (3) `url` is short-lived and must be presigned at dispatch time, not at job creation: a queued job may wait days, and a URL minted when it was queued would be expired by the time anything sent it.",
"properties": {
"action": {
"const": "start_print"
},
"job_id": {
"type": "string",
"format": "uuid",
"description": "The cloud's print_jobs.id. The gateway does not need it to do the work - it is here so that a gateway's logs can be lined up against a job without reading its own telemetry back out of the cloud."
},
"file_name": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "What to call the file on the printer's own storage. A `.3mf` is treated as a sliced project (filament mapping derived from inside it); anything else is treated as bare gcode, which carries no mapping and prints with whatever its own tool changes select."
},
"url": {
"type": "string",
"format": "uri",
"description": "A presigned, expiring GET for the job file. Deliberately a URL rather than the bytes: the file is tens of megabytes, this is a JSON control channel, and handing the gateway a URL keeps object-store credentials out of every gateway on every farm. Expect it to be valid for minutes, and note that an upload to a printer is slow - 0.2 MiB/s measured over FTPS to a P1P - so the download must finish before the transfer starts, not stream through it."
}
},
"required": [
"action",
"job_id",
"file_name",
"url"
],
"additionalProperties": false
},
{
"type": "object",
"title": "SetFanSpeed",
"properties": {
"action": {
"const": "set_fan_speed"
},
"percent": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "A percentage on this wire regardless of what the vendor wants. Each gateway converts: for Bambu that means an M106 PWM value over MQTT, which the printer then reports back as one of 16 fan gears - so a value set here does not read back identically."
}
},
"required": [
"action",
"percent"
],
"additionalProperties": false
},
{
"type": "object",
"title": "ControlPrint",
"description": "Pause, resume or stop the print the printer is currently running. Confirmed live on a P1P (2026-10-11): all three are accepted and a stop during PREPARE/RUNNING moved the printer to FAILED, which is how Bambu reports an operator-cancelled print. Note what the printer's acceptance does not mean - all three answered success while it was IDLE with nothing to pause, so a caller that needs to know the state actually changed must read telemetry back rather than trust the command result.",
"properties": {
"action": {
"const": "control_print"
},
"control": {
"enum": [
"pause",
"resume",
"stop"
],
"description": "Stop is a cancel, not a pause: it ends the print and it cannot be undone. A UI should treat it accordingly."
}
},
"required": [
"action",
"control"
],
"additionalProperties": false
},
{
"type": "object",
"title": "SendGcodeLine",
"properties": {
"action": {
"const": "send_gcode_line"
},
"line": {
"type": "string",
"description": "A single raw gcode line. By far the most powerful variant, and the one to be most careful about ever exposing to a UI."
}
},
"required": [
"action",
"line"
],
"additionalProperties": false
}
]
}
@@ -1,118 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/edge/printer_telemetry.schema.json",
"title": "PrinterTelemetry",
"description": "A flattened snapshot of one printer, in the terms an operator console displays. Deliberately not any vendor's native status shape: it has to describe a Bambu, a Prusa and a Klipper printer alike, and Bambu's wire format in particular has traps that should not reach the rest of the system (fan speeds are 0-15 gears rather than a percent, temperatures arrive as either integers or floats, and protocol v2 reuses one sequence_id indefinitely). Every field but printer_id and name is optional, because a printer that is unreachable or has only just connected genuinely has nothing to report, and 'no data yet' is not the same as zero.",
"type": "object",
"properties": {
"printer_id": {
"type": "string",
"description": "The gateway's own id for this printer, from its printers.toml - NOT the vendor serial number and NOT the backend's printers.id UUID. The backend resolves it to a UUID via printers.gateway_printer_id, scoped to the farm the gateway authenticated as."
},
"name": {
"type": "string"
},
"state": {
"type": [
"string",
"null"
],
"description": "The printer's own state string, passed through rather than narrowed to an enum here, so an unrecognised state reaches the backend as itself instead of being dropped at the edge. Bambu emits IDLE, RUNNING, PAUSE, FINISH and FAILED. Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"percent": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 100,
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"stage": {
"type": "integer",
"description": "The printer's current stage, when it is doing something other than laying down plastic: bed levelling, nozzle cleaning, extrusion calibration and so on. Bambu's `stg_cur`, passed through as the printer's own number rather than decoded, because the code-to-name table is not something we have. 0 means printing.\n\nWithout this, progress is ambiguous in a way that reads as a fault. Confirmed on a P1P (2026-10-11): `percent` climbed to 14% while `layer` stayed 0 for several minutes, because the printer was working through its pre-print stages - it reported `stg_cur` 54, then 14, then 13, then 0, and only then began layer 1. A console showing 14% at layer 0 with no stage looks stuck."
},
"layer": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"total_layers": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"remaining_minutes": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"job_name": {
"type": [
"string",
"null"
],
"description": "Normalised at the edge: a Bambu reports an empty string rather than omitting the field when idle, and the gateway turns that into an absent value so consumers need not know it. Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"nozzle_temp": {
"type": [
"number",
"null"
],
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"nozzle_target": {
"type": [
"number",
"null"
],
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"bed_temp": {
"type": [
"number",
"null"
],
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"bed_target": {
"type": [
"number",
"null"
],
"description": "Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"print_error": {
"type": [
"integer",
"null"
],
"description": "Vendor error code. Non-zero means the printer is reporting a fault. Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"active_alerts": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "How many Health-Management-System entries the printer currently has active. A count only for now - decoding the {attr, code} pairs into messages needs Bambu's HMS tables, which Continuum does not have. Readers must accept an explicit null here as equivalent to the key being absent: a gateway omits what it has no value for, but a serde-derived Rust producer emits null unless every field is annotated, and rejecting a whole batch over that is not worth it."
},
"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."
}
},
"required": [
"printer_id",
"name"
]
}
@@ -1,114 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.octoturge.com/Continuum/continuum-schemas/raw/branch/main/schemas/continuum/v1/edge/server_message.schema.json",
"title": "EdgeServerMessage",
"description": "What continuum-backend sends back down the same WebSocket. Discriminated on 'type'; snake_case for the same reason as EdgeClientMessage.",
"oneOf": [
{
"type": "object",
"title": "HelloAck",
"properties": {
"type": {
"const": "hello_ack"
},
"session_id": {
"type": "string"
}
},
"required": [
"type",
"session_id"
]
},
{
"type": "object",
"title": "HelloError",
"description": "The handshake was rejected. The connection closes immediately after this.",
"properties": {
"type": {
"const": "hello_error"
},
"message": {
"type": "string"
}
},
"required": [
"type",
"message"
]
},
{
"type": "object",
"title": "HeartbeatAck",
"properties": {
"type": {
"const": "heartbeat_ack"
},
"seq": {
"type": "integer",
"minimum": 0,
"description": "Echoes the seq from the heartbeat being acknowledged."
}
},
"required": [
"type",
"seq"
]
},
{
"type": "object",
"title": "Command",
"description": "Do something to one printer on this gateway. Answered with a client command_result carrying the same command_id.",
"properties": {
"type": {
"const": "command"
},
"command_id": {
"type": "string"
},
"printer_id": {
"type": "string",
"description": "The gateway's own printer id, not the backend UUID - the backend translates on the way out, so a gateway never has to know its printers' cloud identities."
},
"command": {
"$ref": "./printer_command.schema.json"
}
},
"required": [
"type",
"command_id",
"printer_id",
"command"
]
},
{
"type": "object",
"title": "WebrtcOffer",
"description": "Relay a browser's WebRTC offer to this gateway's go2rtc and send the answer back. SIGNALLING ONLY - no video crosses this socket. The gateway POSTs the offer to its own go2rtc over loopback (which go2rtc exempts from its API password, so no credential is needed or sent) and returns the answer as a WebrtcAnswer, after which the browser and go2rtc negotiate media directly over ICE. This exists because a browser cannot signal to a gateway itself: measured against go2rtc 1.9.14, it sends no CORS headers on anything and checks its password before CORS, so the unauthenticated preflight a browser always sends gets a 401. Relaying only the signalling keeps the media peer-to-peer - on a workshop LAN it never leaves the building, and go2rtc also offers a server-reflexive candidate, so a remote viewer can often hole-punch straight to the gateway. A TURN server in go2rtc's ice_servers is the fallback for the NATs where that fails; it is configuration, not a change to this protocol.",
"properties": {
"type": {
"const": "webrtc_offer"
},
"session_id": {
"type": "string",
"description": "Correlates the answer with this offer. One negotiation, not a lasting subscription."
},
"camera_stream": {
"type": "string",
"description": "The go2rtc stream name, which is what PrinterTelemetry.camera_stream reported. Deliberately not called a printer id: the two are equal today, but one is the gateway's own naming and the other is the cloud's."
},
"sdp": {
"type": "string",
"description": "The browser's offer SDP, passed through verbatim."
}
},
"required": [
"type",
"session_id",
"camera_stream",
"sdp"
],
"additionalProperties": false
}
]
}
-15
View File
@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"declaration": true,
"composite": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}
+14 -8
View File
@@ -1,10 +1,16 @@
{
// A solution file: it builds nothing itself and only points at the real
// projects. It used to carry the compilerOptions directly with no `include`,
// which made `tsc -b` compile every .ts file in the repository as one project
// and emit .js/.d.ts next to the sources - overriding each package's own
// `outDir`, and leaving generated files in src/ that then got committed.
// Shared options now live in tsconfig.base.json, which each package extends.
"files": [],
"references": [{ "path": "./packages/ts-types" }]
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"declaration": true,
"composite": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"exclude": ["examples", "node_modules"]
}