- WifiLed drives play; LanLed drives wifi+eth pair (matches panel silkscreen) - hasLanCableCarrier(): CLAWD_ETH_IFACE-only carrier when set (reliable unplug) - LAN poll 1s; config.activated persisted for immediate setApps on boot/reconnect - setApps double vfdOn alarm after 50ms for slow OpenVFD - Clear activated + setSetup on credential errors - install.sh env comment for CLAWD_ETH_IFACE Made-with: Cursor
290 lines
6.8 KiB
JavaScript
290 lines
6.8 KiB
JavaScript
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const log = require('./logger');
|
||
const { hasLanCableCarrier } = require('./network');
|
||
|
||
/**
|
||
* OpenVFD 图标:/sys/class/leds/openvfd/led_on|led_off(写入图标名)。
|
||
*
|
||
* 映射(与面板丝印一致:play=WiFi 状态,wifi+eth=有线链路):
|
||
* play → 产品 WiFi 灯(配网逻辑 on/off/blink)
|
||
* wifi + eth 同亮/同灭 → LAN(有线插拔,见 hasLanCableCarrier / CLAWD_ETH_IFACE)
|
||
* alarm → pwr(SETUP=灭 / APPS=亮)
|
||
* BT → 无 sysfs,仅日志
|
||
*
|
||
* 数码管(AP/Conn/时间等):仍仅 debug 输出,不接 sysfs。
|
||
*
|
||
* CLAWD_OPENVFD_PATH 默认 /sys/class/leds/openvfd
|
||
*/
|
||
|
||
const BLINK_INTERVAL_MS = 500;
|
||
const LAN_POLL_MS = 1000;
|
||
|
||
const VFD_BASE = process.env.CLAWD_OPENVFD_PATH || '/sys/class/leds/openvfd';
|
||
|
||
function vfdOn(icon) {
|
||
try {
|
||
fs.writeFileSync(`${VFD_BASE}/led_on`, icon);
|
||
} catch (e) {
|
||
log.debug('led', `openvfd led_on ${icon}: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
function vfdOff(icon) {
|
||
try {
|
||
fs.writeFileSync(`${VFD_BASE}/led_off`, icon);
|
||
} catch (e) {
|
||
log.debug('led', `openvfd led_off ${icon}: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
function vfdWifiPair(on) {
|
||
if (on) {
|
||
vfdOn('wifi');
|
||
vfdOn('eth');
|
||
} else {
|
||
vfdOff('wifi');
|
||
vfdOff('eth');
|
||
}
|
||
}
|
||
|
||
class WifiLed {
|
||
constructor() {
|
||
this._blinkTimer = null;
|
||
this._blinkState = false;
|
||
this._current = null;
|
||
}
|
||
|
||
on() {
|
||
if (this._current === 'on') return;
|
||
this._stopBlink();
|
||
this._write(1);
|
||
this._current = 'on';
|
||
log.info('led', 'WiFi 指示灯 → 常亮');
|
||
}
|
||
|
||
off() {
|
||
if (this._current === 'off') return;
|
||
this._stopBlink();
|
||
this._write(0);
|
||
this._current = 'off';
|
||
log.info('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.info('led', 'WiFi 指示灯 → 闪烁');
|
||
}
|
||
|
||
destroy() {
|
||
this._stopBlink();
|
||
this._write(0);
|
||
this._current = 'off';
|
||
}
|
||
|
||
_stopBlink() {
|
||
if (this._blinkTimer) {
|
||
clearInterval(this._blinkTimer);
|
||
this._blinkTimer = null;
|
||
}
|
||
}
|
||
|
||
_write(val) {
|
||
const on = !!val;
|
||
log.debug('led', `[vfd] WiFi(play)<= ${on ? 1 : 0}`);
|
||
if (on) vfdOn('play');
|
||
else vfdOff('play');
|
||
}
|
||
}
|
||
|
||
class BtLed {
|
||
constructor() {
|
||
this._blinkTimer = null;
|
||
this._blinkState = false;
|
||
this._current = null;
|
||
}
|
||
|
||
on() {
|
||
if (this._current === 'on') return;
|
||
this._stopBlink();
|
||
this._write(1);
|
||
this._current = 'on';
|
||
log.info('led', 'BT 指示灯 → 常亮');
|
||
}
|
||
|
||
off() {
|
||
if (this._current === 'off') return;
|
||
this._stopBlink();
|
||
this._write(0);
|
||
this._current = 'off';
|
||
log.info('led', 'BT 指示灯 → 熄灭');
|
||
}
|
||
|
||
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.info('led', 'BT 指示灯 → 闪烁');
|
||
}
|
||
|
||
destroy() {
|
||
this._stopBlink();
|
||
this._write(0);
|
||
this._current = 'off';
|
||
}
|
||
|
||
_stopBlink() {
|
||
if (this._blinkTimer) {
|
||
clearInterval(this._blinkTimer);
|
||
this._blinkTimer = null;
|
||
}
|
||
}
|
||
|
||
_write(_val) {
|
||
log.debug('led', '[vfd] BT 无 OpenVFD 映射,忽略');
|
||
}
|
||
}
|
||
|
||
class Display {
|
||
constructor() {
|
||
this._blinkTimer = null;
|
||
}
|
||
|
||
showAP() {
|
||
this._stopBlink();
|
||
this._write('#m3AP ');
|
||
log.info('display', '显示屏 → AP');
|
||
}
|
||
|
||
showConn() {
|
||
this._stopBlink();
|
||
this._write('#m3Conn');
|
||
log.info('display', '显示屏 → Conn(闪烁)');
|
||
let visible = true;
|
||
const blink = () => {
|
||
visible = !visible;
|
||
this._write(visible ? '#m3Conn' : '#c1');
|
||
this._blinkTimer = setTimeout(blink, visible ? 1000 : 500);
|
||
};
|
||
this._blinkTimer = setTimeout(blink, 1000);
|
||
}
|
||
|
||
showErr0() {
|
||
this._stopBlink();
|
||
this._write('#m3Err0');
|
||
log.info('display', '显示屏 → Err0');
|
||
}
|
||
|
||
showTime() {
|
||
this._stopBlink();
|
||
this._write('#s1');
|
||
log.info('display', '显示屏 → 时间');
|
||
}
|
||
|
||
showPin(pin) {
|
||
this._stopBlink();
|
||
const s = String(pin || '').padStart(4, '0').slice(-4);
|
||
this._write('#m2' + s);
|
||
log.info('display', `显示屏 → PIN: ${s}(闪烁)`);
|
||
let visible = true;
|
||
const blink = () => {
|
||
visible = !visible;
|
||
this._write(visible ? '#m2' + s : '#c1');
|
||
this._blinkTimer = setTimeout(blink, visible ? 1000 : 500);
|
||
};
|
||
this._blinkTimer = setTimeout(blink, 1000);
|
||
}
|
||
|
||
_stopBlink() {
|
||
if (this._blinkTimer) {
|
||
clearTimeout(this._blinkTimer);
|
||
clearInterval(this._blinkTimer);
|
||
this._blinkTimer = null;
|
||
}
|
||
}
|
||
|
||
_write(val) {
|
||
log.debug('display', `[vfd] ${val}`);
|
||
}
|
||
}
|
||
|
||
class StatusLed {
|
||
setSetup() {
|
||
vfdOff('alarm');
|
||
log.debug('led', '[vfd] alarm(pwr)<= 0');
|
||
log.info('led', '状态灯 → SETUP(未激活)');
|
||
}
|
||
|
||
setApps() {
|
||
vfdOn('alarm');
|
||
// 部分 OpenVFD 驱动单次写入生效慢,短延迟再写一次
|
||
setTimeout(() => vfdOn('alarm'), 50);
|
||
log.debug('led', '[vfd] alarm(pwr)<= 1');
|
||
log.info('led', '状态灯 → APPS(已激活)');
|
||
}
|
||
|
||
off() {
|
||
vfdOff('alarm');
|
||
log.debug('led', '[vfd] alarm(pwr)<= 0 (off)');
|
||
}
|
||
}
|
||
|
||
class LanLed {
|
||
constructor() {
|
||
this._timer = null;
|
||
this._current = null;
|
||
}
|
||
|
||
start() {
|
||
this._sync();
|
||
this._timer = setInterval(() => this._sync(), LAN_POLL_MS);
|
||
}
|
||
|
||
stop() {
|
||
if (this._timer) {
|
||
clearInterval(this._timer);
|
||
this._timer = null;
|
||
}
|
||
vfdWifiPair(false);
|
||
this._current = null;
|
||
}
|
||
|
||
_sync() {
|
||
const up = hasLanCableCarrier();
|
||
if (up) {
|
||
if (this._current !== 'on') {
|
||
vfdWifiPair(true);
|
||
this._current = 'on';
|
||
log.info('led', 'LAN(有线 carrier)→ 亮');
|
||
}
|
||
} else if (this._current !== 'off') {
|
||
vfdWifiPair(false);
|
||
this._current = 'off';
|
||
log.info('led', 'LAN(有线 carrier)→ 灭');
|
||
}
|
||
}
|
||
}
|
||
|
||
const lan = new LanLed();
|
||
|
||
module.exports = new WifiLed();
|
||
module.exports.bt = new BtLed();
|
||
module.exports.status = new StatusLed();
|
||
module.exports.display = new Display();
|
||
module.exports.lan = lan;
|