From 03dc7c2527508a329d3b9bead6672411a321155e Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Sun, 26 Apr 2026 23:12:12 +0800 Subject: [PATCH] feat: split VFD backends by hardware --- lib/led.js | 395 ++++---------------------------------- lib/led/detect.js | 22 +++ lib/led/noop.js | 41 ++++ lib/led/openvfd-class.js | 356 ++++++++++++++++++++++++++++++++++ lib/led/rk3566-openvfd.js | 301 +++++++++++++++++++++++++++++ 5 files changed, 759 insertions(+), 356 deletions(-) create mode 100644 lib/led/detect.js create mode 100644 lib/led/noop.js create mode 100644 lib/led/openvfd-class.js create mode 100644 lib/led/rk3566-openvfd.js diff --git a/lib/led.js b/lib/led.js index a03debf..24decea 100644 --- a/lib/led.js +++ b/lib/led.js @@ -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=play,WiFi=wifi+eth): - * wifi + eth 同亮/同灭 → 产品 WiFi 灯(配网 on/off/blink) - * play → LAN(有线插拔,见 hasLanCableCarrier / CLAWD_ETH_IFACE) - * alarm → pwr(SETUP=灭 / 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_NONBLOCK:FIFO 无读端时立即抛 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] WiFi(wifi+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] 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; - } - vfdOff('play'); - this._current = null; - } - - _sync() { - const up = hasLanCableCarrier(); - if (up) { - if (this._current !== 'on') { - vfdOn('play'); - this._current = 'on'; - log.info('led', 'LAN(play / 有线 carrier)→ 亮'); - } - } else if (this._current !== 'off') { - vfdOff('play'); - this._current = 'off'; - log.info('led', 'LAN(play / 有线 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(); diff --git a/lib/led/detect.js b/lib/led/detect.js new file mode 100644 index 0000000..401cd45 --- /dev/null +++ b/lib/led/detect.js @@ -0,0 +1,22 @@ +'use strict'; + +const fs = require('fs'); + +function readDeviceModel() { + try { + return fs.readFileSync('/proc/device-tree/model', 'utf8') + .replace(/\0/g, '') + .trim(); + } catch (_) { + return ''; + } +} + +function isRK3566() { + return /RK3566/i.test(readDeviceModel()); +} + +module.exports = { + readDeviceModel, + isRK3566, +}; diff --git a/lib/led/noop.js b/lib/led/noop.js new file mode 100644 index 0000000..b4be422 --- /dev/null +++ b/lib/led/noop.js @@ -0,0 +1,41 @@ +'use strict'; + +const log = require('../logger'); + +class BasicLed { + constructor(name) { + this.name = name; + this._current = null; + } + on() { this._current = 'on'; log.debug('led', `[noop] ${this.name} on`); } + off() { this._current = 'off'; log.debug('led', `[noop] ${this.name} off`); } + blink() { this._current = 'blink'; log.debug('led', `[noop] ${this.name} blink`); } + destroy() { this._current = 'off'; log.debug('led', `[noop] ${this.name} destroy`); } +} + +class StatusLed { + setSetup() { log.debug('led', '[noop] status setup'); } + setApps() { log.debug('led', '[noop] status apps'); } + off() { log.debug('led', '[noop] status off'); } +} + +class Display { + showAP() { log.debug('display', '[noop] AP'); } + showConn() { log.debug('display', '[noop] Conn'); } + showErr0() { log.debug('display', '[noop] Err0'); } + showTime() { log.debug('display', '[noop] time'); } + showPin(pin) { log.debug('display', `[noop] PIN ${pin}`); } +} + +class LanLed { + start() { log.debug('led', '[noop] LAN start ignored'); } + stop() { log.debug('led', '[noop] LAN stop ignored'); } +} + +const led = new BasicLed('wifi'); +led.bt = new BasicLed('bt'); +led.status = new StatusLed(); +led.display = new Display(); +led.lan = new LanLed(); + +module.exports = led; diff --git a/lib/led/openvfd-class.js b/lib/led/openvfd-class.js new file mode 100644 index 0000000..36692a7 --- /dev/null +++ b/lib/led/openvfd-class.js @@ -0,0 +1,356 @@ +'use strict'; + +const fs = require('fs'); +const log = require('../logger'); +const { hasLanCableCarrier } = require('../network'); + +/** + * OpenVFD 图标:/sys/class/leds/openvfd/led_on|led_off(写入图标名)。 + * + * 映射(与面板丝印一致:LAN=play,WiFi=wifi+eth): + * wifi + eth 同亮/同灭 → 产品 WiFi 灯(配网 on/off/blink) + * play → LAN(有线插拔,见 hasLanCableCarrier / CLAWD_ETH_IFACE) + * alarm → pwr(SETUP=灭 / 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_NONBLOCK:FIFO 无读端时立即抛 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] WiFi(wifi+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] 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; + } + vfdOff('play'); + this._current = null; + } + + _sync() { + const up = hasLanCableCarrier(); + if (up) { + if (this._current !== 'on') { + vfdOn('play'); + this._current = 'on'; + log.info('led', 'LAN(play / 有线 carrier)→ 亮'); + } + } else if (this._current !== 'off') { + vfdOff('play'); + this._current = 'off'; + log.info('led', 'LAN(play / 有线 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; diff --git a/lib/led/rk3566-openvfd.js b/lib/led/rk3566-openvfd.js new file mode 100644 index 0000000..87dc4fb --- /dev/null +++ b/lib/led/rk3566-openvfd.js @@ -0,0 +1,301 @@ +'use strict'; + +const fs = require('fs'); +const { execSync } = require('child_process'); +const log = require('../logger'); + +/** + * 前面板指示灯控制 + * + * WiFi 灯 (b5): 1 = 亮, 0 = 灭(正逻辑) + * - WiFi 已连接且互联网畅通 → 常亮 + * - WiFi 连接中(正在尝试) → 闪烁 + * - WiFi 未连接 / 无互联网 → 熄灭 + * + * BT 灯 (b6): 1 = 亮, 0 = 灭(正逻辑) + * - BLE 配网进行中 → 闪烁 + * - BLE 配网成功 → 常亮 + * - 蓝牙不工作 → 熄灭 + * + * SETUP 灯 (b2): 0 = 亮, 1 = 灭(反逻辑,与 APPS 互斥) + * APPS 灯 (b1): 0 = 亮, 1 = 灭(反逻辑,与 SETUP 互斥) + * - claw 未激活 → SETUP 亮,APPS 灭 + * - claw 已激活 → APPS 亮,SETUP 灭 + */ + +const LED_PATH = process.env.CLAWD_LED_PATH || '/sys/devices/platform/openvfd/attr/b5'; +const BT_LED_PATH = process.env.CLAWD_BT_LED_PATH || '/sys/devices/platform/openvfd/attr/b6'; +const SETUP_LED_PATH = '/sys/devices/platform/openvfd/attr/b1'; // 物理 SETUP 灯 +const APPS_LED_PATH = '/sys/devices/platform/openvfd/attr/b2'; // 物理 APPS 灯 +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.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) { + try { + fs.writeFileSync(LED_PATH, String(val)); + } catch (e) { + log.warn('led', `写入失败 (${LED_PATH}): ${e.message}`); + } + } +} + +// ── 蓝牙指示灯 ─────────────────────────────────────────────────────────────── + +/** + * BT 指示灯(b6)正逻辑:1 = 亮,0 = 灭。 + * blink() — BLE 配网进行中 + * on() — BLE 配网成功 / 蓝牙功能正常 + * off() — 蓝牙不工作 + */ +class BtLed { + 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.info('led', 'BT 指示灯 → 常亮'); + } + + /** 熄灭(蓝牙不工作) */ + off() { + if (this._current === 'off') return; + this._stopBlink(); + this._write(0); + this._current = 'off'; + log.info('led', 'BT 指示灯 → 熄灭'); + } + + /** 闪烁(BLE 配网进行中) */ + 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) { + try { + fs.writeFileSync(BT_LED_PATH, String(val)); + } catch (e) { + log.warn('led', `写入失败 (${BT_LED_PATH}): ${e.message}`); + } + } +} + +// ── VFD 显示屏 ──────────────────────────────────────────────────────────────── + +const DISPLAY_PATH = '/sys/devices/platform/openvfd/attr/led'; + +/** + * VFD 显示屏控制。 + * #m3 手动模式,显示指定文字 + * #s1 系统时钟模式,显示当前时间 + */ +class Display { + constructor() { + this._blinkTimer = null; + } + + /** 网络断开 / AP 模式 → 显示 "AP " */ + showAP() { + this._stopBlink(); + this._write('#m3AP '); + log.info('display', '显示屏 → AP'); + } + + /** WS 连接中(失败次数 < 3)→ 显示 "Conn" 闪烁 */ + 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); + } + + /** 网络正常但 VPS 不可达 → 显示 "Err0" */ + showErr0() { + this._stopBlink(); + this._write('#m3Err0'); + log.info('display', '显示屏 → Err0'); + } + + /** 网络已连接 → 显示时间 */ + showTime() { + this._stopBlink(); + this._write('#s1'); + log.info('display', '显示屏 → 时间'); + } + + /** 未激活 + 连网 → 显示 PIN 码(4 位数字)并闪烁 */ + showPin(pin) { + this._stopBlink(); + const s = String(pin || '').padStart(4, '0').slice(-4); + this._write('#m2' + s); + log.info('display', `显示屏 → PIN: ${s}(闪烁)`); + // 亮 1s → 灭 0.5s → 循环 + 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) { + try { + execSync(`echo "${val}" | tee ${DISPLAY_PATH} > /dev/null`, { timeout: 3000 }); + } catch (e) { + log.warn('display', `写入失败: ${e.message}`); + } + } +} + +// ── SETUP / APPS 状态灯 ─────────────────────────────────────────────────────── + +/** + * SETUP 灯(b2)与 APPS 灯(b1)互斥控制。 + * 两灯均为反逻辑:写 0 = 亮,写 1 = 灭。 + */ +class StatusLed { + /** claw 未激活 → SETUP 亮,APPS 灭 */ + setSetup() { + this._write(SETUP_LED_PATH, 0); // SETUP 亮 + this._write(APPS_LED_PATH, 1); // APPS 灭 + log.info('led', '状态灯 → SETUP(未激活)'); + } + + /** claw 已激活 → APPS 亮,SETUP 灭 */ + setApps() { + this._write(SETUP_LED_PATH, 1); // SETUP 灭 + this._write(APPS_LED_PATH, 0); // APPS 亮 + log.info('led', '状态灯 → APPS(已激活)'); + } + + /** 两灯全灭(进程退出时调用) */ + off() { + this._write(SETUP_LED_PATH, 1); + this._write(APPS_LED_PATH, 1); + } + + _write(path, val) { + try { + fs.writeFileSync(path, String(val)); + } catch (e) { + log.warn('led', `写入失败 (${path}): ${e.message}`); + } + } +} + +// ── LAN 指示灯 ─────────────────────────────────────────────────────────────── +// RK3566 这版硬件没有 LAN LED 映射;保留统一 API,避免上层分支判断。 +class LanLed { + start() { + log.debug('led', '[rk3566] LAN LED unsupported; start ignored'); + } + + stop() { + log.debug('led', '[rk3566] LAN LED unsupported; stop ignored'); + } +} + +// 全局单例,整个进程共用 +module.exports = new WifiLed(); +module.exports.bt = new BtLed(); +module.exports.status = new StatusLed(); +module.exports.display = new Display(); +module.exports.lan = new LanLed();