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}