nx_moonraker_ready_check.nx source
↩ module page · 130 lines · 5303 B
1// nx_moonraker_ready_check.nx -- pre-print Klipper state gate.
2//
3// Per NISHI_3D_PRINT_BULLETPROOF_PREFLIGHT_ROADMAP ยง1.4 (printer-
4// side ready check): refuse to send print-start unless Klipper is
5// in "ready" state. Catches the failure mode where operator sends
6// a print to a printer in shutdown/error/startup state and Klipper
7// silently ignores it.
8//
9// Composes:
10// nx_moonraker_io -- GET /printer/info roundtrip
11// nx_moonraker_client -- HTTP status parse
12// substring check -- simple text scan for "state":"ready"
13// (full nx_json composition queued v2)
14//
15// Klipper states (per Klipper API server docs):
16// "ready" -- accepting commands, ready to print
17// "startup" -- initializing, not yet ready
18// "shutdown" -- entered error / safe-shutdown state
19// "error" -- runtime error, operator intervention needed
20//
21// Only "ready" returns SAFE. Everything else returns NOT_READY
22// with the actual state-string available for operator diagnosis.
23//
24// license_tier: ORIGINAL
25
26import "nx_syscalls.nx"
27import "nx_moonraker_client.nx"
28import "nx_moonraker_io.nx"
29
30// ===== sealed-enum verdicts ========================================
31
32const NX_READY_OK: i64 = 0
33const NX_READY_NOT_READY: i64 = 1
34const NX_READY_BAD_RESPONSE: i64 = 2
35const NX_READY_NETWORK_ERR: i64 = 3
36
37func nx_ready_verdict_name(v: i64) -> *u8 {
38 if v == NX_READY_OK { return "OK" }
39 if v == NX_READY_NOT_READY { return "NOT_READY" }
40 if v == NX_READY_BAD_RESPONSE { return "BAD_RESPONSE" }
41 if v == NX_READY_NETWORK_ERR { return "NETWORK_ERR" }
42 return "UNKNOWN"
43}
44
45// ===== response-body substring scan ===============================
46//
47// Returns 1 if `needle` (NUL-terminated) appears in buf[0..len).
48// Plain byte-compare loop -- no regex / no JSON parser needed.
49
50func nx_ready_contains(buf: *u8, len: i64, needle: *u8) -> i64 {
51 var nlen: i64 = 0
52 while needle[nlen] != 0 { nlen = nlen + 1 }
53 if nlen == 0 { return 1 }
54 if len < nlen { return 0 }
55 var i: i64 = 0
56 while i <= len - nlen {
57 var j: i64 = 0
58 var matched: i64 = 1
59 while j < nlen {
60 if buf[i + j] != needle[j] { matched = 0; j = nlen }
61 j = j + 1
62 }
63 if matched == 1 { return 1 }
64 i = i + 1
65 }
66 return 0
67}
68
69// ===== response-body state classification =========================
70//
71// Given a Moonraker /printer/info response body, return the
72// appropriate ready-check verdict. Looks for "state":"ready"
73// substring (with optional whitespace tolerated via two patterns).
74// On any non-ready state present, NOT_READY. On no state field at
75// all, BAD_RESPONSE.
76
77func nx_ready_classify_body(body: *u8, body_len: i64) -> i64 {
78 // Tight form: "state":"ready"
79 if nx_ready_contains(body, body_len, "\"state\":\"ready\"") == 1 {
80 return NX_READY_OK
81 }
82 // Loose form: "state": "ready" (with space after colon)
83 if nx_ready_contains(body, body_len, "\"state\": \"ready\"") == 1 {
84 return NX_READY_OK
85 }
86 // Some other state present? Look for the "state" key + any value.
87 if nx_ready_contains(body, body_len, "\"state\"") == 1 {
88 return NX_READY_NOT_READY
89 }
90 return NX_READY_BAD_RESPONSE
91}
92
93// ===== loopback variant (test entry) ==============================
94//
95// Issues GET /printer/info to 127.0.0.1:port via nx_mr_get_loopback,
96// parses status, classifies body. Returns one of NX_READY_*.
97
98func nx_moonraker_ready_check_loopback(port: i64,
99 host: *u8, host_len: i64,
100 api_key: *u8, api_key_len: i64,
101 scratch: *u8, scratch_cap: i64,
102 resp_buf: *u8, resp_cap: i64) -> i64 {
103 let n_resp: i64 = nx_mr_get_loopback(port, host, host_len,
104 "/printer/info", 13,
105 api_key, api_key_len,
106 scratch, scratch_cap,
107 resp_buf, resp_cap)
108 if n_resp <= 0 { return NX_READY_NETWORK_ERR }
109 let status: i64 = nx_mr_parse_status(resp_buf, n_resp)
110 if status != 200 { return NX_READY_BAD_RESPONSE }
111 return nx_ready_classify_body(resp_buf, n_resp)
112}
113
114// ===== operator-facing variant (arbitrary IPv4) ===================
115
116func nx_moonraker_ready_check_ipv4(ipv4: i64, port: i64,
117 host: *u8, host_len: i64,
118 api_key: *u8, api_key_len: i64,
119 scratch: *u8, scratch_cap: i64,
120 resp_buf: *u8, resp_cap: i64) -> i64 {
121 let n_resp: i64 = nx_mr_get_ipv4(ipv4, port, host, host_len,
122 "/printer/info", 13,
123 api_key, api_key_len,
124 scratch, scratch_cap,
125 resp_buf, resp_cap)
126 if n_resp <= 0 { return NX_READY_NETWORK_ERR }
127 let status: i64 = nx_mr_parse_status(resp_buf, n_resp)
128 if status != 200 { return NX_READY_BAD_RESPONSE }
129 return nx_ready_classify_body(resp_buf, n_resp)
130}