diff --git a/install.sh b/install.sh index e1caa1d..9fc20aa 100644 --- a/install.sh +++ b/install.sh @@ -172,6 +172,8 @@ CLAWD_LOG_FILE=1 # CLAWD_ENABLE_BT=1 # OpenVFD sysfs 根路径(默认 /sys/class/leds/openvfd) # CLAWD_OPENVFD_PATH=/sys/class/leds/openvfd +# 数码管 vfdservice 管道(默认 /tmp/openvfd_service) +# CLAWD_VFD_PIPE=/tmp/openvfd_service # 多网口/特殊板型可固定 LAN 灯监控的以太网口(默认由 clawd 自动锁定首次 carrier 口) # CLAWD_ETH_IFACE=end0 EOF diff --git a/lib/led.js b/lib/led.js index c07558f..c0a0c45 100644 --- a/lib/led.js +++ b/lib/led.js @@ -13,15 +13,71 @@ const { hasLanCableCarrier } = require('./network'); * alarm → pwr(SETUP=灭 / APPS=亮) * BT → 无 sysfs,仅日志 * - * 数码管(AP/Conn/时间等):仍仅 debug 输出,不接 sysfs。 + * 数码管:vfdservice 管道(660 字节,mode=4 TITLE / mode=1 CLOCK,与板端 Demo 一致)。 + * CLAWD_VFD_PIPE 默认 /tmp/openvfd_service;管道不存在时仅 debug,不抛错。 * - * CLAWD_OPENVFD_PATH 默认 /sys/class/leds/openvfd + * 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 { @@ -166,18 +222,18 @@ class Display { showAP() { this._stopBlink(); - this._write('#m3AP '); + this._pipeTitle('AP ', '#m3AP '); log.info('display', '显示屏 → AP'); } showConn() { this._stopBlink(); - this._write('#m3Conn'); + this._pipeTitle('Conn', '#m3Conn'); log.info('display', '显示屏 → Conn(闪烁)'); let visible = true; const blink = () => { visible = !visible; - this._write(visible ? '#m3Conn' : '#c1'); + this._pipeTitle(visible ? 'Conn' : ' ', visible ? '#m3Conn' : '#c1'); this._blinkTimer = setTimeout(blink, visible ? 1000 : 500); }; this._blinkTimer = setTimeout(blink, 1000); @@ -185,25 +241,26 @@ class Display { showErr0() { this._stopBlink(); - this._write('#m3Err0'); + this._pipeTitle('Err0', '#m3Err0'); log.info('display', '显示屏 → Err0'); } showTime() { this._stopBlink(); - this._write('#s1'); + log.debug('display', '[vfd] #s1'); + writeVfdPipeClock(); log.info('display', '显示屏 → 时间'); } showPin(pin) { this._stopBlink(); const s = String(pin || '').padStart(4, '0').slice(-4); - this._write('#m2' + s); + this._pipeTitle(s, '#m2' + s); log.info('display', `显示屏 → PIN: ${s}(闪烁)`); let visible = true; const blink = () => { visible = !visible; - this._write(visible ? '#m2' + s : '#c1'); + this._pipeTitle(visible ? s : ' ', visible ? '#m2' + s : '#c1'); this._blinkTimer = setTimeout(blink, visible ? 1000 : 500); }; this._blinkTimer = setTimeout(blink, 1000); @@ -217,8 +274,10 @@ class Display { } } - _write(val) { - log.debug('display', `[vfd] ${val}`); + /** 保留原 #m3/#m2/#c1 的 debug 语义,并写 vfdservice TITLE */ + _pipeTitle(fourCharText, debugLegacy) { + log.debug('display', `[vfd] ${debugLegacy}`); + writeVfdPipeTitle(fourCharText); } }