nx_iot_control_page.nx source
↩ module page · 160 lines · 8802 B
1// nx_iot_control_page.nx -- emit a ZERO-JS, CROSS-BROWSER IoT CONTROL page.
2//
3// Operator 2026-06-22: "work via nishi browser and 3rd party like chrome/firefox/edge
4// and simple for other people to use aka reproducible." The cross-browser + reproducible
5// answer is SERVER-RENDERED HTML + CSS with ZERO <script>: controls are real <form> POSTs
6// to the hub's /home/cmd, so they actuate in EVERY browser with nothing to install (the
7// same no-JS doctrine the reader arc proved). Capability-driven (nx_iot_capability): a
8// switch gets On/Off, a colour bulb gets brightness/colour/warmth/scene -- chosen per
9// device, never hardcoded. Library organ: cp_head/cp_card/cp_foot/cp_find + a demo main()
10// (the gate calls the functions; on import the demo main is stripped).
11//
12// NEVER-BRICK #26: the page only emits on/off/brightness/scene COMMANDS (no firmware).
13// license_tier: ORIGINAL
14
15import "nx_syscalls.nx"
16import "nx_iot_capability.nx"
17
18const CP_OUT: *u8 = "web_assets/iot_control.html"
19const CP_CAP: i64 = 262144
20
21func cp_cat(dst: *u8, off: i64, s: *u8) -> i64 {
22 var o: i64 = off
23 var i: i64 = 0
24 while s[i] != 0 as u8 { dst[o] = s[i]; o = o + 1; i = i + 1 }
25 return o
26}
27func cp_catn(dst: *u8, off: i64, v: i64) -> i64 {
28 var o: i64 = off
29 var n: i64 = v
30 if n < 0 { dst[o] = 45 as u8; o = o + 1; n = 0 - n }
31 let tmp: *u8 = sys_mmap(32)
32 var k: i64 = 0
33 if n == 0 { tmp[0] = 48 as u8; k = 1 }
34 while n > 0 { tmp[k] = (48 + (n % 10)) as u8; n = n / 10; k = k + 1 }
35 var j: i64 = k - 1
36 while j >= 0 { dst[o] = tmp[j]; o = o + 1; j = j - 1 }
37 return o
38}
39// substring search (for the gate's cross-browser assertions)
40func cp_find(buf: *u8, len: i64, pat: *u8, pat_len: i64) -> i64 {
41 if pat_len > len { return 0 - 1 }
42 var i: i64 = 0
43 let end: i64 = len - pat_len + 1
44 var hit: i64 = 0 - 1
45 while i < end {
46 if hit == (0 - 1) {
47 var j: i64 = 0
48 var ok: i64 = 1
49 while j < pat_len {
50 if (buf[i + j] as i64) != (pat[j] as i64) { ok = 0; j = pat_len } else { j = j + 1 }
51 }
52 if ok == 1 { hit = i }
53 }
54 i = i + 1
55 }
56 return hit
57}
58
59// One control form that POSTs id+cmd(+value) to the hub. Zero JS -> works in any browser.
60func cp_cmd_open(h: *u8, off: i64, id: *u8) -> i64 {
61 var w: i64 = off
62 w = cp_cat(h, w, "<form class=\"w\" method=\"post\" action=\"/home/cmd\"><input type=\"hidden\" name=\"id\" value=\"" as *u8)
63 w = cp_cat(h, w, id)
64 w = cp_cat(h, w, "\">" as *u8)
65 return w
66}
67
68// Capability-driven control widgets, each a real <form> POST. Never hardcoded.
69func cp_widgets(h: *u8, off: i64, id: *u8, caps: i64, is_on: i64) -> i64 {
70 var w: i64 = off
71 if nx_iot_cap_has(caps, NX_IOT_CAP_ONOFF) == 1 {
72 w = cp_cmd_open(h, w, id)
73 if is_on == 1 { w = cp_cat(h, w, "<button class=\"b act\" name=\"cmd\" value=\"on\">On</button><button class=\"b\" name=\"cmd\" value=\"off\">Off</button>" as *u8) }
74 else { w = cp_cat(h, w, "<button class=\"b\" name=\"cmd\" value=\"on\">On</button><button class=\"b act\" name=\"cmd\" value=\"off\">Off</button>" as *u8) }
75 w = cp_cat(h, w, "</form>" as *u8)
76 }
77 if nx_iot_cap_has(caps, NX_IOT_CAP_BRIGHTNESS) == 1 {
78 w = cp_cmd_open(h, w, id)
79 w = cp_cat(h, w, "<label>Brightness<input type=\"range\" name=\"level\" min=\"0\" max=\"100\" value=\"70\"></label><button class=\"b\" name=\"cmd\" value=\"bri\">Set</button></form>" as *u8)
80 }
81 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR) == 1 {
82 w = cp_cmd_open(h, w, id)
83 w = cp_cat(h, w, "<button class=\"sw\" name=\"cmd\" value=\"c:f85149\" style=\"background:#f85149\"></button><button class=\"sw\" name=\"cmd\" value=\"c:3fb950\" style=\"background:#3fb950\"></button><button class=\"sw\" name=\"cmd\" value=\"c:4cc2ff\" style=\"background:#4cc2ff\"></button></form>" as *u8)
84 }
85 if nx_iot_cap_has(caps, NX_IOT_CAP_COLOR_TEMP) == 1 {
86 w = cp_cmd_open(h, w, id)
87 w = cp_cat(h, w, "<label>Warmth<input type=\"range\" name=\"level\" min=\"" as *u8)
88 w = cp_catn(h, w, NX_IOT_COLOR_TEMP_MIN)
89 w = cp_cat(h, w, "\" max=\"" as *u8)
90 w = cp_catn(h, w, NX_IOT_COLOR_TEMP_MAX)
91 w = cp_cat(h, w, "\" value=\"4000\"></label><button class=\"b\" name=\"cmd\" value=\"ct\">Set</button></form>" as *u8)
92 }
93 if nx_iot_cap_has(caps, NX_IOT_CAP_SCENE) == 1 {
94 w = cp_cmd_open(h, w, id)
95 w = cp_cat(h, w, "<select name=\"level\"><option value=\"relax\">Relax</option><option value=\"bright\">Bright</option></select><button class=\"b\" name=\"cmd\" value=\"scene\">Go</button></form>" as *u8)
96 }
97 return w
98}
99
100// One device control card. is_on shows current state; controls are capability-gated.
101func cp_card(h: *u8, off: i64, id: *u8, name: *u8, room: *u8, vlabel: *u8, dev_class: i64, is_on: i64) -> i64 {
102 var w: i64 = off
103 let caps: i64 = nx_iot_cap_for_class(0, dev_class)
104 w = cp_cat(h, w, "<div class=\"card\"><div class=\"cn\">" as *u8)
105 w = cp_cat(h, w, name)
106 w = cp_cat(h, w, "</div><div class=\"cr\">" as *u8)
107 w = cp_cat(h, w, room)
108 w = cp_cat(h, w, " · " as *u8)
109 w = cp_cat(h, w, vlabel)
110 w = cp_cat(h, w, "</div><div class=\"row\">" as *u8)
111 w = cp_widgets(h, w, id, caps, is_on)
112 w = cp_cat(h, w, "</div></div>\n" as *u8)
113 return w
114}
115
116func cp_head(h: *u8, off: i64) -> i64 {
117 var w: i64 = off
118 w = cp_cat(h, w, "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\n" as *u8)
119 w = cp_cat(h, w, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n" as *u8)
120 w = cp_cat(h, w, "<title>Nishi IoT — Control</title>\n" as *u8)
121 w = cp_cat(h, w, "<style>\n" as *u8)
122 w = cp_cat(h, w, "*{box-sizing:border-box}body{margin:0;background:#0b0f14;color:#e6edf3;font:15px/1.5 system-ui,Segoe UI,Roboto,sans-serif}\n" as *u8)
123 w = cp_cat(h, w, "header,main{max-width:960px;margin:0 auto;padding:16px 20px}h1{font-size:24px;margin:.2em 0}.sub{color:#8aa0b4;font-size:13px}\n" as *u8)
124 w = cp_cat(h, w, ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px}\n" as *u8)
125 w = cp_cat(h, w, ".card{background:#131b24;border:1px solid #243140;border-radius:10px;padding:12px 13px}\n" as *u8)
126 w = cp_cat(h, w, ".cn{font-weight:600}.cr{color:#8aa0b4;font-size:12.5px;margin:.1em 0 .6em}\n" as *u8)
127 w = cp_cat(h, w, ".row{display:flex;flex-wrap:wrap;gap:7px;align-items:center}form.w{display:inline-flex;gap:4px;align-items:center;margin:0}\n" as *u8)
128 w = cp_cat(h, w, ".b{background:#1b2735;color:#e6edf3;border:1px solid #243140;border-radius:6px;padding:5px 10px;font-weight:600;cursor:pointer}\n" as *u8)
129 w = cp_cat(h, w, ".b.act{background:#4cc2ff;color:#04121c;border-color:#4cc2ff}.sw{width:18px;height:18px;border-radius:50%;border:1px solid #0006;cursor:pointer}\n" as *u8)
130 w = cp_cat(h, w, "label{color:#8aa0b4;font-size:12px;display:inline-flex;flex-direction:column}input[type=range]{width:90px}select{background:#0e1620;color:#e6edf3;border:1px solid #243140;border-radius:6px}\n" as *u8)
131 w = cp_cat(h, w, "</style></head><body>\n" as *u8)
132 w = cp_cat(h, w, "<header><h1>Nishi IoT — Control</h1><p class=\"sub\">Sovereign LAN hub · zero JavaScript · works in any browser. Controls POST to the hub; no app, no cloud.</p></header>\n" as *u8)
133 w = cp_cat(h, w, "<main><div class=\"grid\">\n" as *u8)
134 return w
135}
136
137func cp_foot(h: *u8, off: i64) -> i64 {
138 var w: i64 = off
139 w = cp_cat(h, w, "</div></main>\n" as *u8)
140 w = cp_cat(h, w, "<footer style=\"max-width:960px;margin:0 auto;padding:8px 20px 40px;color:#8aa0b4;font-size:12px\">Emitted by <code>nx_iot_control_page</code> · 0 <script> · reproducible: open in Nishi / Chrome / Firefox / Edge</footer>\n" as *u8)
141 w = cp_cat(h, w, "</body></html>\n" as *u8)
142 return w
143}
144
145func main() -> i64 {
146 let h: *u8 = sys_mmap(CP_CAP)
147 var w: i64 = 0
148 w = cp_head(h, w)
149 w = cp_card(h, w, "d8:0d:17:a1:c2:09" as *u8, "Kitchen Switch" as *u8, "Kitchen" as *u8, "Kasa HS210" as *u8, NX_IOT_CLASS_SWITCH, 1)
150 w = cp_card(h, w, "a8:bb:50:11:22:33" as *u8, "Living Room Lamp" as *u8, "Living Room" as *u8, "WiZ colour" as *u8, NX_IOT_CLASS_BULB_COLOR, 0)
151 w = cp_card(h, w, "magichome-aabbcc" as *u8, "Hallway Dimmer" as *u8, "Hallway" as *u8, "dimmer" as *u8, NX_IOT_CLASS_DIMMER, 1)
152 w = cp_foot(h, w)
153 let fd: i64 = sys_openat_wr(CP_OUT, 0x1a4)
154 if fd < 0 { sys_write(2, "FATAL: cannot open web_assets/iot_control.html\n" as *u8, 47); sys_exit(2); return 2 }
155 sys_write(fd, h, w)
156 sys_close(fd)
157 sys_write(1, "[nx_iot_control_page] wrote iot_control.html\n" as *u8, 45)
158 sys_exit(0)
159 return 0
160}