Files
clawd/lib/led.js
stswangzhiping fb8a408f93 fix(led): VFD 管道用 O_NONBLOCK 防止事件循环阻塞
FIFO 写入改为 O_WRONLY | O_NONBLOCK:
- 无读端(vfdservice 关闭读端后)openSync 立即抛 ENXIO 而非永久阻塞
- showPin blink timer 每 500-1000ms 自动重试,vfdservice 恢复读后
  即可成功显示 PIN,彻底解决"长时间激活后解绑 PIN 不闪"问题

同时移除 66b94d8 加的 showTime keepalive:
- keepalive 用的也是阻塞 openSync,vfdservice 关闭读端时反而会
  在 setTimeout 回调里阻塞整个事件循环,比不加更危险

Made-with: Cursor
2026-04-03 19:01:14 +08:00

357 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const fs = require('fs');
const log = require('./logger');
const { hasLanCableCarrier } = require('./network');
/**
* OpenVFD 图标:/sys/class/leds/openvfd/led_on|led_off写入图标名
*
* 映射与面板丝印一致LAN=playWiFi=wifi+eth
* wifi + eth 同亮/同灭 → 产品 WiFi 灯(配网 on/off/blink
* play → LAN有线插拔见 hasLanCableCarrier / CLAWD_ETH_IFACE
* alarm → pwrSETUP=灭 / APPS=亮)
* BT → 无 sysfs仅日志
*
* 数码管vfdservice 管道660 字节mode=4 TITLE / mode=1 CLOCK与板端 Demo 一致)。
* CLAWD_VFD_PIPE 默认 /tmp/openvfd_service管道不存在时仅 debug不抛错。
*
* CLAWD_OPENVFD_PATH 默认 /sys/class/leds/openvfd图标灯
*/
const BLINK_INTERVAL_MS = 500;
const LAN_POLL_MS = 500;
const VFD_BASE = process.env.CLAWD_OPENVFD_PATH || '/sys/class/leds/openvfd';
const VFD_PIPE = process.env.CLAWD_VFD_PIPE || '/tmp/openvfd_service';
const VFD_BUF_SIZE = 660;
const VFD_MODE_CLOCK = 1;
const VFD_MODE_TITLE = 4;
const VFD_TITLE_OFF = 20;
function vfdPipePresent() {
try {
return fs.existsSync(VFD_PIPE);
} catch (_) {
return false;
}
}
/** TITLE固定 4 字符ASCII供 vfdservice 解析为段码 */
function vfdTitleNormalize(raw) {
return String(raw || '')
.toUpperCase()
.replace(/[^A-Z0-9 \-]/g, ' ')
.slice(0, 4)
.padEnd(4, ' ');
}
// O_WRONLY | O_NONBLOCKFIFO 无读端时立即抛 ENXIO 而非永久阻塞事件循环
const VFD_OPEN_FLAGS = fs.constants.O_WRONLY | fs.constants.O_NONBLOCK;
function writeVfdPipeTitle(text4) {
if (!vfdPipePresent()) return;
const seg = vfdTitleNormalize(text4);
const data = Buffer.alloc(VFD_BUF_SIZE);
data.writeUInt16LE(VFD_MODE_TITLE, 0);
try {
data.write(`${seg}\0`, VFD_TITLE_OFF, 'ascii');
} catch (e) {
log.debug('display', `openvfd title encode: ${e.message}`);
return;
}
try {
const fd = fs.openSync(VFD_PIPE, VFD_OPEN_FLAGS);
fs.writeSync(fd, data);
fs.closeSync(fd);
} catch (e) {
log.debug('display', `openvfd pipe title: ${e.message}`);
}
}
function writeVfdPipeClock() {
if (!vfdPipePresent()) return;
const data = Buffer.alloc(VFD_BUF_SIZE);
data.writeUInt16LE(VFD_MODE_CLOCK, 0);
try {
const fd = fs.openSync(VFD_PIPE, VFD_OPEN_FLAGS);
fs.writeSync(fd, data);
fs.closeSync(fd);
} catch (e) {
log.debug('display', `openvfd pipe clock: ${e.message}`);
}
}
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] WiFiwifi+eth<= ${on ? 1 : 0}`);
vfdWifiPair(on);
}
}
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._pipeTitle('AP ', '#m3AP ');
log.info('display', '显示屏 → AP闪烁');
let visible = true;
const blink = () => {
visible = !visible;
this._pipeTitle(visible ? 'AP ' : ' ', visible ? '#m3AP ' : '#c1');
this._blinkTimer = setTimeout(blink, visible ? 1000 : 500);
};
this._blinkTimer = setTimeout(blink, 1000);
}
showConn() {
this._stopBlink();
this._pipeTitle('Conn', '#m3Conn');
log.info('display', '显示屏 → Conn闪烁');
let visible = true;
const blink = () => {
visible = !visible;
this._pipeTitle(visible ? 'Conn' : ' ', visible ? '#m3Conn' : '#c1');
this._blinkTimer = setTimeout(blink, visible ? 1000 : 500);
};
this._blinkTimer = setTimeout(blink, 1000);
}
showErr0() {
this._stopBlink();
this._pipeTitle('Err0', '#m3Err0');
log.info('display', '显示屏 → Err0');
}
showTime() {
this._stopBlink();
writeVfdPipeClock();
log.info('display', '显示屏 → 时间');
}
showPin(pin) {
this._stopBlink();
const s = String(pin || '').padStart(4, '0').slice(-4);
this._pipeTitle(s, '#m2' + s);
log.info('display', `显示屏 → PIN: ${s}(闪烁)`);
let visible = true;
const blink = () => {
visible = !visible;
this._pipeTitle(visible ? s : ' ', 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;
}
}
/** 保留原 #m3/#m2/#c1 的 debug 语义,并写 vfdservice TITLE */
_pipeTitle(fourCharText, debugLegacy) {
log.debug('display', `[vfd] ${debugLegacy}`);
writeVfdPipeTitle(fourCharText);
}
}
class StatusLed {
setSetup() {
vfdOff('alarm');
log.debug('led', '[vfd] alarmpwr<= 0');
log.info('led', '状态灯 → SETUP未激活');
}
setApps() {
vfdOn('alarm');
// 部分 OpenVFD 驱动单次写入生效慢,短延迟再写一次
setTimeout(() => vfdOn('alarm'), 50);
log.debug('led', '[vfd] alarmpwr<= 1');
log.info('led', '状态灯 → APPS已激活');
}
off() {
vfdOff('alarm');
log.debug('led', '[vfd] alarmpwr<= 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;
}
vfdOff('play');
this._current = null;
}
_sync() {
const up = hasLanCableCarrier();
if (up) {
if (this._current !== 'on') {
vfdOn('play');
this._current = 'on';
log.info('led', 'LANplay / 有线 carrier→ 亮');
}
} else if (this._current !== 'off') {
vfdOff('play');
this._current = 'off';
log.info('led', 'LANplay / 有线 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;