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}