diff --git a/install.sh b/install.sh index 1063444..134cac8 100644 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/lib/client.js b/lib/client.js index bbf4a8a..c77a300 100644 --- a/lib/client.js +++ b/lib/client.js @@ -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(); diff --git a/lib/led.js b/lib/led.js index a05d78e..6294594 100644 --- a/lib/led.js +++ b/lib/led.js @@ -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 → pwr(SETUP=灭 / 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] WiFi(wifi+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] alarm(pwr)<= 0'); log.info('led', '状态灯 → SETUP(未激活)'); } setApps() { - this._write('SETUP', 1); - this._write('APPS', 0); + vfdOn('alarm'); + log.debug('led', '[vfd] alarm(pwr)<= 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] 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 = 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;