feat: add WiFi LED indicator (openvfd b5)

Made-with: Cursor
This commit is contained in:
stswangzhiping
2026-03-18 19:47:20 +08:00
parent d66558c9de
commit 875e69a454
2 changed files with 111 additions and 3 deletions

86
lib/led.js Normal file
View File

@@ -0,0 +1,86 @@
'use strict';
const fs = require('fs');
const log = require('./logger');
/**
* WiFi 指示灯控制
*
* 硬件路径: /sys/devices/platform/openvfd/attr/b5
* 1 = 亮 0 = 灭
*
* LED 状态与 WiFi 状态的对应关系:
* - WiFi 已连接且互联网畅通 → 常亮
* - WiFi 连接中(正在尝试) → 闪烁
* - WiFi 未连接 / 无互联网 → 熄灭
*/
const LED_PATH = process.env.CLAWD_LED_PATH || '/sys/devices/platform/openvfd/attr/b5';
const BLINK_INTERVAL_MS = 500; // 闪烁间隔ms
class WifiLed {
constructor() {
this._blinkTimer = null;
this._blinkState = false;
this._current = null; // 'on' | 'off' | 'blink'
}
/** 常亮 */
on() {
if (this._current === 'on') return;
this._stopBlink();
this._write(1);
this._current = 'on';
log.debug('led', 'WiFi 指示灯 → 常亮');
}
/** 熄灭 */
off() {
if (this._current === 'off') return;
this._stopBlink();
this._write(0);
this._current = 'off';
log.debug('led', 'WiFi 指示灯 → 熄灭');
}
/** 闪烁(连接中) */
blink(intervalMs = BLINK_INTERVAL_MS) {
if (this._current === 'blink') return;
this._stopBlink();
this._blinkState = true;
this._write(1);
this._blinkTimer = setInterval(() => {
this._blinkState = !this._blinkState;
this._write(this._blinkState ? 1 : 0);
}, intervalMs);
this._current = 'blink';
log.debug('led', 'WiFi 指示灯 → 闪烁');
}
/** 释放资源,关灯 */
destroy() {
this._stopBlink();
this._write(0);
this._current = 'off';
}
// ── 内部 ──────────────────────────────────────────────────────────────────
_stopBlink() {
if (this._blinkTimer) {
clearInterval(this._blinkTimer);
this._blinkTimer = null;
}
}
_write(val) {
try {
fs.writeFileSync(LED_PATH, String(val));
} catch (_) {
// 设备不支持 openvfd 时静默忽略(开发机上不会报错)
}
}
}
// 全局单例,整个进程共用
module.exports = new WifiLed();

View File

@@ -5,6 +5,7 @@ const log = require('./logger');
const { hasInternet, hasSavedWifiConnection, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, AP_IP } = require('./network'); const { hasInternet, hasSavedWifiConnection, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, AP_IP } = require('./network');
const { DnsHijack } = require('./dns-hijack'); const { DnsHijack } = require('./dns-hijack');
const { CaptiveServer } = require('./captive-server'); const { CaptiveServer } = require('./captive-server');
const led = require('./led');
const MONITOR_INTERVAL_MS = 30_000; const MONITOR_INTERVAL_MS = 30_000;
const BOOT_WAIT_MAX_MS = 20_000; // 等待 NM 自动连接的最大时间 const BOOT_WAIT_MAX_MS = 20_000; // 等待 NM 自动连接的最大时间
@@ -31,6 +32,8 @@ class ProvisionManager extends EventEmitter {
} }
async start() { async start() {
led.off(); // 初始状态:灭灯
// WiFi 已连接 → 直接进入 STA 模式 // WiFi 已连接 → 直接进入 STA 模式
if (isWifiStaConnected()) { if (isWifiStaConnected()) {
this._state = 'sta'; this._state = 'sta';
@@ -43,6 +46,7 @@ class ProvisionManager extends EventEmitter {
// 有已保存的 WiFi 配置 → 等 NM 自动连接(重启场景) // 有已保存的 WiFi 配置 → 等 NM 自动连接(重启场景)
if (hasSavedWifiConnection()) { if (hasSavedWifiConnection()) {
log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...'); log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...');
led.blink(); // 等待自动重连期间闪烁
const connected = await this._waitForWifiConnect(); const connected = await this._waitForWifiConnect();
if (connected) { if (connected) {
this._state = 'sta'; this._state = 'sta';
@@ -64,7 +68,10 @@ class ProvisionManager extends EventEmitter {
} }
_emitNetworkReady() { _emitNetworkReady() {
if (hasInternet()) this.emit('network-ready'); if (hasInternet()) {
led.on(); // 网络畅通 → 常亮
this.emit('network-ready');
}
} }
/** /**
@@ -90,6 +97,7 @@ class ProvisionManager extends EventEmitter {
this._stopMonitor(); this._stopMonitor();
this._stopAll(); this._stopAll();
this._state = 'idle'; this._state = 'idle';
led.destroy(); // 停止时关灯、释放闪烁定时器
} }
// ── 进入 AP 模式 ───────────────────────────────────────────────────────── // ── 进入 AP 模式 ─────────────────────────────────────────────────────────
@@ -97,6 +105,8 @@ class ProvisionManager extends EventEmitter {
_enterAP() { _enterAP() {
if (this._state === 'ap') return; if (this._state === 'ap') return;
led.off(); // AP 模式WiFi 未连接,灭灯
try { try {
// AP 模式下无法扫描 WiFi必须在开 AP 之前扫描并缓存 // AP 模式下无法扫描 WiFi必须在开 AP 之前扫描并缓存
log.info('provision', '扫描周边 WiFi...'); log.info('provision', '扫描周边 WiFi...');
@@ -131,6 +141,7 @@ class ProvisionManager extends EventEmitter {
this._state = 'connecting'; this._state = 'connecting';
log.info('provision', `用户请求连接 WiFi: ${ssid}`); log.info('provision', `用户请求连接 WiFi: ${ssid}`);
led.blink(); // 正在连接 → 闪烁
this._stopAPServices(); this._stopAPServices();
@@ -139,12 +150,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(); // 连接成功 → 常亮
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(); this._enterAP(); // _enterAP 内部会调用 led.off()
return result; return result;
} }
@@ -158,7 +170,8 @@ class ProvisionManager extends EventEmitter {
if (this._state === 'sta' && !wifiUp) { if (this._state === 'sta' && !wifiUp) {
log.warn('provision', 'WiFi 连接已断开,重新启动 AP'); log.warn('provision', 'WiFi 连接已断开,重新启动 AP');
this._enterAP(); this._enterAP(); // 内部调用 led.off()
return;
} }
if (this._state === 'ap' && wifiUp) { if (this._state === 'ap' && wifiUp) {
@@ -167,6 +180,15 @@ class ProvisionManager extends EventEmitter {
this._state = 'sta'; this._state = 'sta';
this.emit('network-ready'); this.emit('network-ready');
} }
// STA 模式下持续监测互联网连通性,实时更新 LED
if (this._state === 'sta') {
if (hasInternet()) {
led.on();
} else {
led.off(); // WiFi 已连接但无互联网
}
}
}, MONITOR_INTERVAL_MS); }, MONITOR_INTERVAL_MS);
} }