Define the /ws/edge/v1 gateway uplink protocol
continuum-proxy and continuum-backend each had their own idea of what the edge WebSocket carried, and the two did not overlap anywhere: the backend expected an `auth` handshake the gateway never sends, telemetry shaped one printer per message where the gateway sends a batch, and UUID printer ids a gateway has no way to know. Neither end had a command path that matched the other. Nothing failed, because nothing was written down for both sides to mirror. This is that document. schemas/continuum/v1/edge/ is canonical: client_message, server_message, printer_telemetry and printer_command. packages/ts-types gets its first real TypeBox mirror - the placeholder there said to add one "once something actually consumes it", and continuum-backend's edge route now does. The Rust mirror stays in continuum-proxy/src/uplink/protocol.rs rather than packages/rust-types, since the gateway is its only Rust consumer. Two decisions worth recording: Field names are snake_case, unlike the backend's HTTP routes. The other end is serde-derived Rust, and snake_case is what those structs produce without a rename attribute on every field. It is also what the rest of this repository already uses. Optional fields accept an explicit null as well as being absent. A gateway omits what it has no value for, which is what these schemas describe - but serde emits null by default unless every field is annotated, and a single such field would otherwise fail validation and take a whole farm's telemetry batch down with it. Strict in what we send, liberal in what we accept; the contract tests on both sides pin down each half. Also bumps this package's @sinclair/typebox to ^0.33.12 to match what continuum-backend already pins. It was on ^0.32.0, which resolved to a separate 0.32.37 install - two TypeBox versions means two incompatible TSchema identities, and Elysia rejected every schema from here on those grounds alone.
This commit is contained in:
@@ -10,6 +10,6 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.32.0"
|
||||
"@sinclair/typebox": "^0.33.12"
|
||||
}
|
||||
}
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { type Static } from "@sinclair/typebox";
|
||||
/**
|
||||
* 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 declare const PrinterTelemetrySchema: import("@sinclair/typebox").TObject<{
|
||||
/**
|
||||
* 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: import("@sinclair/typebox").TString;
|
||||
name: import("@sinclair/typebox").TString;
|
||||
/**
|
||||
* 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: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
|
||||
percent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
layer: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
total_layers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
remaining_minutes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
job_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
|
||||
nozzle_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
nozzle_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
bed_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
bed_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
/** Vendor error code; non-zero means the printer is reporting a fault. */
|
||||
print_error: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
/** Count of active Health-Management-System entries, not decoded messages. */
|
||||
active_alerts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
}>;
|
||||
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.
|
||||
*/
|
||||
export declare const PrinterCommandSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
||||
action: import("@sinclair/typebox").TLiteral<"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: import("@sinclair/typebox").TInteger;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
action: import("@sinclair/typebox").TLiteral<"send_gcode_line">;
|
||||
/** The most powerful variant by far; be careful about exposing it to a UI. */
|
||||
line: import("@sinclair/typebox").TString;
|
||||
}>]>;
|
||||
export type PrinterCommand = Static<typeof PrinterCommandSchema>;
|
||||
export declare const EdgeClientMessageSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"hello">;
|
||||
gateway_id: import("@sinclair/typebox").TString;
|
||||
farm_id: import("@sinclair/typebox").TString;
|
||||
token: import("@sinclair/typebox").TString;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"heartbeat">;
|
||||
/** Monotonic per connection, restarting at 1 on reconnect. */
|
||||
seq: import("@sinclair/typebox").TInteger;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"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: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
||||
/**
|
||||
* 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: import("@sinclair/typebox").TString;
|
||||
name: import("@sinclair/typebox").TString;
|
||||
/**
|
||||
* 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: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
|
||||
percent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
layer: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
total_layers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
remaining_minutes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
job_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
|
||||
nozzle_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
nozzle_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
bed_temp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
bed_target: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNull]>>;
|
||||
/** Vendor error code; non-zero means the printer is reporting a fault. */
|
||||
print_error: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
/** Count of active Health-Management-System entries, not decoded messages. */
|
||||
active_alerts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TNull]>>;
|
||||
}>>;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"command_result">;
|
||||
command_id: import("@sinclair/typebox").TString;
|
||||
ok: import("@sinclair/typebox").TBoolean;
|
||||
/** Only when `ok` is false. A message for an operator, not a code. */
|
||||
error: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"frame_batch">;
|
||||
printer_id: import("@sinclair/typebox").TString;
|
||||
print_job_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNull]>>;
|
||||
frames: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
||||
object_key: import("@sinclair/typebox").TString;
|
||||
captured_at: import("@sinclair/typebox").TString;
|
||||
}>>;
|
||||
}>]>;
|
||||
export type EdgeClientMessage = Static<typeof EdgeClientMessageSchema>;
|
||||
export declare const EdgeServerMessageSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"hello_ack">;
|
||||
session_id: import("@sinclair/typebox").TString;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"hello_error">;
|
||||
message: import("@sinclair/typebox").TString;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"heartbeat_ack">;
|
||||
seq: import("@sinclair/typebox").TInteger;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
type: import("@sinclair/typebox").TLiteral<"command">;
|
||||
command_id: import("@sinclair/typebox").TString;
|
||||
/** The gateway's own printer id - the backend translates on the way out. */
|
||||
printer_id: import("@sinclair/typebox").TString;
|
||||
command: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
|
||||
action: import("@sinclair/typebox").TLiteral<"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: import("@sinclair/typebox").TInteger;
|
||||
}>, import("@sinclair/typebox").TObject<{
|
||||
action: import("@sinclair/typebox").TLiteral<"send_gcode_line">;
|
||||
/** The most powerful variant by far; be careful about exposing it to a UI. */
|
||||
line: import("@sinclair/typebox").TString;
|
||||
}>]>;
|
||||
}>]>;
|
||||
export type EdgeServerMessage = Static<typeof EdgeServerMessageSchema>;
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Type } 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 = (schema) => 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 })),
|
||||
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 })),
|
||||
});
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const PrinterCommandSchema = Type.Union([
|
||||
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 }),
|
||||
}),
|
||||
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(),
|
||||
}),
|
||||
]);
|
||||
export const EdgeClientMessageSchema = Type.Union([
|
||||
Type.Object({
|
||||
type: Type.Literal("hello"),
|
||||
gateway_id: Type.String(),
|
||||
farm_id: Type.String(),
|
||||
token: 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()),
|
||||
}),
|
||||
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" }),
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
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 }) }),
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
@@ -0,0 +1,137 @@
|
||||
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 })),
|
||||
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.
|
||||
*/
|
||||
export const PrinterCommandSchema = Type.Union([
|
||||
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 }),
|
||||
}),
|
||||
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(),
|
||||
}),
|
||||
]);
|
||||
export type PrinterCommand = Static<typeof PrinterCommandSchema>;
|
||||
|
||||
export const EdgeClientMessageSchema = Type.Union([
|
||||
Type.Object({
|
||||
type: Type.Literal("hello"),
|
||||
gateway_id: Type.String(),
|
||||
farm_id: Type.String(),
|
||||
token: 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()),
|
||||
}),
|
||||
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" }),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]);
|
||||
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 }) }),
|
||||
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
-1
@@ -1 +1 @@
|
||||
export {};
|
||||
export * from "./edge";
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
export {};
|
||||
// The old Material/PrintParameters/PrintJob/Template mirrors were removed
|
||||
// (2026-09) when schemas/continuum/v1/ was restructured into
|
||||
// material/printer/vendor - see that directory and its README. Add a
|
||||
// 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";
|
||||
|
||||
@@ -4,4 +4,8 @@
|
||||
// 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.
|
||||
export {};
|
||||
|
||||
// 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";
|
||||
|
||||
Reference in New Issue
Block a user