feat(led): OpenVFD sysfs (wifi+eth, alarm, play/lan), skip BT without hci

- Write /sys/class/leds/openvfd led_on/led_off (CLAWD_OPENVFD_PATH)
- WiFi product LED: wifi+eth together; SETUP/APPS -> alarm off/on
- LanLed polls hasWiredCarrier -> play; export led.lan, start/stop from client
- BT monitor only if CLAWD_DISABLE_BT unset and hci* exists (RK3528)
- Display strings remain debug-only (数码管暂不驱动)
- install.sh env template: CLAWD_DISABLE_BT, CLAWD_OPENVFD_PATH comments

Made-with: Cursor
This commit is contained in:
stswangzhiping
2026-03-28 21:35:38 +08:00
parent abd123b3dd
commit 347b19a0c9
3 changed files with 105 additions and 19 deletions

View File

@@ -167,6 +167,10 @@ CLAWD_LOG_LEVEL=info
CLAWD_LOG_FILE=1
# 自定义服务器地址(留空则读 config.json
# CLAWD_SERVER=wss://claw.cutos.ai/ws
# 无蓝牙机型(如部分 RK3528强制不写 bluetoothctl
# CLAWD_DISABLE_BT=1
# OpenVFD sysfs 根路径(默认 /sys/class/leds/openvfd
# CLAWD_OPENVFD_PATH=/sys/class/leds/openvfd
EOF
info "环境变量文件已创建:$ENV_FILE"
fi

View File

@@ -1,5 +1,6 @@
'use strict';
const fs = require('fs');
const { getNotifySocket } = require('./systemd-env');
const WebSocket = require('ws');
const { execSync, execFileSync } = require('child_process');
@@ -20,6 +21,15 @@ const PING_INTERVAL_MS = 15_000;
const HEARTBEAT_INTERVAL_MS = 10_000; // 心跳间隔10 秒,用于快速感知网络状态
const METRICS_EVERY_N = 3; // 每 N 次心跳采集一次指标(= 30 秒)
/** CLAWD_DISABLE_BT=1 或系统无 hci* 时不启动 bluetoothctl 轮询(如 RK3528 无蓝牙) */
function bluetoothAdapterPresent() {
try {
return fs.readdirSync('/sys/class/bluetooth').some((n) => n.startsWith('hci'));
} catch (_) {
return false;
}
}
class ClawClient {
constructor() {
this._cfg = config.load();
@@ -165,6 +175,7 @@ class ClawClient {
this._clearHeartbeat();
this._clearPing();
if (this._sdTimer) { clearInterval(this._sdTimer); this._sdTimer = null; }
led.lan.stop();
if (this._btMonitor) { this._btMonitor.stop(); this._btMonitor = null; }
if (this._provisionMgr) { this._provisionMgr.stop(); this._provisionMgr = null; }
this._frpc.stop();

View File

@@ -1,19 +1,53 @@
'use strict';
const fs = require('fs');
const log = require('./logger');
const { hasWiredCarrier } = require('./network');
/**
* 前面板指示灯 / VFD 逻辑(原 openvfd sysfs)。
* 硬件路径因板型而异,当前不直接写 sysfs仅在 debug 记录拟写入内容,业务状态仍以 info 输出。
* OpenVFD 图标:/sys/class/leds/openvfd/led_on|led_off写入图标名)。
*
* WiFi 灯 (b5): 1 = 亮, 0 = 灭(正逻辑)
* BT 灯 (b6): 1 = 亮, 0 = 灭(正逻辑)
* SETUP/APPS (b1/b2): 反逻辑,与 claw 激活状态互斥
* 映射:
* wifi + eth 同亮/同灭 → 产品 WiFi 灯
* alarm → pwrSETUP=灭 / APPS=亮)
* play → lan有线 carrier
* BT → 无 sysfs仅日志
*
* 恢复真机显示时:可在此类 _write 中接新驱动或 CLAWD_* 路径
* 数码管AP/Conn/时间等):仍仅 debug 输出,不接 sysfs
*
* CLAWD_OPENVFD_PATH 默认 /sys/class/leds/openvfd
*/
const BLINK_INTERVAL_MS = 500;
const LAN_POLL_MS = 3000;
const VFD_BASE = process.env.CLAWD_OPENVFD_PATH || '/sys/class/leds/openvfd';
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() {
@@ -65,7 +99,9 @@ class WifiLed {
}
_write(val) {
log.debug('led', `[vfd] WiFi LED (b5) <= ${val}`);
const on = !!val;
log.debug('led', `[vfd] WiFiwifi+eth<= ${on ? 1 : 0}`);
vfdWifiPair(on);
}
}
@@ -118,8 +154,8 @@ class BtLed {
}
}
_write(val) {
log.debug('led', `[vfd] BT LED (b6) <= ${val}`);
_write(_val) {
log.debug('led', '[vfd] BT 无 OpenVFD 映射,忽略');
}
}
@@ -188,28 +224,63 @@ class Display {
class StatusLed {
setSetup() {
this._write('SETUP', 0);
this._write('APPS', 1);
vfdOff('alarm');
log.debug('led', '[vfd] alarmpwr<= 0');
log.info('led', '状态灯 → SETUP未激活');
}
setApps() {
this._write('SETUP', 1);
this._write('APPS', 0);
vfdOn('alarm');
log.debug('led', '[vfd] alarmpwr<= 1');
log.info('led', '状态灯 → APPS已激活');
}
off() {
this._write('SETUP', 1);
this._write('APPS', 1);
}
_write(which, val) {
log.debug('led', `[vfd] 状态灯 ${which} (b1/b2 反逻辑) <= ${val}`);
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 = hasWiredCarrier();
if (up) {
if (this._current !== 'on') {
vfdOn('play');
this._current = 'on';
log.info('led', 'LAN有线 carrier→ 亮');
}
} else if (this._current !== 'off') {
vfdOff('play');
this._current = 'off';
log.info('led', 'LAN有线 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;