feat: split VFD backends by hardware
This commit is contained in:
395
lib/led.js
395
lib/led.js
@@ -1,356 +1,39 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
const log = require('./logger');
|
||||||
const log = require('./logger');
|
const { isRK3566, readDeviceModel } = require('./led/detect');
|
||||||
const { hasLanCableCarrier } = require('./network');
|
|
||||||
|
function loadImpl() {
|
||||||
/**
|
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
|
||||||
* OpenVFD 图标:/sys/class/leds/openvfd/led_on|led_off(写入图标名)。
|
const model = readDeviceModel();
|
||||||
*
|
|
||||||
* 映射(与面板丝印一致:LAN=play,WiFi=wifi+eth):
|
let name;
|
||||||
* wifi + eth 同亮/同灭 → 产品 WiFi 灯(配网 on/off/blink)
|
if (forced) {
|
||||||
* play → LAN(有线插拔,见 hasLanCableCarrier / CLAWD_ETH_IFACE)
|
name = forced;
|
||||||
* alarm → pwr(SETUP=灭 / APPS=亮)
|
} else if (isRK3566()) {
|
||||||
* BT → 无 sysfs,仅日志
|
name = 'rk3566';
|
||||||
*
|
} else {
|
||||||
* 数码管:vfdservice 管道(660 字节,mode=4 TITLE / mode=1 CLOCK,与板端 Demo 一致)。
|
name = 'openvfd';
|
||||||
* CLAWD_VFD_PIPE 默认 /tmp/openvfd_service;管道不存在时仅 debug,不抛错。
|
}
|
||||||
*
|
|
||||||
* CLAWD_OPENVFD_PATH 默认 /sys/class/leds/openvfd(图标灯)
|
try {
|
||||||
*/
|
if (name === 'rk3566' || name === '3566') {
|
||||||
|
log.info('led', `LED/VFD backend → rk3566-openvfd (${model || 'unknown model'})`);
|
||||||
const BLINK_INTERVAL_MS = 500;
|
return require('./led/rk3566-openvfd');
|
||||||
const LAN_POLL_MS = 500;
|
}
|
||||||
|
if (name === 'noop' || name === 'none' || name === 'off') {
|
||||||
const VFD_BASE = process.env.CLAWD_OPENVFD_PATH || '/sys/class/leds/openvfd';
|
log.info('led', `LED/VFD backend → noop (${model || 'unknown model'})`);
|
||||||
const VFD_PIPE = process.env.CLAWD_VFD_PIPE || '/tmp/openvfd_service';
|
return require('./led/noop');
|
||||||
const VFD_BUF_SIZE = 660;
|
}
|
||||||
const VFD_MODE_CLOCK = 1;
|
if (name !== 'openvfd' && name !== 'default') {
|
||||||
const VFD_MODE_TITLE = 4;
|
log.warn('led', `未知 CLAWD_LED_IMPL=${name},回退 openvfd`);
|
||||||
const VFD_TITLE_OFF = 20;
|
}
|
||||||
|
log.info('led', `LED/VFD backend → openvfd-class (${model || 'unknown model'})`);
|
||||||
function vfdPipePresent() {
|
return require('./led/openvfd-class');
|
||||||
try {
|
} catch (e) {
|
||||||
return fs.existsSync(VFD_PIPE);
|
log.warn('led', `LED/VFD backend ${name} 加载失败:${e.message},回退 noop`);
|
||||||
} catch (_) {
|
return require('./led/noop');
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
module.exports = loadImpl();
|
||||||
/** 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;
|
|
||||||
|
|||||||
22
lib/led/detect.js
Normal file
22
lib/led/detect.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
41
lib/led/noop.js
Normal file
41
lib/led/noop.js
Normal file
@@ -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;
|
||||||
356
lib/led/openvfd-class.js
Normal file
356
lib/led/openvfd-class.js
Normal file
@@ -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;
|
||||||
301
lib/led/rk3566-openvfd.js
Normal file
301
lib/led/rk3566-openvfd.js
Normal file
@@ -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 <text> 手动模式,显示指定文字
|
||||||
|
* #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();
|
||||||
Reference in New Issue
Block a user