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:
107
lib/bt-monitor.js
Normal file
107
lib/bt-monitor.js
Normal 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 };
|
||||||
@@ -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');
|
||||||
|
|
||||||
@@ -32,8 +33,9 @@ class ClawClient {
|
|||||||
this._hbTimer = null;
|
this._hbTimer = null;
|
||||||
this._backoff = 1_000;
|
this._backoff = 1_000;
|
||||||
this._stopped = false;
|
this._stopped = false;
|
||||||
this._frpc = new FrpcManager();
|
this._frpc = new FrpcManager();
|
||||||
this._dashInfo = {};
|
this._btMonitor = null;
|
||||||
|
this._dashInfo = {};
|
||||||
this._hbCount = 0; // 心跳计数器,用于定期刷新 dashboard 信息
|
this._hbCount = 0; // 心跳计数器,用于定期刷新 dashboard 信息
|
||||||
this._externalIp = null; // 外网 IP
|
this._externalIp = null; // 外网 IP
|
||||||
this._location = null; // 地理位置(由 ipplus360 返回,如"北京市-北京市西城区")
|
this._location = null; // 地理位置(由 ipplus360 返回,如"北京市-北京市西城区")
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ class ProvisionManager extends EventEmitter {
|
|||||||
isApMode() { return this._state === 'ap'; }
|
isApMode() { return this._state === 'ap'; }
|
||||||
|
|
||||||
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;
|
||||||
@@ -118,8 +114,7 @@ class ProvisionManager extends EventEmitter {
|
|||||||
this._stopMonitor();
|
this._stopMonitor();
|
||||||
this._stopAll();
|
this._stopAll();
|
||||||
this._state = 'idle';
|
this._state = 'idle';
|
||||||
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
|
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
|
||||||
led.bt.destroy(); // BT 灯:停止时关灯
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
|
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
|
||||||
@@ -127,8 +122,7 @@ class ProvisionManager extends EventEmitter {
|
|||||||
_enterAP() {
|
_enterAP() {
|
||||||
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 {
|
||||||
@@ -174,14 +168,13 @@ class ProvisionManager extends EventEmitter {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user