code wiki / (root) / nx_env_sample.nx

nx_env_sample.nx source

↩ module page · 141 lines · 6104 B

1// nx_env_sample.nx -- environmental sensor sample container. 2// 3// license_tier: PUBLIC_NISHI_SUBSTRATE 4// genealogy_id: govee_h5179_lan_protocol_2024 + 5// sensirion_sht45_datasheet + 6// atlas_scientific_ezo_circuit_protocols + 7// nishi_iot_local_substrate_2026 8// 9// Not to be confused with nx_env.nx (shell envp lookup) -- different 10// concern. This is a single environmental telemetry reading from a 11// physical sensor in the grow space, reservoir, or storage container. 12// 13// Generic enough to live in nishi-core: any project that ingests 14// sensor data (industrial IoT, weather station, biology lab, growroom) 15// uses this same struct shape. 16// 17// ===== Provenance ================================================ 18// 19// Sealed source_kind enum: distinguishes where the reading came from. 20// Drives data-quality verdict downstream -- a manual reading has 21// different trust + frequency than a continuous BLE-logger trace. 22 23// nx_safety_envelope: 24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 25// sil_target: SIL1 26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 27// verdict: NOT_YET_EVALUATED 28 29import "nx_syscalls.nx" 30 31// Source-kind sealed enum. 32const NX_ENV_SOURCE_BLE_LOGGER: i64 = 1 // Govee H5179 / SensorPush HT.w continuous 33const NX_ENV_SOURCE_IOT_RELAY: i64 = 2 // nx_iot_local_grow_relay polling 34const NX_ENV_SOURCE_INLINE_PROBE: i64 = 3 // EC/pH/DO directly wired to controller 35const NX_ENV_SOURCE_MANUAL: i64 = 4 // grower hand-entered reading 36const NX_ENV_SOURCE_LAB_RETURN: i64 = 5 // mail-in lab result (Logan/Ward/UMass) 37 38func nx_env_source_kind_name(s: i64) -> *u8 { 39 if s == NX_ENV_SOURCE_BLE_LOGGER { return "BLE_LOGGER" } 40 if s == NX_ENV_SOURCE_IOT_RELAY { return "IOT_RELAY" } 41 if s == NX_ENV_SOURCE_INLINE_PROBE { return "INLINE_PROBE" } 42 if s == NX_ENV_SOURCE_MANUAL { return "MANUAL" } 43 if s == NX_ENV_SOURCE_LAB_RETURN { return "LAB_RETURN" } 44 return "UNKNOWN" 45} 46 47// Q10 fixed-point convention per nx_q10.nx (1024 = 1.0). 48// Sentinel for unset fields: a value of -2^63 (i64 minimum). 49const NX_ENV_UNSET: i64 = -9223372036854775808 50 51// === EnvSample struct ============================================= 52 53struct EnvSample { 54 container_id: i64, // FK to GrowUnit / StorageContainer / Reservoir 55 ts_unix: i64, // unix epoch seconds (use nx_iso8601 for formatting) 56 source_kind: i64, // one of NX_ENV_SOURCE_* 57 sensor_id: i64, // FK to physical sensor (MAC for BLE, port for inline) 58 // Climate readings (Q10). Each may be NX_ENV_UNSET if not measured. 59 temp_c_q10: i64, 60 rh_pct_q10: i64, 61 co2_ppm_q10: i64, 62 // Reservoir readings (Q10). 63 ph_q10: i64, 64 ec_us_per_cm_q10: i64, 65 do_mg_per_l_q10: i64, 66 // Light + pressure. 67 ppfd_umol_q10: i64, 68 mist_psi_q10: i64, 69 // Data-quality flag. 70 quality_verdict: i64, // see nx_env_quality_* below 71} 72 73const NX_ENV_SAMPLE_BYTES: i64 = 96 // 12 fields * 8 bytes 74 75// === Quality verdict ============================================== 76const NX_ENV_QUALITY_TRUSTED: i64 = 1 77const NX_ENV_QUALITY_FLAGGED_RANGE: i64 = 2 // reading outside plausible range 78const NX_ENV_QUALITY_FLAGGED_STALE: i64 = 3 // older than max-silence threshold 79const NX_ENV_QUALITY_DUPLICATE: i64 = 4 // same reading reported twice 80 81func nx_env_quality_name(v: i64) -> *u8 { 82 if v == NX_ENV_QUALITY_TRUSTED { return "TRUSTED" } 83 if v == NX_ENV_QUALITY_FLAGGED_RANGE { return "FLAGGED_RANGE" } 84 if v == NX_ENV_QUALITY_FLAGGED_STALE { return "FLAGGED_STALE" } 85 if v == NX_ENV_QUALITY_DUPLICATE { return "DUPLICATE" } 86 return "UNKNOWN" 87} 88 89// === Constructor ================================================== 90 91func nx_env_sample_new(container_id: i64, ts_unix: i64, source_kind: i64, sensor_id: i64) -> *EnvSample { 92 let raw: *u8 = sys_mmap(NX_ENV_SAMPLE_BYTES) 93 let s: *EnvSample = raw as *EnvSample 94 s.container_id = container_id 95 s.ts_unix = ts_unix 96 s.source_kind = source_kind 97 s.sensor_id = sensor_id 98 s.temp_c_q10 = NX_ENV_UNSET 99 s.rh_pct_q10 = NX_ENV_UNSET 100 s.co2_ppm_q10 = NX_ENV_UNSET 101 s.ph_q10 = NX_ENV_UNSET 102 s.ec_us_per_cm_q10 = NX_ENV_UNSET 103 s.do_mg_per_l_q10 = NX_ENV_UNSET 104 s.ppfd_umol_q10 = NX_ENV_UNSET 105 s.mist_psi_q10 = NX_ENV_UNSET 106 s.quality_verdict = NX_ENV_QUALITY_TRUSTED 107 return s 108} 109 110// === Range-check primitive (data-quality at boundaries, Cardinal 12) ==== 111// 112// Returns quality verdict given the sample. Plausibility ranges 113// chosen for indoor-grow + seed-storage applications. 114 115const NX_ENV_TEMP_MIN_Q10: i64 = -30720 // -30 C 116const NX_ENV_TEMP_MAX_Q10: i64 = 61440 // 60 C 117const NX_ENV_RH_MIN_Q10: i64 = 0 118const NX_ENV_RH_MAX_Q10: i64 = 102400 // 100% in Q10 119const NX_ENV_PH_MIN_Q10: i64 = 0 120const NX_ENV_PH_MAX_Q10: i64 = 14336 // 14 in Q10 121const NX_ENV_EC_MAX_Q10: i64 = 10240000 // 10000 uS/cm 122 123func nx_env_sample_check_range(s: *EnvSample) -> i64 { 124 if s.temp_c_q10 != NX_ENV_UNSET { 125 if s.temp_c_q10 < NX_ENV_TEMP_MIN_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 126 if s.temp_c_q10 > NX_ENV_TEMP_MAX_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 127 } 128 if s.rh_pct_q10 != NX_ENV_UNSET { 129 if s.rh_pct_q10 < NX_ENV_RH_MIN_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 130 if s.rh_pct_q10 > NX_ENV_RH_MAX_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 131 } 132 if s.ph_q10 != NX_ENV_UNSET { 133 if s.ph_q10 < NX_ENV_PH_MIN_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 134 if s.ph_q10 > NX_ENV_PH_MAX_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 135 } 136 if s.ec_us_per_cm_q10 != NX_ENV_UNSET { 137 if s.ec_us_per_cm_q10 < 0 { return NX_ENV_QUALITY_FLAGGED_RANGE } 138 if s.ec_us_per_cm_q10 > NX_ENV_EC_MAX_Q10 { return NX_ENV_QUALITY_FLAGGED_RANGE } 139 } 140 return NX_ENV_QUALITY_TRUSTED 141}