diff --git a/lib/bt-monitor.js b/lib/bt-monitor.js new file mode 100644 index 0000000..685e5b3 --- /dev/null +++ b/lib/bt-monitor.js @@ -0,0 +1,107 @@ +'use strict'; + +const { execSync } = require('child_process'); +const fs = require('fs'); +const log = require('./logger'); +const led = require('./led'); + +const POLL_INTERVAL_MS = 3000; + +function findBin(name, candidates) { + for (const p of candidates) { + try { fs.accessSync(p); return p; } catch (_) {} + } + return name; // fallback: rely on PATH +} + +/** + * 监控蓝牙状态,驱动 BT 指示灯(b6)。 + * + * 状态优先级: + * connected → 常亮 (hcitool con 检测到 ACL 连接) + * scanning → 闪烁 (bluetoothctl show: Discovering: yes) + * connecting → 闪烁 (bluetoothctl devices: 有 Connecting 状态) + * off → 熄灭 (adapter 不存在 / Powered: no / 静止) + */ +class BtMonitor { + constructor() { + this._timer = null; + this._btctl = findBin('bluetoothctl', [ + '/usr/bin/bluetoothctl', '/bin/bluetoothctl', + ]); + this._hcitool = findBin('hcitool', [ + '/usr/bin/hcitool', '/usr/sbin/hcitool', '/bin/hcitool', + ]); + } + + start() { + this._poll(); + this._timer = setInterval(() => this._poll(), POLL_INTERVAL_MS); + log.info('bt', 'BT 状态监控已启动'); + } + + stop() { + if (this._timer) { + clearInterval(this._timer); + this._timer = null; + } + led.bt.off(); + log.info('bt', 'BT 状态监控已停止'); + } + + // ── 内部 ────────────────────────────────────────────────────────────────── + + _poll() { + try { + const state = this._getBtState(); + if (state === 'connected') { + led.bt.on(); + } else if (state === 'scanning' || state === 'connecting') { + led.bt.blink(); + } else { + led.bt.off(); + } + } catch (e) { + log.warn('bt', `状态检测异常: ${e.message}`); + led.bt.off(); + } + } + + _exec(cmd, timeout = 3000) { + return execSync(cmd, { + timeout, + stdio: ['ignore', 'pipe', 'ignore'], + encoding: 'utf8', + }); + } + + _getBtState() { + // 1. 检查 adapter 是否存在且已开启 + let show; + try { + show = this._exec(`${this._btctl} show`); + } catch (_) { + return 'off'; // bluetoothctl 不可用 或 无 adapter + } + if (!show.includes('Powered: yes')) return 'off'; + + // 2. 检查是否有已连接的 ACL 设备(A2DP 连接) + try { + const con = this._exec(`${this._hcitool} con`); + if (/ACL\s+[0-9A-Fa-f:]{17}/i.test(con)) return 'connected'; + } catch (_) {} + + // 3. 部分版本支持 bluetoothctl devices Connected + try { + const devs = this._exec(`${this._btctl} devices Connected`); + if (devs.trim()) return 'connected'; + } catch (_) {} + + // 4. 检查是否正在扫描 + if (show.includes('Discovering: yes')) return 'scanning'; + + return 'off'; + } +} + +module.exports = { BtMonitor }; diff --git a/lib/client.js b/lib/client.js index 63f6ab9..2bd95e9 100644 --- a/lib/client.js +++ b/lib/client.js @@ -8,6 +8,7 @@ const { getBoxId } = require('./fingerprint'); const { collect } = require('./metrics'); const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc'); // getDashboardInfo 也用于心跳中定期刷新 const { ProvisionManager } = require('./provisioning'); +const { BtMonitor } = require('./bt-monitor'); const { hasInternet, getLocalIps } = require('./network'); const led = require('./led'); @@ -32,8 +33,9 @@ class ClawClient { this._hbTimer = null; this._backoff = 1_000; this._stopped = false; - this._frpc = new FrpcManager(); - this._dashInfo = {}; + this._frpc = new FrpcManager(); + this._btMonitor = null; + this._dashInfo = {}; this._hbCount = 0; // 心跳计数器,用于定期刷新 dashboard 信息 this._externalIp = null; // 外网 IP this._location = null; // 地理位置(由 ipplus360 返回,如"北京市-北京市西城区") @@ -83,6 +85,10 @@ class ClawClient { this._startSdNotify(); + // 启动蓝牙状态监控(独立于网络,立即开始) + this._btMonitor = new BtMonitor(); + this._btMonitor.start(); + // 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP) this._provisionMgr = new ProvisionManager(this._cfg.claw_id); this._connectionStarted = false; @@ -167,6 +173,7 @@ class ClawClient { this._clearPing(); this._clearNetMonitor(); if (this._sdTimer) { clearInterval(this._sdTimer); this._sdTimer = null; } + if (this._btMonitor) { this._btMonitor.stop(); this._btMonitor = null; } if (this._provisionMgr) { this._provisionMgr.stop(); this._provisionMgr = null; } this._frpc.stop(); if (this._ws) this._ws.terminate(); diff --git a/lib/provisioning.js b/lib/provisioning.js index fe4bd8d..7aa6e2d 100644 --- a/lib/provisioning.js +++ b/lib/provisioning.js @@ -35,14 +35,12 @@ class ProvisionManager extends EventEmitter { isApMode() { return this._state === 'ap'; } async start() { - led.off(); // WiFi 灯初始状态:熄灭 - led.bt.off(); // BT 灯初始状态:熄灭 + led.off(); // WiFi 灯初始状态:熄灭 // WiFi STA 已连接 → 直接进入 STA 模式 if (isWifiStaConnected()) { this._state = 'sta'; log.info('provision', 'WiFi STA 已连接,AP 不启动'); - led.bt.on(); // 已配网,BT 灯常亮 this._emitNetworkReady(); this._startMonitor(); return; @@ -63,12 +61,10 @@ class ProvisionManager extends EventEmitter { if (hasSavedWifiConnection()) { log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...'); led.blink(); // WiFi 灯:等待自动重连期间闪烁 - // BT 灯:等待 NM 自动重连不属于 BLE 搜索/连接阶段,保持熄灭 const connected = await this._waitForWifiConnect(); if (connected) { this._state = 'sta'; log.info('provision', 'WiFi 自动连接成功,AP 不启动'); - led.bt.on(); // 自动重连成功,BT 灯常亮 this._emitNetworkReady(); this._startMonitor(); return; @@ -118,8 +114,7 @@ class ProvisionManager extends EventEmitter { this._stopMonitor(); this._stopAll(); this._state = 'idle'; - led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器 - led.bt.destroy(); // BT 灯:停止时关灯 + led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器 } // ── 进入 AP 模式 ───────────────────────────────────────────────────────── @@ -127,8 +122,7 @@ class ProvisionManager extends EventEmitter { _enterAP() { if (this._state === 'ap') return; - led.off(); // AP 模式:WiFi 未连接,WiFi 灯熄灭 - led.bt.blink(); // AP 模式:BLE 配网进行中,BT 灯闪烁 + led.off(); // AP 模式:WiFi 未连接,WiFi 灯熄灭 if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP,有线时等 WS 连接后再定 try { @@ -174,14 +168,13 @@ class ProvisionManager extends EventEmitter { if (result.success) { this._state = 'sta'; log.info('provision', `WiFi 已连接: ${ssid}`); - led.on(); // WiFi 灯:连接成功 → 常亮 - led.bt.on(); // BT 灯:配网成功 → 常亮 + led.on(); // WiFi 灯:连接成功 → 常亮 this.emit('network-ready'); return result; } log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`); - this._enterAP(); // _enterAP 内部会调用 led.off() / led.bt.blink() + this._enterAP(); return result; } @@ -203,7 +196,6 @@ class ProvisionManager extends EventEmitter { log.info('provision', 'WiFi 已外部连接,关闭 AP'); this._stopAPServices(); this._state = 'sta'; - led.bt.on(); // 外部连接成功,BT 灯常亮 this.emit('network-ready'); } @@ -211,13 +203,11 @@ class ProvisionManager extends EventEmitter { if (this._state === 'sta') { if (hasInternet()) { led.on(); - led.bt.on(); // 有网 → BT 灯常亮 } else { - led.off(); // WiFi 已连接但无互联网 - led.bt.off(); // 无互联网时 BT 灯也熄灭 + led.off(); // WiFi 已连接但无互联网 } } - // AP 模式下 led/bt 已在 _enterAP() 中设置,无需重复操作 + // AP 模式下 led 已在 _enterAP() 中熄灭,无需重复操作 }, MONITOR_INTERVAL_MS); }