| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <title>AI-Light BLE Config</title>
- <style>
- *{box-sizing:border-box;margin:0;padding:0}
- body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#f0f2f5;min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px}
- .container{width:100%;max-width:420px}
- h1{font-size:22px;text-align:center;color:#333;margin-bottom:20px}
- h1 small{display:block;font-size:12px;color:#888;font-weight:normal;margin-top:4px}
- .card{background:#fff;border-radius:14px;padding:18px;margin-bottom:14px;box-shadow:0 2px 8px rgba(0,0,0,.06)}
- .card h2{font-size:13px;color:#999;margin-bottom:14px;text-transform:uppercase;letter-spacing:1.5px;font-weight:600}
- label{display:block;font-size:12px;color:#666;margin:10px 0 5px;font-weight:600}
- 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}
- input:focus,select:focus{border-color:#4CAF50;background:#fff}
- .row{display:flex;gap:10px}
- .row>div{flex:1}
- .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}
- .btn:active{transform:scale(.98)}
- .btn-connect{background:#2196F3;color:#fff}
- .btn-connect.connected{background:#f44336}
- .btn-save{background:#4CAF50;color:#fff;margin-top:10px}
- .btn-restart{background:#ff9800;color:#fff;margin-top:10px}
- .btn:disabled{opacity:.5;cursor:not-allowed}
- .status-bar{padding:12px 16px;border-radius:10px;font-size:13px;font-weight:500;margin-bottom:14px;text-align:center}
- .status-bar.disconnected{background:#ffebee;color:#c62828}
- .status-bar.connected{background:#e8f5e9;color:#2e7d32}
- .status-bar.connecting{background:#fff3e0;color:#e65100}
- .badge{display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:700}
- .badge-ok{background:#e8f5e9;color:#2e7d32}
- .badge-err{background:#ffebee;color:#c62828}
- .status-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #f5f5f5}
- .status-row:last-child{border:none}
- .status-label{font-size:13px;color:#999}
- .status-value{font-size:13px;color:#333;font-weight:600}
- .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}
- .hidden{display:none}
- .mode-btns{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
- .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}
- .mode-btn:hover{border-color:#4CAF50;background:#f1f8e9}
- .mode-btn:active{transform:scale(.95)}
- .mode-btn.active{border-color:#4CAF50;background:#4CAF50;color:#fff}
- </style>
- </head>
- <body>
- <div class="container">
- <h1>AI-Light<small>Web Bluetooth Configuration</small></h1>
- <!-- Connection -->
- <div id="conn-bar" class="status-bar disconnected">Not connected</div>
- <button id="btn-connect" class="btn btn-connect" onclick="toggleConnect()">Connect</button>
- <!-- Status (shown after connect) -->
- <div id="sec-status" class="card hidden">
- <h2>Device Status</h2>
- <div class="status-row"><span class="status-label">WiFi</span><span class="status-value" id="s-wifi">-</span></div>
- <div class="status-row"><span class="status-label">MQTT</span><span class="status-value" id="s-mqtt">-</span></div>
- <div class="status-row"><span class="status-label">Mode</span><span class="status-value" id="s-mode">-</span></div>
- <div class="status-row"><span class="status-label">Comm</span><span class="status-value" id="s-comm">-</span></div>
- </div>
- <!-- Light Mode (shown after connect) -->
- <div id="sec-mode" class="card hidden">
- <h2>Light Mode</h2>
- <div class="mode-btns">
- <div class="mode-btn" onclick="setMode('traffic')">Traffic</div>
- <div class="mode-btn" onclick="setMode('thinking')">Thinking</div>
- <div class="mode-btn" onclick="setMode('ai')">AI</div>
- <div class="mode-btn" onclick="setMode('busy')">Busy</div>
- <div class="mode-btn" onclick="setMode('success')">Success</div>
- <div class="mode-btn" onclick="setMode('error')">Error</div>
- <div class="mode-btn" onclick="setMode('alarm')">Alarm</div>
- <div class="mode-btn" onclick="setMode('init')">Init</div>
- <div class="mode-btn" onclick="setMode('off')">Off</div>
- </div>
- </div>
- <!-- WiFi Config -->
- <div id="sec-wifi" class="card hidden">
- <h2>WiFi</h2>
- <label>SSID</label>
- <input id="f-ssid" placeholder="WiFi name">
- <label>Password</label>
- <input id="f-pass" type="password" placeholder="WiFi password">
- </div>
- <!-- MQTT Config -->
- <div id="sec-mqtt" class="card hidden">
- <h2>MQTT</h2>
- <label>Broker</label>
- <input id="f-broker" placeholder="192.168.1.100">
- <div class="row">
- <div><label>Port</label><input id="f-port" type="number" value="1883"></div>
- <div><label>Client ID</label><input id="f-client" value="AI-Light"></div>
- </div>
- <label>Username</label>
- <input id="f-muser" placeholder="(optional)">
- <label>Password</label>
- <input id="f-mpass" type="password" placeholder="(optional)">
- <label>Subscribe Topic</label>
- <input id="f-topic" placeholder="opencode/status">
- <label>Status Topic</label>
- <input id="f-stopic" placeholder="openCodeLight/status">
- </div>
- <!-- Actions (shown after connect) -->
- <div id="sec-actions" class="hidden">
- <button class="btn btn-save" onclick="saveConfig()">Save & Restart</button>
- <button class="btn btn-restart" onclick="restartDevice()">Restart</button>
- </div>
- <!-- Log -->
- <div id="sec-log" class="card hidden">
- <h2>Log</h2>
- <div id="log" class="log"></div>
- </div>
- </div>
- <script>
- const SERVICE_UUID = 'b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001';
- const MODE_UUID = 'b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001';
- const CONFIG_UUID = 'b8b7e003-7a6b-4f4f-9a8b-11c0ffee0001';
- let device = null;
- let modeChar = null;
- let configChar = null;
- let connected = false;
- const $ = id => document.getElementById(id);
- const logEl = $('log');
- function log(msg) {
- const t = new Date().toLocaleTimeString();
- logEl.innerHTML += `<div>[${t}] ${msg}</div>`;
- logEl.scrollTop = logEl.scrollHeight;
- }
- function showSections(show) {
- ['sec-status','sec-mode','sec-wifi','sec-mqtt','sec-actions','sec-log'].forEach(id => {
- $(id).classList.toggle('hidden', !show);
- });
- }
- function setStatus(text, type) {
- const bar = $('conn-bar');
- bar.textContent = text;
- bar.className = 'status-bar ' + type;
- }
- async function toggleConnect() {
- if (connected) {
- if (device && device.gatt.connected) device.gatt.disconnect();
- return;
- }
- try {
- setStatus('Scanning...', 'connecting');
- log('Requesting BLE device...');
- device = await navigator.bluetooth.requestDevice({
- filters: [{ name: 'AI-Light' }],
- optionalServices: [SERVICE_UUID]
- });
- log('Device found: ' + device.name);
- setStatus('Connecting...', 'connecting');
- device.addEventListener('gattserverdisconnected', onDisconnected);
- const server = await device.gatt.connect();
- log('GATT connected');
- const service = await server.getPrimaryService(SERVICE_UUID);
- log('Service found');
- modeChar = await service.getCharacteristic(MODE_UUID);
- configChar = await service.getCharacteristic(CONFIG_UUID);
- log('Characteristics found');
- modeChar.addEventListener('characteristicvaluechanged', onModeChanged);
- await modeChar.startNotifications();
- log('Mode notifications enabled');
- connected = true;
- setStatus('Connected: ' + device.name, 'connected');
- $('btn-connect').textContent = 'Disconnect';
- $('btn-connect').classList.add('connected');
- showSections(true);
- await readConfig();
- await readMode();
- } catch (err) {
- log('Error: ' + err.message);
- setStatus('Connection failed', 'disconnected');
- connected = false;
- showSections(false);
- }
- }
- function onDisconnected() {
- connected = false;
- modeChar = null;
- configChar = null;
- setStatus('Disconnected', 'disconnected');
- $('btn-connect').textContent = 'Connect';
- $('btn-connect').classList.remove('connected');
- showSections(false);
- log('Device disconnected');
- }
- function onModeChanged(event) {
- const decoder = new TextDecoder();
- const mode = decoder.decode(event.target.value).trim();
- $('s-mode').textContent = mode;
- log('Mode: ' + mode);
- updateModeButtons(mode);
- }
- function updateModeButtons(mode) {
- document.querySelectorAll('.mode-btn').forEach(btn => {
- btn.classList.toggle('active', btn.textContent.toLowerCase() === mode);
- });
- }
- async function readMode() {
- try {
- const val = await modeChar.readValue();
- const decoder = new TextDecoder();
- const mode = decoder.decode(val).trim();
- $('s-mode').textContent = mode;
- updateModeButtons(mode);
- log('Current mode: ' + mode);
- } catch (err) {
- log('Read mode error: ' + err.message);
- }
- }
- async function readConfig() {
- try {
- const val = await configChar.readValue();
- const decoder = new TextDecoder();
- const json = decoder.decode(val);
- log('Config loaded');
- const cfg = JSON.parse(json);
- $('f-ssid').value = cfg.wifi_ssid || '';
- $('f-broker').value = cfg.mqtt_broker || '';
- $('f-port').value = cfg.mqtt_port || 1883;
- $('f-client').value = cfg.mqtt_client || 'AI-Light';
- $('f-muser').value = cfg.mqtt_user || '';
- $('f-topic').value = cfg.mqtt_topic || '';
- $('f-stopic').value = cfg.mqtt_status || '';
- $('s-wifi').innerHTML = cfg.wifi_ssid ? '<span class="badge badge-ok">' + cfg.wifi_ssid + '</span>' : '<span class="badge badge-err">Not configured</span>';
- $('s-mqtt').innerHTML = cfg.mqtt_broker ? '<span class="badge badge-ok">' + cfg.mqtt_broker + '</span>' : '<span class="badge badge-err">Not configured</span>';
- $('s-comm').textContent = cfg.comm_mode === 1 ? 'MQTT' : 'BLE-only';
- } catch (err) {
- log('Read config error: ' + err.message);
- }
- }
- async function setMode(mode) {
- if (!modeChar) return;
- try {
- const encoder = new TextEncoder();
- await modeChar.writeValue(encoder.encode(mode));
- log('Set mode: ' + mode);
- } catch (err) {
- log('Set mode error: ' + err.message);
- }
- }
- async function saveConfig() {
- if (!configChar) return;
- const cfg = {
- wifi_ssid: $('f-ssid').value,
- wifi_pass: $('f-pass').value,
- mqtt_broker: $('f-broker').value,
- mqtt_port: parseInt($('f-port').value) || 1883,
- mqtt_user: $('f-muser').value,
- mqtt_pass: $('f-mpass').value,
- mqtt_client: $('f-client').value,
- mqtt_topic: $('f-topic').value,
- mqtt_status: $('f-stopic').value
- };
- try {
- const encoder = new TextEncoder();
- await configChar.writeValue(encoder.encode(JSON.stringify(cfg)));
- log('Config saved! Device will restart...');
- alert('Config saved! Device will restart.');
- } catch (err) {
- log('Save config error: ' + err.message);
- alert('Error: ' + err.message);
- }
- }
- async function restartDevice() {
- if (!confirm('Restart device?')) return;
- if (!configChar) return;
- try {
- const encoder = new TextEncoder();
- await configChar.writeValue(encoder.encode('{"restart":true}'));
- log('Restart command sent');
- } catch (err) {
- log('Restart error: ' + err.message);
- }
- }
- showSections(false);
- </script>
- </body>
- </html>
|