Close the printer command variants

Found by a test of mine that asserted the union was closed and then failed:
TypeBox objects permit unknown properties by default, so a `start_print`
carrying an `ams_mapping` validated and the field was silently dropped.

Every command variant is now `additionalProperties: false`, and
`printer_telemetry` deliberately stays open. The asymmetry follows from who
writes each message. Telemetry comes from gateways in the field and is read
liberally: an unknown field must not cost a whole farm's batch, and a gateway
newer than the backend should still be understood. A command is written by
the backend, is four fields, and moves a physical machine - there an
unrecognised field is a bug on the sending side, and dropping it silently is
the worst outcome available, because a caller that misspells a field watches
its command succeed while the machine does something else.

`ams_mapping` is the case that matters: it is deliberately absent from
`start_print` because the gateway reads the mapping out of the project
archive, and a caller passing one has misunderstood something that previously
cost a heated nozzle and a self-paused print. It is now told so.
This commit is contained in:
2026-10-15 22:53:45 +00:00
parent b454e30c87
commit 99545703cb
2 changed files with 61 additions and 14 deletions
+21 -4
View File
@@ -74,6 +74,23 @@ export type PrinterTelemetry = Static<typeof PrinterTelemetrySchema>;
* 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"),
@@ -98,7 +115,7 @@ export const PrinterCommandSchema = Type.Union([
* 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"),
/**
@@ -107,7 +124,7 @@ export const PrinterCommandSchema = Type.Union([
* is set here does not read back identically.
*/
percent: Type.Integer({ minimum: 0, maximum: 100 }),
}),
}, { additionalProperties: false }),
Type.Object({
action: Type.Literal("control_print"),
/**
@@ -124,12 +141,12 @@ export const PrinterCommandSchema = Type.Union([
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>;