Expose control_print, and say what the gateway's ok does not mean
The route's body is `PrinterCommandSchema` straight from the shared schema, so adding the variant there is what exposes it here - that is the point of taking the type from one place. What needed saying is in the comments: stop is a cancel with no undo, and a gateway returning ok means the printer accepted the command rather than that the print state changed, since an idle P1P answers success to all three verbs. A UI that puts stop where pause is expected, or that treats the 200 as confirmation, gets this wrong. The contract test gains the three control frames and one guard: a verb only one vendor recognises - `cancel`, Moonraker's word for it - must not validate, or it reaches a gateway that cannot map it. Mirrored in continuum-proxy/tests/uplink_protocol.rs.
This commit is contained in:
+12
-2
@@ -131,6 +131,14 @@ export const printerRoutes = new Elysia({ prefix: "/printers" })
|
|||||||
* physical machine, and the blast radius of getting this boundary wrong is
|
* physical machine, and the blast radius of getting this boundary wrong is
|
||||||
* a ruined print or a hot end driven into a plate. It is the one role
|
* a ruined print or a hot end driven into a plate. It is the one role
|
||||||
* boundary here that is not about data.
|
* boundary here that is not about data.
|
||||||
|
*
|
||||||
|
* As of 2026-10-11 the set includes `control_print`, whose `stop` is a
|
||||||
|
* cancel: confirmed on a real P1P, it ends the running print and the
|
||||||
|
* printer then reports `FAILED`. There is no undo, so a UI must not put it
|
||||||
|
* where `pause` is expected. Note also that a gateway returning `ok` means
|
||||||
|
* the printer *accepted* the command - on an idle P1P all three verbs
|
||||||
|
* answer success - so whether the state changed has to be read back from
|
||||||
|
* telemetry.
|
||||||
*/
|
*/
|
||||||
.post(
|
.post(
|
||||||
"/:id/commands",
|
"/:id/commands",
|
||||||
@@ -166,8 +174,10 @@ export const printerRoutes = new Elysia({ prefix: "/printers" })
|
|||||||
},
|
},
|
||||||
// The same command set the gateway accepts, straight from the shared
|
// The same command set the gateway accepts, straight from the shared
|
||||||
// schema - so this route cannot drift into offering something no gateway
|
// schema - so this route cannot drift into offering something no gateway
|
||||||
// can execute. Note what is absent: there is no "start a print" command
|
// can execute. Note what is absent: there is still no "start a print"
|
||||||
// yet, deliberately.
|
// command, though no longer because the path is unproven (it was
|
||||||
|
// confirmed on a real P1P on 2026-10-11) - nothing uploads a sliced file
|
||||||
|
// to a gateway, and nothing decides which printer runs which job.
|
||||||
{ params: IdParams, body: PrinterCommandSchema, detail: { tags: ["printers"] } },
|
{ params: IdParams, body: PrinterCommandSchema, detail: { tags: ["printers"] } },
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
|
|||||||
@@ -87,6 +87,24 @@ describe("backend -> gateway frames", () => {
|
|||||||
printer_id: "p1p",
|
printer_id: "p1p",
|
||||||
command: { action: "send_gcode_line", line: "M106 S0" },
|
command: { action: "send_gcode_line", line: "M106 S0" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
command_id: "9a7d2f12-0000-4000-8000-000000000000",
|
||||||
|
printer_id: "p1p",
|
||||||
|
command: { action: "control_print", control: "pause" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
command_id: "9a7d2f13-0000-4000-8000-000000000000",
|
||||||
|
printer_id: "p1p",
|
||||||
|
command: { action: "control_print", control: "resume" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "command",
|
||||||
|
command_id: "9a7d2f14-0000-4000-8000-000000000000",
|
||||||
|
printer_id: "p1p",
|
||||||
|
command: { action: "control_print", control: "stop" },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const frame of outbound) {
|
for (const frame of outbound) {
|
||||||
@@ -95,10 +113,29 @@ describe("backend -> gateway frames", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("rejects a print control verb the gateway does not implement", () => {
|
||||||
|
// "cancel" is the trap: it is what Moonraker calls this, and the Klipper
|
||||||
|
// implementation behind `control_print` really does send a cancel - but
|
||||||
|
// the wire word is "stop". A verb that only one vendor recognises must
|
||||||
|
// not validate here, or it reaches a gateway that cannot map it.
|
||||||
|
for (const control of ["cancel", "abort", "Pause", ""]) {
|
||||||
|
expect(
|
||||||
|
Value.Check(EdgeServerMessageSchema, {
|
||||||
|
type: "command",
|
||||||
|
command_id: "x",
|
||||||
|
printer_id: "p1p",
|
||||||
|
command: { action: "control_print", control },
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("has no start-print command to send", () => {
|
it("has no start-print command to send", () => {
|
||||||
// Deliberate: the gateway can start an uploaded file, but that path has
|
// Still deliberate, but no longer for the original reason: starting an
|
||||||
// never been run against real hardware. Delete this test when it has been,
|
// uploaded file *has* now been confirmed on a real P1P (2026-10-11).
|
||||||
// and add the variant to the schema.
|
// What is missing is the path around it - nothing uploads a sliced file
|
||||||
|
// to a gateway and nothing decides which printer runs which job. Delete
|
||||||
|
// this test when there is something to start.
|
||||||
expect(
|
expect(
|
||||||
Value.Check(EdgeServerMessageSchema, {
|
Value.Check(EdgeServerMessageSchema, {
|
||||||
type: "command",
|
type: "command",
|
||||||
|
|||||||
Reference in New Issue
Block a user