Files
clawd/lib/led.js
stswangzhiping 66b94d828f fix: 还原 inactive 路径 restartGateway + showTime keepalive
1. 还原 7737e08:removeProviderByName 重新调用 restartGateway(),
   inactive/active 状态变化均重启 gateway 使 openclaw.json 生效

2. led.js showTime() 加 30s keepalive 定时器:持续向 vfdservice
   管道写入,防止长时间无写入后管道"冷却",导致随后 showPin()
   调用 openSync(FIFO, 'w') 因无读端而阻塞、blink timer 无法触发

Made-with: Cursor
2026-04-03 18:40:08 +08:00

361 lines
9.1 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, ' ');
}
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, 'w');
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, 'w');
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', '显示屏 → 时间');
// 每 30s 刷新一次,保持 vfdservice 持续从管道读取,
// 防止长时间无写入后管道"冷却",导致后续 showPin 写入阻塞
const refresh = () => {
writeVfdPipeClock();
this._blinkTimer = setTimeout(refresh, 30_000);
};
this._blinkTimer = setTimeout(refresh, 30_000);
}
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;