|
|
@@ -1,317 +0,0 @@
|
|
|
-<!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>
|