code wiki / shims / nx_obd2_shim.nx

nx_obd2_shim.nx source

↩ module page · 423 lines · 18575 B

1// nx_obd2_shim.nx -- OBD-II protocol shim. 2// 3// WHEELER-IMPLEMENTATION-OF-CANONICAL-SPEC. 4// Wire-spec sources (re-implemented clean-room from published specs; 5// no external code imported): 6// - ISO 15765-2:2016 -- ISO-TP network layer over CAN 7// - ISO 14229-1:2020 -- Unified Diagnostic Services (UDS) 8// - ISO 15031-5 -- Emissions-related diagnostic services 9// - SAE J1979 -- PID definitions for Mode 01-09 10// - ISO 15031-6 -- DTC numbering / Mode 03 format 11// 12// First implementation of the INTEROPERABILITY_CHARTER.md 13// (nishi-silicon, commit 534e11d) shim layer pattern. Per the 14// charter §6 NxProtocolShim API surface contract. 15// 16// Status: SEED v0.1.0. 2026-05-26. 17// WINNER-TIER: BASELINE-C provisional (no measured incumbent 18// comparison yet; provisional pending paired-bench run 19// vs python-obd / obdlib). 20// INCUMBENTS: python-obd v0.7.x, obdlib v0.10.x, ELM327-compatible 21// AT-command tools, Vector CANalyzer (commercial) 22// PLAN: M-next: paired bench (mode 01 PID round-trip latency 23// + DTC read throughput) on CAN logger hardware vs the 24// named incumbents above. Re-rate after measurement. 25// GAP TODAY: unmeasured; provisional rating per BENCH_WINNER_AUDIT pattern. 26// 27// V1 SCOPE: 28// - ISO-TP single-frame parse (data length <= 7 bytes; the common 29// case for mode 01 + mode 03) 30// - Mode 01 (current data) for the 12 PIDs the operator's 31// predictive-maintenance use case needs (RPM, speed, temps, 32// fuel-trim, MAP, intake-temp, MAF, throttle, fuel-level, 33// voltage) 34// - Mode 03 (read stored DTCs); 5-byte DTC format 35// - Bring-the-most-out-of-it value-add hook: emits one 36// NxProtocolEvent per parsed PID so the substrate's predictive- 37// maintenance consumer (future commit) can do cross-PID 38// anomaly aggregation 39// 40// V2+ SCOPE (TODO): 41// - ISO-TP multi-frame (mode 09 vehicle info often > 7 bytes) 42// - Mode 02 freeze-frame 43// - Mode 04 clear DTCs 44// - Mode 06 on-board test results 45// - Mode 07 pending DTCs 46// - Mode 09 vehicle info 47// - Mode 0A permanent DTCs 48// - DoIP (Diagnostics over IP) per ISO 13400 49// - J1939 (heavy-duty) variant 50 51import "nx_syscalls.nx" 52 53// ===== Cross-shim verdict codes (mirror INTEROPERABILITY_CHARTER §9) ================================================= 54const NX_SHIM_OK: i64 = 0 55const NX_SHIM_BAD_FRAME: i64 = 1 56const NX_SHIM_PROTOCOL_VIOLATION: i64 = 2 57const NX_SHIM_TIMEOUT: i64 = 3 58const NX_SHIM_BACKPRESSURE: i64 = 4 59const NX_SHIM_UNSUPPORTED_PID: i64 = 5 60const NX_SHIM_NO_WIRE: i64 = 6 61 62// ===== OBD-II-specific verdict codes (extend cross-shim) ================================================= 63const NX_OBD2_VERDICT_BASE: i64 = 100 64const NX_OBD2_BAD_ISOTP_TYPE: i64 = 100 // bits 7-4 of byte0 not in {0,1,2,3} 65const NX_OBD2_MULTIFRAME_NOT_IMPL: i64 = 101 // V1 single-frame only 66const NX_OBD2_BAD_MODE: i64 = 102 // mode byte not in 0x01-0x0A (or +0x40 response) 67const NX_OBD2_NEGATIVE_RESP: i64 = 103 // 0x7F + mode + NRC; common case ECU rejection 68const NX_OBD2_PID_DATA_TRUNCATED: i64 = 104 // expected N data bytes for PID; got < N 69 70// ===== OBD-II mode codes (sealed enum per ISO 15031-5) ================================================= 71const NX_OBD2_MODE_01: i64 = 0x01 // current data 72const NX_OBD2_MODE_02: i64 = 0x02 // freeze frame 73const NX_OBD2_MODE_03: i64 = 0x03 // read stored DTCs 74const NX_OBD2_MODE_04: i64 = 0x04 // clear DTCs 75const NX_OBD2_MODE_05: i64 = 0x05 // O2 sensor monitoring (legacy) 76const NX_OBD2_MODE_06: i64 = 0x06 // on-board test results 77const NX_OBD2_MODE_07: i64 = 0x07 // pending DTCs 78const NX_OBD2_MODE_08: i64 = 0x08 // control operation of on-board systems 79const NX_OBD2_MODE_09: i64 = 0x09 // vehicle info 80const NX_OBD2_MODE_0A: i64 = 0x0A // permanent DTCs 81const NX_OBD2_RESP_OFFSET: i64 = 0x40 // ECU response = request mode + 0x40 82 83// ===== Common Mode 01 PIDs the operator's predictive-maintenance needs ================================================= 84const NX_OBD2_PID_MONITOR_STATUS: i64 = 0x01 // bytes: monitor status since DTCs cleared 85const NX_OBD2_PID_FUEL_SYSTEM_STAT: i64 = 0x03 // fuel system status (2 bytes) 86const NX_OBD2_PID_ENGINE_LOAD: i64 = 0x04 // calculated engine load (1 byte; %) 87const NX_OBD2_PID_COOLANT_TEMP: i64 = 0x05 // coolant temp (1 byte; signed -40..215 C) 88const NX_OBD2_PID_FUEL_TRIM_S1: i64 = 0x06 // short-term fuel trim bank 1 (%) 89const NX_OBD2_PID_FUEL_TRIM_L1: i64 = 0x07 // long-term fuel trim bank 1 (%) 90const NX_OBD2_PID_FUEL_PRESSURE: i64 = 0x0A // fuel pressure (1 byte; *3 kPa) 91const NX_OBD2_PID_MAP: i64 = 0x0B // manifold absolute pressure (1 byte; kPa) 92const NX_OBD2_PID_RPM: i64 = 0x0C // engine RPM (2 bytes; /4) 93const NX_OBD2_PID_VEHICLE_SPEED: i64 = 0x0D // vehicle speed (1 byte; km/h) 94const NX_OBD2_PID_INTAKE_AIR_TEMP: i64 = 0x0F // intake air temp (1 byte; signed -40..215 C) 95const NX_OBD2_PID_MAF: i64 = 0x10 // mass air flow (2 bytes; /100 g/s) 96const NX_OBD2_PID_THROTTLE_POS: i64 = 0x11 // throttle position (1 byte; %) 97const NX_OBD2_PID_FUEL_LEVEL: i64 = 0x2F // fuel tank level (1 byte; %) 98const NX_OBD2_PID_CONTROL_VOLTAGE: i64 = 0x42 // ECU control voltage (2 bytes; /1000 V) 99 100// ===== Decoded event kinds (Nishi-native; substrate-facing) ================================================= 101// 102// Per INTEROPERABILITY_CHARTER §M2: substrate never sees raw OBD 103// frame structures. These NxProtocolEvent kinds are what flow out. 104const NX_OBD2_EVT_RPM: i64 = 200 // payload: i64 RPM 105const NX_OBD2_EVT_VEHICLE_SPEED: i64 = 201 // payload: i64 km/h 106const NX_OBD2_EVT_COOLANT_TEMP: i64 = 202 // payload: i64 deg C 107const NX_OBD2_EVT_ENGINE_LOAD: i64 = 203 // payload: i64 % * 100 108const NX_OBD2_EVT_FUEL_TRIM_S1: i64 = 204 // payload: i64 % * 100 (signed; -100% to +99.2%) 109const NX_OBD2_EVT_FUEL_TRIM_L1: i64 = 205 110const NX_OBD2_EVT_MAP: i64 = 206 // payload: i64 kPa 111const NX_OBD2_EVT_INTAKE_AIR_TEMP: i64 = 207 // payload: i64 deg C 112const NX_OBD2_EVT_MAF: i64 = 208 // payload: i64 g/s * 100 113const NX_OBD2_EVT_THROTTLE_POS: i64 = 209 // payload: i64 % * 100 114const NX_OBD2_EVT_FUEL_LEVEL: i64 = 210 // payload: i64 % 115const NX_OBD2_EVT_CONTROL_VOLTAGE: i64 = 211 // payload: i64 mV 116const NX_OBD2_EVT_FUEL_PRESSURE: i64 = 212 // payload: i64 kPa 117const NX_OBD2_EVT_DTC: i64 = 220 // payload: i64 encoded DTC (P0117 etc) 118 119// ===== Shim state ================================================= 120 121struct NxObd2Shim { 122 name_buf: *u8 // "OBD-II ISO 15765" 123 wire_spec_id: *u8 // "ISO 15765-2:2016 + ISO 14229-1" 124 rx_byte_count: i64 // diag: total bytes received 125 rx_event_count: i64 // diag: total events emitted 126 rx_error_count: i64 // diag: total frame-parse failures 127 last_verdict: i64 // most recent verdict code 128 valid: i64 129} 130 131func nx_obd2_shim_init(s: *NxObd2Shim) -> i64 { 132 if (s as i64) == 0 { return 0 - NX_SHIM_BAD_FRAME } 133 s.name_buf = "OBD-II ISO 15765" as *u8 134 s.wire_spec_id = "ISO 15765-2:2016 + ISO 14229-1:2020" as *u8 135 s.rx_byte_count = 0 136 s.rx_event_count = 0 137 s.rx_error_count = 0 138 s.last_verdict = NX_SHIM_OK 139 s.valid = 1 140 return NX_SHIM_OK 141} 142 143// ===== ISO-TP single-frame parse ================================================= 144// 145// ISO-TP frame format (per ISO 15765-2): 146// byte 0: high nibble = frame type; low nibble = data length (single frame) 147// Type 0 = single frame (SF); low nibble = data length (1..7) 148// Type 1 = first frame (FF) -- NOT IMPLEMENTED V1 149// Type 2 = consecutive (CF) -- NOT IMPLEMENTED V1 150// Type 3 = flow control (FC) -- NOT IMPLEMENTED V1 151// bytes 1..N: payload 152// 153// Returns the data length (1..7) on success, or 0 - verdict on failure. 154 155func nx_obd2_isotp_parse_sf(frame: *u8, frame_len: i64) -> i64 { 156 if frame_len < 1 { return 0 - NX_SHIM_BAD_FRAME } 157 let type_nibble: i64 = (frame[0] as i64) >> 4 158 let len_nibble: i64 = (frame[0] as i64) & 0x0f 159 160 if type_nibble != 0 { 161 if type_nibble == 1 { return 0 - NX_OBD2_MULTIFRAME_NOT_IMPL } 162 if type_nibble == 2 { return 0 - NX_OBD2_MULTIFRAME_NOT_IMPL } 163 if type_nibble == 3 { return 0 - NX_OBD2_MULTIFRAME_NOT_IMPL } 164 return 0 - NX_OBD2_BAD_ISOTP_TYPE 165 } 166 if len_nibble < 1 { return 0 - NX_SHIM_BAD_FRAME } 167 if len_nibble > 7 { return 0 - NX_SHIM_BAD_FRAME } 168 if frame_len < (1 + len_nibble) { return 0 - NX_SHIM_BAD_FRAME } 169 return len_nibble 170} 171 172// ===== Mode 01 PID -> NxProtocolEvent translation ================================================= 173// 174// Per SAE J1979 Mode 01 PID scaling tables. Each PID has its own 175// byte-count + transform from raw bytes to engineering units. 176// 177// Returned (event_kind, value) via out-params; verdict via return. 178// Substrate consumer receives the EVENT, never the raw bytes (per 179// charter §M2 encapsulation). 180 181func nx_obd2_pid_to_event(pid: i64, data: *u8, data_len: i64, 182 out_evt_kind: *i64, out_value: *i64) -> i64 { 183 if (out_evt_kind as i64) == 0 { return 0 - NX_SHIM_BAD_FRAME } 184 if (out_value as i64) == 0 { return 0 - NX_SHIM_BAD_FRAME } 185 186 if pid == NX_OBD2_PID_RPM { 187 // RPM = ((A * 256) + B) / 4 188 if data_len < 2 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 189 let a: i64 = data[0] as i64 190 let b: i64 = data[1] as i64 191 out_evt_kind[0] = NX_OBD2_EVT_RPM 192 out_value[0] = ((a << 8) | b) >> 2 193 return NX_SHIM_OK 194 } 195 if pid == NX_OBD2_PID_VEHICLE_SPEED { 196 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 197 out_evt_kind[0] = NX_OBD2_EVT_VEHICLE_SPEED 198 out_value[0] = data[0] as i64 199 return NX_SHIM_OK 200 } 201 if pid == NX_OBD2_PID_COOLANT_TEMP { 202 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 203 out_evt_kind[0] = NX_OBD2_EVT_COOLANT_TEMP 204 out_value[0] = (data[0] as i64) - 40 205 return NX_SHIM_OK 206 } 207 if pid == NX_OBD2_PID_ENGINE_LOAD { 208 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 209 // load = A * 100 / 255 % ; stored as percent * 100 for resolution 210 let a: i64 = data[0] as i64 211 out_evt_kind[0] = NX_OBD2_EVT_ENGINE_LOAD 212 out_value[0] = (a * 10000) / 255 213 return NX_SHIM_OK 214 } 215 if pid == NX_OBD2_PID_FUEL_TRIM_S1 { 216 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 217 // trim = (A - 128) * 100 / 128 % ; stored as percent * 100 218 let a: i64 = data[0] as i64 219 out_evt_kind[0] = NX_OBD2_EVT_FUEL_TRIM_S1 220 out_value[0] = ((a - 128) * 10000) / 128 221 return NX_SHIM_OK 222 } 223 if pid == NX_OBD2_PID_FUEL_TRIM_L1 { 224 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 225 let a: i64 = data[0] as i64 226 out_evt_kind[0] = NX_OBD2_EVT_FUEL_TRIM_L1 227 out_value[0] = ((a - 128) * 10000) / 128 228 return NX_SHIM_OK 229 } 230 if pid == NX_OBD2_PID_MAP { 231 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 232 out_evt_kind[0] = NX_OBD2_EVT_MAP 233 out_value[0] = data[0] as i64 234 return NX_SHIM_OK 235 } 236 if pid == NX_OBD2_PID_INTAKE_AIR_TEMP { 237 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 238 out_evt_kind[0] = NX_OBD2_EVT_INTAKE_AIR_TEMP 239 out_value[0] = (data[0] as i64) - 40 240 return NX_SHIM_OK 241 } 242 if pid == NX_OBD2_PID_MAF { 243 // MAF = ((A * 256) + B) / 100 g/s; stored as g/s * 100 244 if data_len < 2 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 245 let a: i64 = data[0] as i64 246 let b: i64 = data[1] as i64 247 out_evt_kind[0] = NX_OBD2_EVT_MAF 248 out_value[0] = (a << 8) | b 249 return NX_SHIM_OK 250 } 251 if pid == NX_OBD2_PID_THROTTLE_POS { 252 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 253 let a: i64 = data[0] as i64 254 out_evt_kind[0] = NX_OBD2_EVT_THROTTLE_POS 255 out_value[0] = (a * 10000) / 255 256 return NX_SHIM_OK 257 } 258 if pid == NX_OBD2_PID_FUEL_LEVEL { 259 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 260 let a: i64 = data[0] as i64 261 out_evt_kind[0] = NX_OBD2_EVT_FUEL_LEVEL 262 out_value[0] = (a * 100) / 255 263 return NX_SHIM_OK 264 } 265 if pid == NX_OBD2_PID_CONTROL_VOLTAGE { 266 // voltage = ((A * 256) + B) / 1000 V; stored as mV 267 if data_len < 2 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 268 let a: i64 = data[0] as i64 269 let b: i64 = data[1] as i64 270 out_evt_kind[0] = NX_OBD2_EVT_CONTROL_VOLTAGE 271 out_value[0] = (a << 8) | b 272 return NX_SHIM_OK 273 } 274 if pid == NX_OBD2_PID_FUEL_PRESSURE { 275 if data_len < 1 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 276 let a: i64 = data[0] as i64 277 out_evt_kind[0] = NX_OBD2_EVT_FUEL_PRESSURE 278 out_value[0] = a * 3 279 return NX_SHIM_OK 280 } 281 return 0 - NX_SHIM_UNSUPPORTED_PID 282} 283 284// ===== Mode 03 DTC decode ================================================= 285// 286// Per ISO 15031-6: DTCs are 2 bytes per code, encoded: 287// high 2 bits of byte0: code letter 288// 00 = P (powertrain), 01 = C (chassis), 10 = B (body), 11 = U (network) 289// low 6 bits of byte0 + 8 bits of byte1: 14-bit hex code 290// 291// E.g., P0117 = 0x0117 with prefix "P" -> bytes 0x01, 0x17. 292// 293// Each DTC is returned as a single i64 encoding for substrate 294// processing convenience: 295// bits 16-17: letter (0=P, 1=C, 2=B, 3=U) 296// bits 0-15: hex code 297 298func nx_obd2_mode03_decode_dtc(byte0: i64, byte1: i64) -> i64 { 299 let letter: i64 = (byte0 >> 6) & 0x03 300 let code: i64 = ((byte0 & 0x3f) << 8) | (byte1 & 0xff) 301 return (letter << 16) | code 302} 303 304// ===== Top-level rx_fn ================================================= 305// 306// Per INTEROPERABILITY_CHARTER §6 NxProtocolShim.rx_fn semantics: 307// consumes raw bytes from the wire; emits zero or more 308// NxProtocolEvents. V1: synchronously parses one frame at a time; 309// future commit adds a streaming variant. 310// 311// Frame shape after ISO-TP single-frame parse: 312// payload[0]: mode (or mode + 0x40 for response) 313// payload[1]: PID (for mode 01/02) OR DTC count (for mode 03) 314// payload[2..]: data 315// 316// Returns event count emitted (>=0) or negated verdict on failure. 317 318func nx_obd2_rx_fn(s: *NxObd2Shim, frame: *u8, frame_len: i64, 319 out_evt_kinds: *i64, out_values: *i64, cap: i64) -> i64 { 320 if s.valid != 1 { return 0 - NX_SHIM_NO_WIRE } 321 s.rx_byte_count = s.rx_byte_count + frame_len 322 323 let data_len: i64 = nx_obd2_isotp_parse_sf(frame, frame_len) 324 if data_len < 0 { 325 s.rx_error_count = s.rx_error_count + 1 326 s.last_verdict = 0 - data_len 327 return data_len 328 } 329 330 // payload starts at byte 1. 331 let mode_byte: i64 = frame[1] as i64 332 333 // Detect negative response (0x7F). 334 if mode_byte == 0x7F { 335 s.rx_error_count = s.rx_error_count + 1 336 s.last_verdict = NX_OBD2_NEGATIVE_RESP 337 return 0 - NX_OBD2_NEGATIVE_RESP 338 } 339 340 // Strip response offset. 341 var mode: i64 = mode_byte 342 if mode >= NX_OBD2_RESP_OFFSET { 343 mode = mode - NX_OBD2_RESP_OFFSET 344 } 345 346 if mode == NX_OBD2_MODE_01 { 347 if data_len < 2 { return 0 - NX_OBD2_PID_DATA_TRUNCATED } 348 let pid: i64 = frame[2] as i64 349 let pid_data: *u8 = (frame as i64 + 3) as *u8 350 let pid_data_len: i64 = data_len - 2 351 if cap < 1 { return 0 - NX_SHIM_BACKPRESSURE } 352 let evt_kind_buf: *i64 = (sys_mmap(8)) as *i64 353 let value_buf: *i64 = (sys_mmap(8)) as *i64 354 let v: i64 = nx_obd2_pid_to_event(pid, pid_data, pid_data_len, 355 evt_kind_buf, value_buf) 356 if v != NX_SHIM_OK { return v } 357 out_evt_kinds[0] = evt_kind_buf[0] 358 out_values[0] = value_buf[0] 359 s.rx_event_count = s.rx_event_count + 1 360 return 1 361 } 362 363 if mode == NX_OBD2_MODE_03 { 364 // Mode 03 response: byte[1] = number of DTCs * 1 (some ECUs) 365 // or DTCs immediately follow (CAN-format). V1 assumes 366 // CAN-format: 2 bytes per DTC, packed. 367 let n_dtc: i64 = (data_len - 1) / 2 368 if n_dtc * 2 > cap { return 0 - NX_SHIM_BACKPRESSURE } 369 var i: i64 = 0 370 var emitted: i64 = 0 371 while i < n_dtc { 372 let byte0: i64 = frame[2 + i * 2] as i64 373 let byte1: i64 = frame[2 + i * 2 + 1] as i64 374 if emitted >= cap { return emitted } 375 out_evt_kinds[emitted] = NX_OBD2_EVT_DTC 376 out_values[emitted] = nx_obd2_mode03_decode_dtc(byte0, byte1) 377 emitted = emitted + 1 378 i = i + 1 379 } 380 s.rx_event_count = s.rx_event_count + emitted 381 return emitted 382 } 383 384 return 0 - NX_OBD2_BAD_MODE 385} 386 387// ===== tx_fn: emit a Mode 01 PID request ================================================= 388// 389// Build a single-frame ISO-TP request for Mode 01 + PID. Caller 390// passes a buffer of at least 8 bytes; returns bytes written. 391 392func nx_obd2_tx_fn_mode01_pid(s: *NxObd2Shim, pid: i64, 393 out_frame: *u8, out_cap: i64) -> i64 { 394 if s.valid != 1 { return 0 - NX_SHIM_NO_WIRE } 395 if out_cap < 8 { return 0 - NX_SHIM_BACKPRESSURE } 396 // Single-frame, 2 data bytes (mode + PID). 397 out_frame[0] = 0x02 as u8 // SF + len=2 398 out_frame[1] = NX_OBD2_MODE_01 as u8 399 out_frame[2] = (pid & 0xff) as u8 400 // CAN frames are 8 bytes; pad with 0x55 (ISO-TP convention). 401 var i: i64 = 3 402 while i < 8 { 403 out_frame[i] = 0x55 as u8 404 i = i + 1 405 } 406 return 8 407} 408 409// ===== diag_fn: emit shim health metrics ================================================= 410// 411// Per INTEROPERABILITY_CHARTER §6 diag_fn semantics: reports 412// shim-internal metrics for the substrate's telemetry bus. 413// V1 writes a 4-i64 vector: (rx_byte_count, rx_event_count, 414// rx_error_count, last_verdict). 415 416func nx_obd2_diag_fn(s: *NxObd2Shim, out_vec: *i64) -> i64 { 417 if s.valid != 1 { return 0 - NX_SHIM_NO_WIRE } 418 out_vec[0] = s.rx_byte_count 419 out_vec[1] = s.rx_event_count 420 out_vec[2] = s.rx_error_count 421 out_vec[3] = s.last_verdict 422 return NX_SHIM_OK 423}