feat: BtMonitor 监控 bluetoothctl 状态驱动 BT 指示灯

新增 lib/bt-monitor.js:
- 每 3 秒轮询 bluetoothctl show + hcitool con
- connected(ACL 连接存在)→ 常亮
- scanning/connecting(Discovering: yes)→ 闪烁
- 无 adapter / Powered: no / 静止 → 熄灭

client.js:启动时开启 BtMonitor,stop() 时清理
provisioning.js:移除所有 led.bt 调用,BT 灯统一由 BtMonitor 管理

Made-with: Cursor
This commit is contained in:
stswangzhiping
2026-03-24 23:14:02 +08:00
parent dcc20e2cad
commit 837cb8865f
3 changed files with 123 additions and 19 deletions

107
lib/bt-monitor.js Normal file
View File

@@ -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 };

View File

@@ -8,6 +8,7 @@ const { getBoxId } = require('./fingerprint');
const { collect } = require('./metrics'); const { collect } = require('./metrics');
const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc'); // getDashboardInfo 也用于心跳中定期刷新 const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc'); // getDashboardInfo 也用于心跳中定期刷新
const { ProvisionManager } = require('./provisioning'); const { ProvisionManager } = require('./provisioning');
const { BtMonitor } = require('./bt-monitor');
const { hasInternet, getLocalIps } = require('./network'); const { hasInternet, getLocalIps } = require('./network');
const led = require('./led'); const led = require('./led');
@@ -33,6 +34,7 @@ class ClawClient {
this._backoff = 1_000; this._backoff = 1_000;
this._stopped = false; this._stopped = false;
this._frpc = new FrpcManager(); this._frpc = new FrpcManager();
this._btMonitor = null;
this._dashInfo = {}; this._dashInfo = {};
this._hbCount = 0; // 心跳计数器,用于定期刷新 dashboard 信息 this._hbCount = 0; // 心跳计数器,用于定期刷新 dashboard 信息
this._externalIp = null; // 外网 IP this._externalIp = null; // 外网 IP
@@ -83,6 +85,10 @@ class ClawClient {
this._startSdNotify(); this._startSdNotify();
// 启动蓝牙状态监控(独立于网络,立即开始)
this._btMonitor = new BtMonitor();
this._btMonitor.start();
// 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP // 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP
this._provisionMgr = new ProvisionManager(this._cfg.claw_id); this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
this._connectionStarted = false; this._connectionStarted = false;
@@ -167,6 +173,7 @@ class ClawClient {
this._clearPing(); this._clearPing();
this._clearNetMonitor(); this._clearNetMonitor();
if (this._sdTimer) { clearInterval(this._sdTimer); this._sdTimer = null; } 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; } if (this._provisionMgr) { this._provisionMgr.stop(); this._provisionMgr = null; }
this._frpc.stop(); this._frpc.stop();
if (this._ws) this._ws.terminate(); if (this._ws) this._ws.terminate();

View File

@@ -36,13 +36,11 @@ class ProvisionManager extends EventEmitter {
async start() { async start() {
led.off(); // WiFi 灯初始状态:熄灭 led.off(); // WiFi 灯初始状态:熄灭
led.bt.off(); // BT 灯初始状态:熄灭
// WiFi STA 已连接 → 直接进入 STA 模式 // WiFi STA 已连接 → 直接进入 STA 模式
if (isWifiStaConnected()) { if (isWifiStaConnected()) {
this._state = 'sta'; this._state = 'sta';
log.info('provision', 'WiFi STA 已连接AP 不启动'); log.info('provision', 'WiFi STA 已连接AP 不启动');
led.bt.on(); // 已配网BT 灯常亮
this._emitNetworkReady(); this._emitNetworkReady();
this._startMonitor(); this._startMonitor();
return; return;
@@ -63,12 +61,10 @@ class ProvisionManager extends EventEmitter {
if (hasSavedWifiConnection()) { if (hasSavedWifiConnection()) {
log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...'); log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...');
led.blink(); // WiFi 灯:等待自动重连期间闪烁 led.blink(); // WiFi 灯:等待自动重连期间闪烁
// BT 灯:等待 NM 自动重连不属于 BLE 搜索/连接阶段,保持熄灭
const connected = await this._waitForWifiConnect(); const connected = await this._waitForWifiConnect();
if (connected) { if (connected) {
this._state = 'sta'; this._state = 'sta';
log.info('provision', 'WiFi 自动连接成功AP 不启动'); log.info('provision', 'WiFi 自动连接成功AP 不启动');
led.bt.on(); // 自动重连成功BT 灯常亮
this._emitNetworkReady(); this._emitNetworkReady();
this._startMonitor(); this._startMonitor();
return; return;
@@ -119,7 +115,6 @@ class ProvisionManager extends EventEmitter {
this._stopAll(); this._stopAll();
this._state = 'idle'; this._state = 'idle';
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器 led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
led.bt.destroy(); // BT 灯:停止时关灯
} }
// ── 进入 AP 模式 ───────────────────────────────────────────────────────── // ── 进入 AP 模式 ─────────────────────────────────────────────────────────
@@ -128,7 +123,6 @@ class ProvisionManager extends EventEmitter {
if (this._state === 'ap') return; if (this._state === 'ap') return;
led.off(); // AP 模式WiFi 未连接WiFi 灯熄灭 led.off(); // AP 模式WiFi 未连接WiFi 灯熄灭
led.bt.blink(); // AP 模式BLE 配网进行中BT 灯闪烁
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP有线时等 WS 连接后再定 if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP有线时等 WS 连接后再定
try { try {
@@ -175,13 +169,12 @@ class ProvisionManager extends EventEmitter {
this._state = 'sta'; this._state = 'sta';
log.info('provision', `WiFi 已连接: ${ssid}`); log.info('provision', `WiFi 已连接: ${ssid}`);
led.on(); // WiFi 灯:连接成功 → 常亮 led.on(); // WiFi 灯:连接成功 → 常亮
led.bt.on(); // BT 灯:配网成功 → 常亮
this.emit('network-ready'); this.emit('network-ready');
return result; return result;
} }
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`); log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
this._enterAP(); // _enterAP 内部会调用 led.off() / led.bt.blink() this._enterAP();
return result; return result;
} }
@@ -203,7 +196,6 @@ class ProvisionManager extends EventEmitter {
log.info('provision', 'WiFi 已外部连接,关闭 AP'); log.info('provision', 'WiFi 已外部连接,关闭 AP');
this._stopAPServices(); this._stopAPServices();
this._state = 'sta'; this._state = 'sta';
led.bt.on(); // 外部连接成功BT 灯常亮
this.emit('network-ready'); this.emit('network-ready');
} }
@@ -211,13 +203,11 @@ class ProvisionManager extends EventEmitter {
if (this._state === 'sta') { if (this._state === 'sta') {
if (hasInternet()) { if (hasInternet()) {
led.on(); led.on();
led.bt.on(); // 有网 → BT 灯常亮
} else { } else {
led.off(); // WiFi 已连接但无互联网 led.off(); // WiFi 已连接但无互联网
led.bt.off(); // 无互联网时 BT 灯也熄灭
} }
} }
// AP 模式下 led/bt 已在 _enterAP() 中设置,无需重复操作 // AP 模式下 led 已在 _enterAP() 中熄灭,无需重复操作
}, MONITOR_INTERVAL_MS); }, MONITOR_INTERVAL_MS);
} }