code wiki / consumers / nx_modbus_pred_maint_pipeline.nx

nx_modbus_pred_maint_pipeline.nx source

↩ module page · 204 lines · 9190 B

1// nx_modbus_pred_maint_pipeline.nx -- Modbus + pred-maint composer. 2// 3// Wires the Modbus shim (commit eee5855b) -> nx_pred_maint_consumer 4// (commit 87dca0a0) -> nx_telemetry bus (commit 469f822f), proving 5// the consumer is PROTOCOL-AGNOSTIC: the same Welford / 3-sigma / 6// drift-extrapolation pipeline that catches OBD coolant-temp 7// anomalies also catches industrial-sensor anomalies via Modbus. 8// 9// This commit is the structural proof that the substrate-side 10// value-add layer (consumer) reuses across the ยง5 INTEROPERABILITY_CHARTER 11// catalog families. Future protocol shims (CAN raw / J1939 / BLE / 12// Modbus extensions / etc) all reuse the SAME consumer with the 13// SAME telemetry bus. 14// 15// Operator use case (W0+W1 tier per WORKLOAD_TARGETS.md): 16// Industrial pressure sensor on a Modbus RTU bus. Sensor reading 17// normally hovers around register-value 1500 (e.g., 15.00 bar via 18// centibar Q2 scaling). Slow drift toward 2500 indicates a 19// diaphragm wear-out failure mode. Pipeline detects the drift 20// 4-7 days before the sensor crosses its DTC-equivalent 21// threshold; operator schedules planned maintenance vs unplanned 22// outage. Same value-add proposition as OBD pred-maint; same 23// substrate code path. 24// 25// Status: SEED v0.1.0. 2026-05-27. 26// WINNER-TIER: WINNER-S CANDIDATE (preserved from pred-maint; 27// this commit doesn't add new claims, it ports the 28// S-CANDIDATE pred-maint claim to the industrial tier) 29// INCUMBENTS: same as pred-maint (no incumbent matches the 30// cross-protocol value-add); within Modbus specifically, 31// vendor SCADA tools (Ignition, Wonderware, ClearSCADA) 32// have trend-tracking but not substrate-integrated 33// prediction 34// PLAN: M-next: paired bench on real Modbus RTU sensor data 35// with known failure-mode corpus 36// EXEMPTION REASON: n/a; provisional pending hardware measurement 37 38import "nx_syscalls.nx" 39import "nx_telemetry.nx" 40import "shims/nx_modbus_shim.nx" 41import "consumers/nx_pred_maint_consumer.nx" 42 43// ===== Sealed verdict surface ================================================= 44// 45// Pipeline-specific verdicts; extends the underlying shim + consumer 46// verdict surfaces transparently (caller distinguishes by code 47// range; pipeline owns 400-409). 48 49const NX_MODPMP_OK: i64 = 0 50const NX_MODPMP_BAD_INPUT: i64 = 400 51const NX_MODPMP_NOT_INITIALIZED: i64 = 401 52const NX_MODPMP_WRONG_UNIT_ID: i64 = 402 // response unit_id != configured 53const NX_MODPMP_NO_REG_EVENT: i64 = 403 // parsed frame had no register-read event 54 55// ===== Pipeline state ================================================= 56// 57// V1: tracks ONE register per pipeline instance (one sensor). For 58// multi-register, V2 grows the tracked-register list + maintains 59// per-request state to match responses to requests. 60 61struct NxModbusPMP { 62 shim: *NxModbusShim 63 consumer: *NxPredMaintConsumer 64 telemetry: *NxTelemetryBus 65 66 tracked_unit_id: i64 // Modbus slave address 67 tracked_reg_addr: i64 // holding-register address 68 tracked_evt_kind: i64 // operator-chosen NxProtocolEvent key 69 // (used as the consumer's ring lookup 70 // key; opaque to the shim) 71 valid: i64 72} 73 74// ===== Init ================================================= 75// 76// Caller supplies pre-initialised shim + consumer + telemetry bus. 77// Pipeline binds them + records the tracked (unit_id, reg_addr). 78 79func nx_modpmp_init(p: *NxModbusPMP, 80 shim: *NxModbusShim, 81 consumer: *NxPredMaintConsumer, 82 telemetry: *NxTelemetryBus, 83 unit_id: i64, reg_addr: i64, evt_kind: i64) -> i64 { 84 if (p as i64) == 0 { return 0 - NX_MODPMP_BAD_INPUT } 85 if (shim as i64) == 0 { return 0 - NX_MODPMP_BAD_INPUT } 86 if (consumer as i64) == 0 { return 0 - NX_MODPMP_BAD_INPUT } 87 p.shim = shim 88 p.consumer = consumer 89 p.telemetry = telemetry 90 p.tracked_unit_id = unit_id 91 p.tracked_reg_addr = reg_addr 92 p.tracked_evt_kind = evt_kind 93 p.valid = 1 94 return NX_MODPMP_OK 95} 96 97// ===== Register: declare the tracked register's thresholds ================================================= 98// 99// Wraps nx_pm_consumer_register so the operator only has to declare 100// "this Modbus register's value drift triggers anomalies between 101// threshold_lo and threshold_hi"; pipeline handles the consumer- 102// register plumbing. 103 104func nx_modpmp_register(p: *NxModbusPMP, 105 times_buf: *i64, values_buf: *i64, 106 threshold_lo: i64, threshold_hi: i64) -> i64 { 107 if p.valid != 1 { return 0 - NX_MODPMP_NOT_INITIALIZED } 108 let rc: i64 = nx_pm_consumer_register(p.consumer, p.tracked_evt_kind, 109 times_buf, values_buf, 110 threshold_lo, threshold_hi) 111 if rc != NX_PM_OK { return rc } 112 // Also wire telemetry if the consumer doesn't yet have a bus. 113 if (p.telemetry as i64) != 0 { 114 nx_pm_consumer_set_telemetry_bus(p.consumer, p.telemetry) 115 } 116 return NX_MODPMP_OK 117} 118 119// ===== Build a poll request (RTU) ================================================= 120// 121// Convenience: builds an FC 03 read-holding-registers request for 122// the pipeline's tracked (unit_id, reg_addr) with quantity=1. 123// Operator sends the resulting bytes over the wire (out of scope). 124 125func nx_modpmp_build_rtu_poll(p: *NxModbusPMP, out: *u8, out_cap: i64) -> i64 { 126 if p.valid != 1 { return 0 - NX_MODPMP_NOT_INITIALIZED } 127 return nx_modbus_rtu_build_read_holding(p.shim, 128 p.tracked_unit_id, 129 p.tracked_reg_addr, 130 1, 131 out, out_cap) 132} 133 134// ===== Build a poll request (TCP) ================================================= 135 136func nx_modpmp_build_tcp_poll(p: *NxModbusPMP, txn_id: i64, 137 out: *u8, out_cap: i64) -> i64 { 138 if p.valid != 1 { return 0 - NX_MODPMP_NOT_INITIALIZED } 139 return nx_modbus_tcp_build_read_holding(p.shim, txn_id, 140 p.tracked_unit_id, 141 p.tracked_reg_addr, 142 1, 143 out, out_cap) 144} 145 146// ===== Process a response frame (RTU) ================================================= 147// 148// Parses the frame via the shim + dispatches the first register-read 149// event to the consumer. Consumer classifies + emits telemetry per 150// its own wiring (commit d3037668). 151 152func nx_modpmp_on_rtu_response(p: *NxModbusPMP, 153 frame: *u8, frame_len: i64, ts_ns: i64) -> i64 { 154 if p.valid != 1 { return 0 - NX_MODPMP_NOT_INITIALIZED } 155 156 let evt_kinds: *i64 = (sys_mmap(8 * 8)) as *i64 157 let evt_values: *i64 = (sys_mmap(8 * 8)) as *i64 158 let evt_units: *i64 = (sys_mmap(8 * 8)) as *i64 159 160 let n_evts: i64 = nx_modbus_rtu_parse(p.shim, frame, frame_len, 161 evt_kinds, evt_values, evt_units, 8) 162 if n_evts < 0 { return n_evts } // shim verdict passes through 163 if n_evts == 0 { return 0 - NX_MODPMP_NO_REG_EVENT } 164 165 // First event is the holding-register read (assumption: V1 polls 166 // one register, so response has one register event). 167 if evt_units[0] != p.tracked_unit_id { return 0 - NX_MODPMP_WRONG_UNIT_ID } 168 if evt_kinds[0] != NX_MODBUS_EVT_HOLDING_REG_READ { 169 if evt_kinds[0] != NX_MODBUS_EVT_INPUT_REG_READ { 170 return 0 - NX_MODPMP_NO_REG_EVENT 171 } 172 } 173 174 let stats: *NxPidStats = (sys_mmap(64)) as *NxPidStats 175 return nx_pm_consumer_on_event(p.consumer, p.tracked_evt_kind, 176 evt_values[0], ts_ns, stats) 177} 178 179// ===== Process a response frame (TCP) ================================================= 180 181func nx_modpmp_on_tcp_response(p: *NxModbusPMP, 182 frame: *u8, frame_len: i64, ts_ns: i64) -> i64 { 183 if p.valid != 1 { return 0 - NX_MODPMP_NOT_INITIALIZED } 184 185 let evt_kinds: *i64 = (sys_mmap(8 * 8)) as *i64 186 let evt_values: *i64 = (sys_mmap(8 * 8)) as *i64 187 let evt_units: *i64 = (sys_mmap(8 * 8)) as *i64 188 189 let n_evts: i64 = nx_modbus_tcp_parse(p.shim, frame, frame_len, 190 evt_kinds, evt_values, evt_units, 8) 191 if n_evts < 0 { return n_evts } 192 if n_evts == 0 { return 0 - NX_MODPMP_NO_REG_EVENT } 193 194 if evt_units[0] != p.tracked_unit_id { return 0 - NX_MODPMP_WRONG_UNIT_ID } 195 if evt_kinds[0] != NX_MODBUS_EVT_HOLDING_REG_READ { 196 if evt_kinds[0] != NX_MODBUS_EVT_INPUT_REG_READ { 197 return 0 - NX_MODPMP_NO_REG_EVENT 198 } 199 } 200 201 let stats: *NxPidStats = (sys_mmap(64)) as *NxPidStats 202 return nx_pm_consumer_on_event(p.consumer, p.tracked_evt_kind, 203 evt_values[0], ts_ns, stats) 204}