code wiki / (root) / nx_moonraker_telemetry_parse.nx

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}