nx_moonraker_telemetry_parse.nx source
↩ module page · 460 lines · 15747 B
1// nx_moonraker_telemetry_parse.nx -- pure-substrate parser for
2// Moonraker /printer/objects/query responses; populates the
3// thermal section of NxPrintTelemetry.
4//
5// Strategy: substring-window scan (no recursive descent). Moonraker
6// JSON shape is documented and stable enough that this approach
7// works without a full tree parser, but it IS brittle if Moonraker
8// changes the response layout -- a future arc can swap this for an
9// nx_json-based stateful parser without changing the API.
10//
11// For /printer/objects/query?extruder&heater_bed the response is:
12// {"result":{"status":{
13// "extruder": {"temperature":209.8,"target":210.0,...},
14// "heater_bed": {"temperature":59.7, "target":60.0, ...}
15// },"eventtime":12345.678}}
16//
17// We:
18// 1. Find the scope window `"extruder":{...}` (matched braces).
19// 2. Within that window, find `"temperature":` and read the next
20// number. Likewise `"target":`.
21// 3. Same for heater_bed.
22//
23// Numbers convert to whole °C (truncate fractional). 1°C precision
24// is acceptable given typical 2-5°C tolerances; operator can tighten
25// if needed. Sentinel -1 means "not found" for setpoints (lets
26// NxPrintTelemetry skip that axis cleanly).
27//
28// license_tier: ORIGINAL
29
30import "nx_syscalls.nx"
31import "nx_print_telemetry.nx"
32const NX_MAGIC_32768: i64 = 32768
33
34const NX_MR_PARSE_OK: i64 = 0
35const NX_MR_PARSE_NO_OBJECT: i64 = 1
36const NX_MR_PARSE_BAD_INPUT: i64 = 2
37
38// Compare body[off..off+n] to literal lit (length n). Returns 1 on
39// match, 0 otherwise.
40func mr_p_byte_eq(body: *u8, off: i64, lit: *u8, n: i64) -> i64 {
41 var i: i64 = 0
42 while i < n {
43 if body[off + i] != lit[i] { return 0 }
44 i = i + 1
45 }
46 return 1
47}
48
49// strlen-like for cstr literals.
50func mr_p_cstr_len(s: *u8) -> i64 {
51 var n: i64 = 0
52 while s[n] != 0 { n = n + 1 }
53 return n
54}
55
56// Find the first occurrence of needle (cstr) in body[start..body_len).
57// Returns offset of match or -1.
58func mr_p_find(body: *u8, body_len: i64, start: i64, needle: *u8) -> i64 {
59 let n_len: i64 = mr_p_cstr_len(needle)
60 if n_len == 0 { return start }
61 if start < 0 { return -1 }
62 if start + n_len > body_len { return -1 }
63 var i: i64 = start
64 let stop: i64 = body_len - n_len
65 while i <= stop {
66 if mr_p_byte_eq(body, i, needle, n_len) == 1 { return i }
67 i = i + 1
68 }
69 return -1
70}
71
72// Given offset of the opening `{` of a JSON object, return the offset
73// of the matching `}`. Counts braces, respects strings (skips escape
74// sequences). Returns -1 if unmatched.
75func mr_p_match_brace(body: *u8, body_len: i64, open_at: i64) -> i64 {
76 if open_at < 0 { return -1 }
77 if open_at >= body_len { return -1 }
78 if body[open_at] != 0x7B { return -1 } // '{'
79 var depth: i64 = 1
80 var p: i64 = open_at + 1
81 var in_string: i64 = 0
82 while p < body_len {
83 let c: i64 = body[p]
84 if in_string == 1 {
85 if c == 0x5C { // backslash escape
86 p = p + 2
87 } else {
88 if c == 0x22 { in_string = 0 } // closing quote
89 p = p + 1
90 }
91 } else {
92 if c == 0x22 { in_string = 1; p = p + 1 }
93 else {
94 if c == 0x7B { depth = depth + 1; p = p + 1 }
95 else {
96 if c == 0x7D {
97 depth = depth - 1
98 if depth == 0 { return p }
99 p = p + 1
100 } else { p = p + 1 }
101 }
102 }
103 }
104 }
105 return -1
106}
107
108// Parse an integer °C from body[off..end) starting at a number.
109// Skips leading whitespace. Stops at first non-digit (truncates
110// fractional part). Returns sentinel -32768 if no digit found.
111func mr_p_parse_int_degrees(body: *u8, off: i64, end: i64) -> i64 {
112 var q: i64 = off
113 var skipping: i64 = 1
114 while skipping == 1 {
115 if q >= end { skipping = 0 }
116 else {
117 let c: i64 = body[q]
118 if c == 0x20 { q = q + 1 } // space
119 else { if c == 0x09 { q = q + 1 } // tab
120 else { if c == 0x0A { q = q + 1 } // \n
121 else { if c == 0x0D { q = q + 1 } // \r
122 else { skipping = 0 }
123 }
124 }
125 }
126 }
127 }
128 if q >= end { return 0 - NX_MAGIC_32768 }
129
130 var neg: i64 = 0
131 if body[q] == 0x2D { neg = 1; q = q + 1 }
132
133 var have_digit: i64 = 0
134 var n: i64 = 0
135 var reading: i64 = 1
136 while reading == 1 {
137 if q >= end { reading = 0 }
138 else {
139 let d: i64 = body[q]
140 if d >= 0x30 {
141 if d <= 0x39 {
142 n = n * 10 + (d - 0x30)
143 have_digit = 1
144 q = q + 1
145 } else { reading = 0 }
146 } else { reading = 0 }
147 }
148 }
149
150 if have_digit == 0 { return 0 - NX_MAGIC_32768 }
151 if neg == 1 { return 0 - n }
152 return n
153}
154
155// Within body[scope_start..scope_end), find `"key":` then parse the
156// integer degrees that follow. Returns sentinel -32768 if not found.
157func mr_p_extract_int_in_scope(
158 body: *u8, body_len: i64,
159 scope_start: i64, scope_end: i64,
160 key_with_quotes_and_colon: *u8
161) -> i64 {
162 let k_off: i64 = mr_p_find(body, scope_end, scope_start, key_with_quotes_and_colon)
163 if k_off < 0 { return 0 - NX_MAGIC_32768 }
164 let k_len: i64 = mr_p_cstr_len(key_with_quotes_and_colon)
165 let val_start: i64 = k_off + k_len
166 return mr_p_parse_int_degrees(body, val_start, scope_end)
167}
168
169// Public entry: parse a Moonraker /printer/objects/query response
170// into the thermal fields of an NxPrintTelemetry event. Sets the
171// has_thermal flag if at least one of (extruder, heater_bed) was
172// found. Setpoints not present default to -1 (skip that axis).
173//
174// Returns NX_MR_PARSE_OK on success, NO_OBJECT if neither extruder
175// nor heater_bed object was found.
176func nx_moonraker_parse_thermal(
177 body: *u8, body_len: i64,
178 ev: *NxPrintTelemetry
179) -> i64 {
180 if (body as i64) == 0 { return NX_MR_PARSE_BAD_INPUT }
181 if (ev as i64) == 0 { return NX_MR_PARSE_BAD_INPUT }
182 if body_len <= 0 { return NX_MR_PARSE_BAD_INPUT }
183
184 var found: i64 = 0
185
186 // ----- extruder -----
187 let ex_key_start: i64 = mr_p_find(body, body_len, 0, "\"extruder\":{")
188 if ex_key_start >= 0 {
189 // Position of the '{'
190 let ex_brace: i64 = ex_key_start + 11 // strlen("\"extruder\":") = 11
191 let ex_end: i64 = mr_p_match_brace(body, body_len, ex_brace)
192 if ex_end > ex_brace {
193 let t: i64 = mr_p_extract_int_in_scope(
194 body, body_len, ex_brace + 1, ex_end, "\"temperature\":")
195 let s: i64 = mr_p_extract_int_in_scope(
196 body, body_len, ex_brace + 1, ex_end, "\"target\":")
197 if t != 0 - NX_MAGIC_32768 {
198 ev.hotend_actual_c = t
199 found = 1
200 }
201 if s != 0 - NX_MAGIC_32768 {
202 ev.hotend_setpoint_c = s
203 } else {
204 ev.hotend_setpoint_c = -1
205 }
206 }
207 }
208
209 // ----- heater_bed -----
210 let hb_key_start: i64 = mr_p_find(body, body_len, 0, "\"heater_bed\":{")
211 if hb_key_start >= 0 {
212 let hb_brace: i64 = hb_key_start + 13 // strlen("\"heater_bed\":") = 13
213 let hb_end: i64 = mr_p_match_brace(body, body_len, hb_brace)
214 if hb_end > hb_brace {
215 let t: i64 = mr_p_extract_int_in_scope(
216 body, body_len, hb_brace + 1, hb_end, "\"temperature\":")
217 let s: i64 = mr_p_extract_int_in_scope(
218 body, body_len, hb_brace + 1, hb_end, "\"target\":")
219 if t != 0 - NX_MAGIC_32768 {
220 ev.bed_actual_c = t
221 found = 1
222 }
223 if s != 0 - NX_MAGIC_32768 {
224 ev.bed_setpoint_c = s
225 } else {
226 ev.bed_setpoint_c = -1
227 }
228 }
229 }
230
231 if found == 1 { ev.has_thermal = 1 }
232
233 if found == 0 { return NX_MR_PARSE_NO_OBJECT }
234 return NX_MR_PARSE_OK
235}
236
237// ===== Motion section parser ======================================
238//
239// Parses a number like "120.5" into Q14 fixed-point: 120.5 × 16384
240// = 1974272. Truncates at the first non-digit (stops at fraction
241// exponent etc.; Moonraker doesn't emit exponential notation for
242// position/velocity values).
243//
244// Caller passes a byte range [off, end). Returns sentinel
245// 0 - 0x7FFFFFFF (-2147483647) for "no number found".
246const MR_P_Q14_NOT_FOUND: i64 = -2147483647
247const MR_P_Q14_SCALE: i64 = 16384
248
249func mr_p_parse_q14(body: *u8, off: i64, end: i64) -> i64 {
250 var q: i64 = off
251 var skipping: i64 = 1
252 while skipping == 1 {
253 if q >= end { skipping = 0 }
254 else {
255 let c: i64 = body[q]
256 if c == 0x20 { q = q + 1 }
257 else { if c == 0x09 { q = q + 1 }
258 else { if c == 0x0A { q = q + 1 }
259 else { if c == 0x0D { q = q + 1 }
260 else { skipping = 0 }
261 }
262 }
263 }
264 }
265 }
266 if q >= end { return MR_P_Q14_NOT_FOUND }
267
268 var neg: i64 = 0
269 if body[q] == 0x2D { neg = 1; q = q + 1 }
270
271 var have_digit: i64 = 0
272 var int_part: i64 = 0
273 var reading_int: i64 = 1
274 while reading_int == 1 {
275 if q >= end { reading_int = 0 }
276 else {
277 let d: i64 = body[q]
278 if d >= 0x30 {
279 if d <= 0x39 {
280 int_part = int_part * 10 + (d - 0x30)
281 have_digit = 1
282 q = q + 1
283 } else { reading_int = 0 }
284 } else { reading_int = 0 }
285 }
286 }
287
288 var frac_q14: i64 = 0
289 if q < end {
290 if body[q] == 0x2E {
291 q = q + 1
292 // Read up to 8 fractional digits (more than enough for Q14).
293 var frac_int: i64 = 0
294 var frac_div: i64 = 1
295 var n_frac: i64 = 0
296 var reading_frac: i64 = 1
297 while reading_frac == 1 {
298 if q >= end { reading_frac = 0 }
299 else {
300 if n_frac >= 8 { reading_frac = 0 }
301 else {
302 let d2: i64 = body[q]
303 if d2 >= 0x30 {
304 if d2 <= 0x39 {
305 frac_int = frac_int * 10 + (d2 - 0x30)
306 frac_div = frac_div * 10
307 n_frac = n_frac + 1
308 have_digit = 1
309 q = q + 1
310 } else { reading_frac = 0 }
311 } else { reading_frac = 0 }
312 }
313 }
314 }
315 if frac_div > 1 {
316 // frac_q14 = frac_int × Q14_SCALE / frac_div
317 frac_q14 = (frac_int * MR_P_Q14_SCALE) / frac_div
318 }
319 }
320 }
321
322 if have_digit == 0 { return MR_P_Q14_NOT_FOUND }
323
324 var n: i64 = int_part * MR_P_Q14_SCALE + frac_q14
325 if neg == 1 { n = 0 - n }
326 return n
327}
328
329// Within body[scope_start..scope_end), find `"key":` then parse the
330// Q14 number that follows. Returns MR_P_Q14_NOT_FOUND if not found.
331func mr_p_extract_q14_in_scope(
332 body: *u8, body_len: i64,
333 scope_start: i64, scope_end: i64,
334 key_with_quotes_and_colon: *u8
335) -> i64 {
336 let k_off: i64 = mr_p_find(body, scope_end, scope_start, key_with_quotes_and_colon)
337 if k_off < 0 { return MR_P_Q14_NOT_FOUND }
338 let k_len: i64 = mr_p_cstr_len(key_with_quotes_and_colon)
339 return mr_p_parse_q14(body, k_off + k_len, scope_end)
340}
341
342// Within body[scope_start..scope_end), find `"live_position":[`,
343// then parse the first 4 comma-separated Q14 numbers into
344// out_xyze[0..3]. Returns 1 on success, 0 if array not found.
345func mr_p_extract_live_position(
346 body: *u8, body_len: i64,
347 scope_start: i64, scope_end: i64,
348 out_x_box: *i64, out_y_box: *i64,
349 out_z_box: *i64, out_e_box: *i64
350) -> i64 {
351 let key_off: i64 = mr_p_find(body, scope_end, scope_start, "\"live_position\":[")
352 if key_off < 0 { return 0 }
353 // After `[`, scan up to next `]`
354 let arr_start: i64 = key_off + 17 // strlen("\"live_position\":[") = 17
355 var arr_end: i64 = arr_start
356 var found_close: i64 = 0
357 while found_close == 0 {
358 if arr_end >= scope_end { found_close = 1 }
359 else {
360 if body[arr_end] == 0x5D { found_close = 1 } // ']'
361 else { arr_end = arr_end + 1 }
362 }
363 }
364 if arr_end >= scope_end { return 0 }
365
366 // Parse 4 comma-separated numbers
367 var p: i64 = arr_start
368 let xv: i64 = mr_p_parse_q14(body, p, arr_end)
369 if xv == MR_P_Q14_NOT_FOUND { return 0 }
370 // Advance past the parsed number to the next comma
371 var seek: i64 = p
372 var found_c1: i64 = 0
373 while found_c1 == 0 {
374 if seek >= arr_end { found_c1 = 1 }
375 else {
376 if body[seek] == 0x2C { found_c1 = 1 } // ','
377 else { seek = seek + 1 }
378 }
379 }
380 if seek >= arr_end { return 0 }
381 p = seek + 1
382 let yv: i64 = mr_p_parse_q14(body, p, arr_end)
383 if yv == MR_P_Q14_NOT_FOUND { return 0 }
384 seek = p
385 var found_c2: i64 = 0
386 while found_c2 == 0 {
387 if seek >= arr_end { found_c2 = 1 }
388 else {
389 if body[seek] == 0x2C { found_c2 = 1 }
390 else { seek = seek + 1 }
391 }
392 }
393 if seek >= arr_end { return 0 }
394 p = seek + 1
395 let zv: i64 = mr_p_parse_q14(body, p, arr_end)
396 if zv == MR_P_Q14_NOT_FOUND { return 0 }
397 seek = p
398 var found_c3: i64 = 0
399 while found_c3 == 0 {
400 if seek >= arr_end { found_c3 = 1 }
401 else {
402 if body[seek] == 0x2C { found_c3 = 1 }
403 else { seek = seek + 1 }
404 }
405 }
406 if seek >= arr_end { return 0 }
407 p = seek + 1
408 let ev2: i64 = mr_p_parse_q14(body, p, arr_end)
409 if ev2 == MR_P_Q14_NOT_FOUND { return 0 }
410
411 *out_x_box = xv
412 *out_y_box = yv
413 *out_z_box = zv
414 *out_e_box = ev2
415 return 1
416}
417
418// Public entry: parse the motion_report section into the supplied
419// Q14 output boxes. Returns NX_MR_PARSE_OK on success.
420//
421// Outputs are NOT written if the motion_report is absent or
422// malformed; caller may pre-init the boxes to sentinel values to
423// detect this.
424func nx_moonraker_parse_motion(
425 body: *u8, body_len: i64,
426 out_x_q14: *i64, out_y_q14: *i64,
427 out_z_q14: *i64, out_e_q14: *i64,
428 out_velocity_q14: *i64
429) -> i64 {
430 if (body as i64) == 0 { return NX_MR_PARSE_BAD_INPUT }
431 if body_len <= 0 { return NX_MR_PARSE_BAD_INPUT }
432
433 let mr_key: i64 = mr_p_find(body, body_len, 0, "\"motion_report\":{")
434 if mr_key < 0 { return NX_MR_PARSE_NO_OBJECT }
435
436 let mr_brace: i64 = mr_key + 16 // strlen("\"motion_report\":") = 16
437 let mr_end: i64 = mr_p_match_brace(body, body_len, mr_brace)
438 if mr_end < mr_brace { return NX_MR_PARSE_NO_OBJECT }
439
440 // live_position is the primary source for x/y/z/e
441 let lp_ok: i64 = mr_p_extract_live_position(
442 body, body_len,
443 mr_brace + 1, mr_end,
444 out_x_q14, out_y_q14, out_z_q14, out_e_q14)
445
446 // live_extruder_position overrides e if present
447 let lep: i64 = mr_p_extract_q14_in_scope(
448 body, body_len, mr_brace + 1, mr_end, "\"live_extruder_position\":")
449 if lep != MR_P_Q14_NOT_FOUND { *out_e_q14 = lep }
450
451 // velocity
452 let v: i64 = mr_p_extract_q14_in_scope(
453 body, body_len, mr_brace + 1, mr_end, "\"live_velocity\":")
454 if v != MR_P_Q14_NOT_FOUND { *out_velocity_q14 = v }
455
456 if lp_ok == 0 {
457 if lep == MR_P_Q14_NOT_FOUND { return NX_MR_PARSE_NO_OBJECT }
458 }
459 return NX_MR_PARSE_OK
460}