nx_iot_capability.nx source
↩ module page · 198 lines · 7106 B
1// nx_iot_capability.nx -- per-device capability model for the hub UI.
2//
3// Roadmap: NISHI_IOT_HUB_ROADMAP.md Epoch 6 (CALIBRATE) + the
4// nishifamily.com/iot control-panel substrate. Operator ask
5// (2026-06-18): manage devices "with the ability to change the lights
6// on what capabilities they have like color and light intensity."
7//
8// The triad: nx_iot_anchor = WHO (stable identity), the vendor driver
9// = HOW (wire protocol), this = WHAT-IT-CAN-DO. The /iot UI reads a
10// device's capability set and renders exactly the right controls -- a
11// toggle for a switch, a slider for a dimmer, a colour wheel + temp
12// slider for a colour bulb. Data-driven (CLAUDE.md #11): the UI never
13// hardcodes per-device controls; it asks this model.
14//
15// Capabilities are bit flags so a device advertises a SET. Device
16// classes map to default capability sets; per-vendor quirks refine.
17//
18// Pure caller-buffer, no syscalls, no imports -> portable + sovereign.
19//
20// license_tier: ORIGINAL
21//
22// nx_capability_claims:
23// needs: [pointer_arithmetic, bit_ops]
24// provides: [capability_flags, class_to_caps, caps_has,
25// caps_clamp_level, caps_emit_json]
26// safety: [no_unchecked_deref, no_floating_point, no_syscall,
27// bounded_iteration, data_driven]
28// verdict: [sealed_enum_class, no_silent_failure]
29// license: ORIGINAL
30// kind: iot_runtime_primitive
31// sss: [S6, S7]
32
33// ---- Capability bit flags -----------------------------------------
34
35const NX_IOT_CAP_ONOFF: i64 = 1 // bit 0 -> render a toggle
36const NX_IOT_CAP_BRIGHTNESS: i64 = 2 // bit 1 -> render a 0-100 slider
37const NX_IOT_CAP_COLOR: i64 = 4 // bit 2 -> render a hue/sat picker
38const NX_IOT_CAP_COLOR_TEMP: i64 = 8 // bit 3 -> render a Kelvin slider
39const NX_IOT_CAP_SCENE: i64 = 16 // bit 4 -> render a scene list
40const NX_IOT_CAP_ENERGY: i64 = 32 // bit 5 -> render energy readout
41
42// ---- Sealed enum: device class ------------------------------------
43
44const NX_IOT_CLASS_UNKNOWN: i64 = 0
45const NX_IOT_CLASS_SWITCH: i64 = 1
46const NX_IOT_CLASS_PLUG: i64 = 2
47const NX_IOT_CLASS_PLUG_ENERGY: i64 = 3
48const NX_IOT_CLASS_DIMMER: i64 = 4
49const NX_IOT_CLASS_BULB_WHITE: i64 = 5
50const NX_IOT_CLASS_BULB_TUNABLE: i64 = 6
51const NX_IOT_CLASS_BULB_COLOR: i64 = 7
52const NX_IOT_CLASS_N: i64 = 8
53
54func nx_iot_class_is_valid(c: i64) -> i64 {
55 if c < 0 { return 0 }
56 if c >= NX_IOT_CLASS_N { return 0 }
57 return 1
58}
59
60// ---- Control ranges (data, not magic numbers) ---------------------
61
62const NX_IOT_BRIGHTNESS_MIN: i64 = 0
63const NX_IOT_BRIGHTNESS_MAX: i64 = 100
64const NX_IOT_COLOR_TEMP_MIN: i64 = 2700 // warm white, Kelvin
65const NX_IOT_COLOR_TEMP_MAX: i64 = 6500 // cool daylight, Kelvin
66
67// ---- local string helper ------------------------------------------
68
69func nx_iot_cap_puts_z(out: *u8, off: i64, s: *u8) -> i64 {
70 var i: i64 = 0
71 var keep: i64 = 1
72 while keep == 1 {
73 let ch: u8 = s[i]
74 if ch == (0 as u8) { keep = 0 }
75 else {
76 out[off + i] = ch
77 i = i + 1
78 }
79 }
80 return off + i
81}
82
83// ---- class -> default capability set ------------------------------
84
85func nx_iot_cap_for_class(vendor: i64, dev_class: i64) -> i64 {
86 if dev_class == NX_IOT_CLASS_SWITCH { return NX_IOT_CAP_ONOFF }
87 if dev_class == NX_IOT_CLASS_PLUG { return NX_IOT_CAP_ONOFF }
88 if dev_class == NX_IOT_CLASS_PLUG_ENERGY {
89 return NX_IOT_CAP_ONOFF | NX_IOT_CAP_ENERGY
90 }
91 if dev_class == NX_IOT_CLASS_DIMMER {
92 return NX_IOT_CAP_ONOFF | NX_IOT_CAP_BRIGHTNESS
93 }
94 if dev_class == NX_IOT_CLASS_BULB_WHITE {
95 return NX_IOT_CAP_ONOFF | NX_IOT_CAP_BRIGHTNESS
96 }
97 if dev_class == NX_IOT_CLASS_BULB_TUNABLE {
98 return NX_IOT_CAP_ONOFF | NX_IOT_CAP_BRIGHTNESS | NX_IOT_CAP_COLOR_TEMP
99 }
100 if dev_class == NX_IOT_CLASS_BULB_COLOR {
101 return NX_IOT_CAP_ONOFF | NX_IOT_CAP_BRIGHTNESS | NX_IOT_CAP_COLOR | NX_IOT_CAP_COLOR_TEMP | NX_IOT_CAP_SCENE
102 }
103 return 0
104}
105
106// ---- queries ------------------------------------------------------
107
108func nx_iot_cap_has(caps: i64, flag: i64) -> i64 {
109 if (caps & flag) != 0 { return 1 }
110 return 0
111}
112
113// Clamp a brightness/level request to the supported range; if the
114// device has no brightness capability, returns -1.
115func nx_iot_cap_clamp_brightness(caps: i64, v: i64) -> i64 {
116 if nx_iot_cap_has(caps, NX_IOT_CAP_BRIGHTNESS) == 0 { return -1 }
117 if v < NX_IOT_BRIGHTNESS_MIN { return NX_IOT_BRIGHTNESS_MIN }
118 if v > NX_IOT_BRIGHTNESS_MAX { return NX_IOT_BRIGHTNESS_MAX }
119 return v
120}
121
122func nx_iot_cap_clamp_color_temp(caps: i64, k: i64) -> i64 {
123 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR_TEMP) == 0 { return -1 }
124 if k < NX_IOT_COLOR_TEMP_MIN { return NX_IOT_COLOR_TEMP_MIN }
125 if k > NX_IOT_COLOR_TEMP_MAX { return NX_IOT_COLOR_TEMP_MAX }
126 return k
127}
128
129// ---- emit capability descriptor for the UI ------------------------
130//
131// Writes a JSON array of the supported capability names, e.g.
132// ["on_off","brightness","color","color_temp","scene"]
133// The /iot front-end maps each name to a control widget. Returns the
134// byte length, or -1 if the buffer is too small.
135
136func nx_iot_cap_emit_json(caps: i64, out: *u8, out_cap: i64) -> i64 {
137 if out_cap < 96 { return -1 }
138 var o: i64 = 0
139 out[o] = 0x5B as u8 // [
140 o = o + 1
141 var first: i64 = 1
142 if nx_iot_cap_has(caps, NX_IOT_CAP_ONOFF) == 1 {
143 out[o] = 0x22 as u8
144 o = o + 1
145 o = nx_iot_cap_puts_z(out, o, "on_off")
146 out[o] = 0x22 as u8
147 o = o + 1
148 first = 0
149 }
150 if nx_iot_cap_has(caps, NX_IOT_CAP_BRIGHTNESS) == 1 {
151 if first == 0 { out[o] = 0x2C as u8; o = o + 1 }
152 out[o] = 0x22 as u8
153 o = o + 1
154 o = nx_iot_cap_puts_z(out, o, "brightness")
155 out[o] = 0x22 as u8
156 o = o + 1
157 first = 0
158 }
159 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR) == 1 {
160 if first == 0 { out[o] = 0x2C as u8; o = o + 1 }
161 out[o] = 0x22 as u8
162 o = o + 1
163 o = nx_iot_cap_puts_z(out, o, "color")
164 out[o] = 0x22 as u8
165 o = o + 1
166 first = 0
167 }
168 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR_TEMP) == 1 {
169 if first == 0 { out[o] = 0x2C as u8; o = o + 1 }
170 out[o] = 0x22 as u8
171 o = o + 1
172 o = nx_iot_cap_puts_z(out, o, "color_temp")
173 out[o] = 0x22 as u8
174 o = o + 1
175 first = 0
176 }
177 if nx_iot_cap_has(caps, NX_IOT_CAP_SCENE) == 1 {
178 if first == 0 { out[o] = 0x2C as u8; o = o + 1 }
179 out[o] = 0x22 as u8
180 o = o + 1
181 o = nx_iot_cap_puts_z(out, o, "scene")
182 out[o] = 0x22 as u8
183 o = o + 1
184 first = 0
185 }
186 if nx_iot_cap_has(caps, NX_IOT_CAP_ENERGY) == 1 {
187 if first == 0 { out[o] = 0x2C as u8; o = o + 1 }
188 out[o] = 0x22 as u8
189 o = o + 1
190 o = nx_iot_cap_puts_z(out, o, "energy")
191 out[o] = 0x22 as u8
192 o = o + 1
193 first = 0
194 }
195 out[o] = 0x5D as u8 // ]
196 o = o + 1
197 return o
198}