feat: split VFD backends by hardware

This commit is contained in:
OpenClaw Bot
2026-04-26 23:12:12 +08:00
parent c9ce87c93a
commit 03dc7c2527
5 changed files with 759 additions and 356 deletions

View File

@@ -1,356 +1,39 @@
'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;
'use strict';
const log = require('./logger');
const { isRK3566, readDeviceModel } = require('./led/detect');
function loadImpl() {
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
const model = readDeviceModel();
let name;
if (forced) {
name = forced;
} else if (isRK3566()) {
name = 'rk3566';
} else {
name = 'openvfd';
}
try {
if (name === 'rk3566' || name === '3566') {
log.info('led', `LED/VFD backend → rk3566-openvfd (${model || 'unknown model'})`);
return require('./led/rk3566-openvfd');
}
if (name === 'noop' || name === 'none' || name === 'off') {
log.info('led', `LED/VFD backend → noop (${model || 'unknown model'})`);
return require('./led/noop');
}
if (name !== 'openvfd' && name !== 'default') {
log.warn('led', `未知 CLAWD_LED_IMPL=${name},回退 openvfd`);
}
log.info('led', `LED/VFD backend → openvfd-class (${model || 'unknown model'})`);
return require('./led/openvfd-class');
} catch (e) {
log.warn('led', `LED/VFD backend ${name} 加载失败:${e.message},回退 noop`);
return require('./led/noop');
}
}
module.exports = loadImpl();