ble-config.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1">
  6. <title>AI-Light BLE Config</title>
  7. <style>
  8. *{box-sizing:border-box;margin:0;padding:0}
  9. body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#f0f2f5;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px}
  10. .container{width:100%;max-width:420px}
  11. h1{font-size:22px;text-align:center;color:#333;margin-bottom:20px}
  12. h1 small{display:block;font-size:12px;color:#888;font-weight:normal;margin-top:4px}
  13. .card{background:#fff;border-radius:14px;padding:18px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.06)}
  14. .card h2{font-size:13px;color:#999;margin-bottom:14px;text-transform:uppercase;letter-spacing:1.5px;font-weight:600}
  15. label{display:block;font-size:12px;color:#666;margin:10px 0 5px;font-weight:600}
  16. input,select{width:100%;padding:11px 12px;border:1.5px solid #e0e0e0;border-radius:10px;font-size:14px;outline:none;transition:border .2s;background:#fafafa}
  17. input:focus,select:focus{border-color:#4CAF50;background:#fff}
  18. .row{display:flex;gap:10px}
  19. .row>div{flex:1}
  20. .btn{display:block;width:100%;padding:14px;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;transition:all .2s;letter-spacing:.5px}
  21. .btn:active{transform:scale(.98)}
  22. .btn-connect{background:#2196F3;color:#fff}
  23. .btn-connect.connected{background:#f44336}
  24. .btn-save{background:#4CAF50;color:#fff;margin-top:10px}
  25. .btn-restart{background:#ff9800;color:#fff;margin-top:10px}
  26. .btn:disabled{opacity:.5;cursor:not-allowed}
  27. .status-bar{padding:12px 16px;border-radius:10px;font-size:13px;font-weight:500;margin-bottom:14px;text-align:center}
  28. .status-bar.disconnected{background:#ffebee;color:#c62828}
  29. .status-bar.connected{background:#e8f5e9;color:#2e7d32}
  30. .status-bar.connecting{background:#fff3e0;color:#e65100}
  31. .badge{display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:700}
  32. .badge-ok{background:#e8f5e9;color:#2e7d32}
  33. .badge-err{background:#ffebee;color:#c62828}
  34. .status-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #f5f5f5}
  35. .status-row:last-child{border:none}
  36. .status-label{font-size:13px;color:#999}
  37. .status-value{font-size:13px;color:#333;font-weight:600}
  38. .log{background:#263238;color:#a5d6a7;border-radius:10px;padding:12px;font-family:'SF Mono',monospace;font-size:11px;max-height:120px;overflow-y:auto;line-height:1.6;word-break:break-all}
  39. .hidden{display:none}
  40. .mode-btns{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
  41. .mode-btn{padding:10px 4px;border:1.5px solid #e0e0e0;border-radius:10px;background:#fff;font-size:12px;font-weight:600;cursor:pointer;text-align:center;transition:all .15s}
  42. .mode-btn:hover{border-color:#4CAF50;background:#f1f8e9}
  43. .mode-btn:active{transform:scale(.95)}
  44. .mode-btn.active{border-color:#4CAF50;background:#4CAF50;color:#fff}
  45. </style>
  46. </head>
  47. <body>
  48. <div class="container">
  49. <h1>AI-Light<small>Web Bluetooth Configuration</small></h1>
  50. <!-- Connection -->
  51. <div id="conn-bar" class="status-bar disconnected">Not connected</div>
  52. <button id="btn-connect" class="btn btn-connect" onclick="toggleConnect()">Connect</button>
  53. <!-- Status (shown after connect) -->
  54. <div id="sec-status" class="card hidden">
  55. <h2>Device Status</h2>
  56. <div class="status-row"><span class="status-label">WiFi</span><span class="status-value" id="s-wifi">-</span></div>
  57. <div class="status-row"><span class="status-label">MQTT</span><span class="status-value" id="s-mqtt">-</span></div>
  58. <div class="status-row"><span class="status-label">Mode</span><span class="status-value" id="s-mode">-</span></div>
  59. <div class="status-row"><span class="status-label">Comm</span><span class="status-value" id="s-comm">-</span></div>
  60. </div>
  61. <!-- Light Mode (shown after connect) -->
  62. <div id="sec-mode" class="card hidden">
  63. <h2>Light Mode</h2>
  64. <div class="mode-btns">
  65. <div class="mode-btn" onclick="setMode('traffic')">Traffic</div>
  66. <div class="mode-btn" onclick="setMode('thinking')">Thinking</div>
  67. <div class="mode-btn" onclick="setMode('ai')">AI</div>
  68. <div class="mode-btn" onclick="setMode('busy')">Busy</div>
  69. <div class="mode-btn" onclick="setMode('success')">Success</div>
  70. <div class="mode-btn" onclick="setMode('error')">Error</div>
  71. <div class="mode-btn" onclick="setMode('alarm')">Alarm</div>
  72. <div class="mode-btn" onclick="setMode('init')">Init</div>
  73. <div class="mode-btn" onclick="setMode('off')">Off</div>
  74. </div>
  75. </div>
  76. <!-- WiFi Config -->
  77. <div id="sec-wifi" class="card hidden">
  78. <h2>WiFi</h2>
  79. <label>SSID</label>
  80. <input id="f-ssid" placeholder="WiFi name">
  81. <label>Password</label>
  82. <input id="f-pass" type="password" placeholder="WiFi password">
  83. </div>
  84. <!-- MQTT Config -->
  85. <div id="sec-mqtt" class="card hidden">
  86. <h2>MQTT</h2>
  87. <label>Broker</label>
  88. <input id="f-broker" placeholder="192.168.1.100">
  89. <div class="row">
  90. <div><label>Port</label><input id="f-port" type="number" value="1883"></div>
  91. <div><label>Client ID</label><input id="f-client" value="AI-Light"></div>
  92. </div>
  93. <label>Username</label>
  94. <input id="f-muser" placeholder="(optional)">
  95. <label>Password</label>
  96. <input id="f-mpass" type="password" placeholder="(optional)">
  97. <label>Subscribe Topic</label>
  98. <input id="f-topic" placeholder="opencode/status">
  99. <label>Status Topic</label>
  100. <input id="f-stopic" placeholder="openCodeLight/status">
  101. </div>
  102. <!-- Actions (shown after connect) -->
  103. <div id="sec-actions" class="hidden">
  104. <button class="btn btn-save" onclick="saveConfig()">Save & Restart</button>
  105. <button class="btn btn-restart" onclick="restartDevice()">Restart</button>
  106. </div>
  107. <!-- Log -->
  108. <div id="sec-log" class="card hidden">
  109. <h2>Log</h2>
  110. <div id="log" class="log"></div>
  111. </div>
  112. </div>
  113. <script>
  114. const SERVICE_UUID = 'b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001';
  115. const MODE_UUID = 'b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001';
  116. const CONFIG_UUID = 'b8b7e003-7a6b-4f4f-9a8b-11c0ffee0001';
  117. let device = null;
  118. let modeChar = null;
  119. let configChar = null;
  120. let connected = false;
  121. const $ = id => document.getElementById(id);
  122. const logEl = $('log');
  123. function log(msg) {
  124. const t = new Date().toLocaleTimeString();
  125. logEl.innerHTML += `<div>[${t}] ${msg}</div>`;
  126. logEl.scrollTop = logEl.scrollHeight;
  127. }
  128. function showSections(show) {
  129. ['sec-status','sec-mode','sec-wifi','sec-mqtt','sec-actions','sec-log'].forEach(id => {
  130. $(id).classList.toggle('hidden', !show);
  131. });
  132. }
  133. function setStatus(text, type) {
  134. const bar = $('conn-bar');
  135. bar.textContent = text;
  136. bar.className = 'status-bar ' + type;
  137. }
  138. async function toggleConnect() {
  139. if (connected) {
  140. if (device && device.gatt.connected) device.gatt.disconnect();
  141. return;
  142. }
  143. try {
  144. setStatus('Scanning...', 'connecting');
  145. log('Requesting BLE device...');
  146. device = await navigator.bluetooth.requestDevice({
  147. filters: [{ name: 'AI-Light' }],
  148. optionalServices: [SERVICE_UUID]
  149. });
  150. log('Device found: ' + device.name);
  151. setStatus('Connecting...', 'connecting');
  152. device.addEventListener('gattserverdisconnected', onDisconnected);
  153. const server = await device.gatt.connect();
  154. log('GATT connected');
  155. const service = await server.getPrimaryService(SERVICE_UUID);
  156. log('Service found');
  157. modeChar = await service.getCharacteristic(MODE_UUID);
  158. configChar = await service.getCharacteristic(CONFIG_UUID);
  159. log('Characteristics found');
  160. modeChar.addEventListener('characteristicvaluechanged', onModeChanged);
  161. await modeChar.startNotifications();
  162. log('Mode notifications enabled');
  163. connected = true;
  164. setStatus('Connected: ' + device.name, 'connected');
  165. $('btn-connect').textContent = 'Disconnect';
  166. $('btn-connect').classList.add('connected');
  167. showSections(true);
  168. await readConfig();
  169. await readMode();
  170. } catch (err) {
  171. log('Error: ' + err.message);
  172. setStatus('Connection failed', 'disconnected');
  173. connected = false;
  174. showSections(false);
  175. }
  176. }
  177. function onDisconnected() {
  178. connected = false;
  179. modeChar = null;
  180. configChar = null;
  181. setStatus('Disconnected', 'disconnected');
  182. $('btn-connect').textContent = 'Connect';
  183. $('btn-connect').classList.remove('connected');
  184. showSections(false);
  185. log('Device disconnected');
  186. }
  187. function onModeChanged(event) {
  188. const decoder = new TextDecoder();
  189. const mode = decoder.decode(event.target.value).trim();
  190. $('s-mode').textContent = mode;
  191. log('Mode: ' + mode);
  192. updateModeButtons(mode);
  193. }
  194. function updateModeButtons(mode) {
  195. document.querySelectorAll('.mode-btn').forEach(btn => {
  196. btn.classList.toggle('active', btn.textContent.toLowerCase() === mode);
  197. });
  198. }
  199. async function readMode() {
  200. try {
  201. const val = await modeChar.readValue();
  202. const decoder = new TextDecoder();
  203. const mode = decoder.decode(val).trim();
  204. $('s-mode').textContent = mode;
  205. updateModeButtons(mode);
  206. log('Current mode: ' + mode);
  207. } catch (err) {
  208. log('Read mode error: ' + err.message);
  209. }
  210. }
  211. async function readConfig() {
  212. try {
  213. const val = await configChar.readValue();
  214. const decoder = new TextDecoder();
  215. const json = decoder.decode(val);
  216. log('Config loaded');
  217. const cfg = JSON.parse(json);
  218. $('f-ssid').value = cfg.wifi_ssid || '';
  219. $('f-broker').value = cfg.mqtt_broker || '';
  220. $('f-port').value = cfg.mqtt_port || 1883;
  221. $('f-client').value = cfg.mqtt_client || 'AI-Light';
  222. $('f-muser').value = cfg.mqtt_user || '';
  223. $('f-topic').value = cfg.mqtt_topic || '';
  224. $('f-stopic').value = cfg.mqtt_status || '';
  225. $('s-wifi').innerHTML = cfg.wifi_ssid ? '<span class="badge badge-ok">' + cfg.wifi_ssid + '</span>' : '<span class="badge badge-err">Not configured</span>';
  226. $('s-mqtt').innerHTML = cfg.mqtt_broker ? '<span class="badge badge-ok">' + cfg.mqtt_broker + '</span>' : '<span class="badge badge-err">Not configured</span>';
  227. $('s-comm').textContent = cfg.comm_mode === 1 ? 'MQTT' : 'BLE-only';
  228. } catch (err) {
  229. log('Read config error: ' + err.message);
  230. }
  231. }
  232. async function setMode(mode) {
  233. if (!modeChar) return;
  234. try {
  235. const encoder = new TextEncoder();
  236. await modeChar.writeValue(encoder.encode(mode));
  237. log('Set mode: ' + mode);
  238. } catch (err) {
  239. log('Set mode error: ' + err.message);
  240. }
  241. }
  242. async function saveConfig() {
  243. if (!configChar) return;
  244. const cfg = {
  245. wifi_ssid: $('f-ssid').value,
  246. wifi_pass: $('f-pass').value,
  247. mqtt_broker: $('f-broker').value,
  248. mqtt_port: parseInt($('f-port').value) || 1883,
  249. mqtt_user: $('f-muser').value,
  250. mqtt_pass: $('f-mpass').value,
  251. mqtt_client: $('f-client').value,
  252. mqtt_topic: $('f-topic').value,
  253. mqtt_status: $('f-stopic').value
  254. };
  255. try {
  256. const encoder = new TextEncoder();
  257. await configChar.writeValue(encoder.encode(JSON.stringify(cfg)));
  258. log('Config saved! Device will restart...');
  259. alert('Config saved! Device will restart.');
  260. } catch (err) {
  261. log('Save config error: ' + err.message);
  262. alert('Error: ' + err.message);
  263. }
  264. }
  265. async function restartDevice() {
  266. if (!confirm('Restart device?')) return;
  267. if (!configChar) return;
  268. try {
  269. const encoder = new TextEncoder();
  270. await configChar.writeValue(encoder.encode('{"restart":true}'));
  271. log('Restart command sent');
  272. } catch (err) {
  273. log('Restart error: ' + err.message);
  274. }
  275. }
  276. showSections(false);
  277. </script>
  278. </body>
  279. </html>