Compare commits
10 Commits
d9f826f978
...
8518232978
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8518232978 | ||
|
|
03dc7c2527 | ||
|
|
c9ce87c93a | ||
|
|
a1c9cc9657 | ||
|
|
f6aad310a8 | ||
|
|
18bea4ae38 | ||
|
|
4cf0e4e948 | ||
|
|
c64aeab3b2 | ||
|
|
8f6e7c55e9 | ||
|
|
3d2e5d477a |
0
bin/clawd.js
Normal file → Executable file
0
bin/clawd.js
Normal file → Executable file
@@ -11,8 +11,8 @@ const { collect } = require('./metrics');
|
|||||||
const { getDashboardInfo, resolveOpenclawConfigFile, startTtyd, FrpcManager } = require('./frpc'); // getDashboardInfo 也用于心跳中定期刷新
|
const { getDashboardInfo, resolveOpenclawConfigFile, startTtyd, FrpcManager } = require('./frpc'); // getDashboardInfo 也用于心跳中定期刷新
|
||||||
const { ProvisionManager } = require('./provisioning');
|
const { ProvisionManager } = require('./provisioning');
|
||||||
const { BtMonitor } = require('./bt-monitor');
|
const { BtMonitor } = require('./bt-monitor');
|
||||||
const { hasInternet, hasWiredInternetProbe, getLocalIps } = require('./network');
|
const { hasInternet, hasWiredInternetProbe, getLocalIps, getLocalNetworks } = require('./network');
|
||||||
const { applyFullProviderFromVps, removeProviderByName, isFullProvider } = require('./openclaw-provider');
|
const { applyFullProviderFromVps, removeProviderByName, refreshModelsIfChanged, isFullProvider } = require('./openclaw-provider');
|
||||||
const led = require('./led');
|
const led = require('./led');
|
||||||
|
|
||||||
const MAX_BACKOFF_MS = 60_000;
|
const MAX_BACKOFF_MS = 60_000;
|
||||||
@@ -371,8 +371,9 @@ class ClawClient {
|
|||||||
box_id: this._boxId,
|
box_id: this._boxId,
|
||||||
claw_id: this._cfg.claw_id ?? null,
|
claw_id: this._cfg.claw_id ?? null,
|
||||||
token: this._cfg.token ?? null,
|
token: this._cfg.token ?? null,
|
||||||
local_ip: getLocalIps(),
|
local_ip: getLocalIps(),
|
||||||
external_ip: this._externalIp ?? null,
|
local_networks: getLocalNetworks(),
|
||||||
|
external_ip: this._externalIp ?? null,
|
||||||
location: this._location ?? null,
|
location: this._location ?? null,
|
||||||
...this._dashInfo,
|
...this._dashInfo,
|
||||||
};
|
};
|
||||||
@@ -469,7 +470,10 @@ class ClawClient {
|
|||||||
this._updateOpenClawOrigin(clawIdStr);
|
this._updateOpenClawOrigin(clawIdStr);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._updateOpenClawOrigin(clawIdStr);
|
// 重连场景:检查模型列表是否有变化,有变化才写盘
|
||||||
|
refreshModelsIfChanged(() => {
|
||||||
|
this._updateOpenClawOrigin(clawIdStr);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -592,8 +596,10 @@ class ClawClient {
|
|||||||
// 每 METRICS_EVERY_N 次心跳(30 秒)采集一次指标,其余发轻量心跳
|
// 每 METRICS_EVERY_N 次心跳(30 秒)采集一次指标,其余发轻量心跳
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'heartbeat',
|
type: 'heartbeat',
|
||||||
claw_id: this._cfg.claw_id,
|
claw_id: this._cfg.claw_id,
|
||||||
token: this._cfg.token,
|
token: this._cfg.token,
|
||||||
|
local_ip: getLocalIps(),
|
||||||
|
local_networks: getLocalNetworks(),
|
||||||
...this._dashInfo,
|
...this._dashInfo,
|
||||||
};
|
};
|
||||||
if (this._hbCount % METRICS_EVERY_N === 0) {
|
if (this._hbCount % METRICS_EVERY_N === 0) {
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ function downloadFile(url, dest) {
|
|||||||
function writeFrpcConfig(clawId, frpConfig) {
|
function writeFrpcConfig(clawId, frpConfig) {
|
||||||
const { auth_token, dashboard_local_port = 18789 } = frpConfig;
|
const { auth_token, dashboard_local_port = 18789 } = frpConfig;
|
||||||
const ttyRemotePort = 10000 + Number(clawId);
|
const ttyRemotePort = 10000 + Number(clawId);
|
||||||
// 固定使用 WebSocket over HTTPS (443),可穿透仅开放 443 的网络环境;
|
|
||||||
// 旧版 clawd 仍使用后端下发的 server:7000(TCP),两者并存互不影响。
|
|
||||||
const toml = `# 由 clawd 自动生成,请勿手动修改
|
const toml = `# 由 clawd 自动生成,请勿手动修改
|
||||||
serverAddr = "frp.claw.cutos.ai"
|
serverAddr = "frp.claw.cutos.ai"
|
||||||
serverPort = 443
|
serverPort = 443
|
||||||
@@ -157,7 +155,6 @@ method = "token"
|
|||||||
token = "${auth_token}"
|
token = "${auth_token}"
|
||||||
|
|
||||||
[transport]
|
[transport]
|
||||||
protocol = "websocket"
|
|
||||||
tls.enable = true
|
tls.enable = true
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
|
|||||||
367
lib/led.js
367
lib/led.js
@@ -1,356 +1,39 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { hasLanCableCarrier } = require('./network');
|
const { isRK3566, readDeviceModel } = require('./led/detect');
|
||||||
|
|
||||||
/**
|
function loadImpl() {
|
||||||
* OpenVFD 图标:/sys/class/leds/openvfd/led_on|led_off(写入图标名)。
|
const forced = String(process.env.CLAWD_LED_IMPL || '').trim().toLowerCase();
|
||||||
*
|
const model = readDeviceModel();
|
||||||
* 映射(与面板丝印一致: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;
|
let name;
|
||||||
const LAN_POLL_MS = 500;
|
if (forced) {
|
||||||
|
name = forced;
|
||||||
const VFD_BASE = process.env.CLAWD_OPENVFD_PATH || '/sys/class/leds/openvfd';
|
} else if (isRK3566()) {
|
||||||
const VFD_PIPE = process.env.CLAWD_VFD_PIPE || '/tmp/openvfd_service';
|
name = 'rk3566';
|
||||||
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 {
|
} else {
|
||||||
vfdOff('wifi');
|
name = 'openvfd';
|
||||||
vfdOff('eth');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WifiLed {
|
|
||||||
constructor() {
|
|
||||||
this._blinkTimer = null;
|
|
||||||
this._blinkState = false;
|
|
||||||
this._current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on() {
|
try {
|
||||||
if (this._current === 'on') return;
|
if (name === 'rk3566' || name === '3566') {
|
||||||
this._stopBlink();
|
log.info('led', `LED/VFD backend → rk3566-openvfd (${model || 'unknown model'})`);
|
||||||
this._write(1);
|
return require('./led/rk3566-openvfd');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
if (name === 'noop' || name === 'none' || name === 'off') {
|
||||||
|
log.info('led', `LED/VFD backend → noop (${model || 'unknown model'})`);
|
||||||
_write(val) {
|
return require('./led/noop');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
if (name !== 'openvfd' && name !== 'default') {
|
||||||
|
log.warn('led', `未知 CLAWD_LED_IMPL=${name},回退 openvfd`);
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
}
|
log.info('led', `LED/VFD backend → openvfd-class (${model || 'unknown model'})`);
|
||||||
|
return require('./led/openvfd-class');
|
||||||
/** 保留原 #m3/#m2/#c1 的 debug 语义,并写 vfdservice TITLE */
|
} catch (e) {
|
||||||
_pipeTitle(fourCharText, debugLegacy) {
|
log.warn('led', `LED/VFD backend ${name} 加载失败:${e.message},回退 noop`);
|
||||||
log.debug('display', `[vfd] ${debugLegacy}`);
|
return require('./led/noop');
|
||||||
writeVfdPipeTitle(fourCharText);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusLed {
|
module.exports = loadImpl();
|
||||||
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();
|
||||||
189
lib/network.js
189
lib/network.js
@@ -180,19 +180,24 @@ function scanWifi() {
|
|||||||
// 等扫描完成
|
// 等扫描完成
|
||||||
sleep(2000);
|
sleep(2000);
|
||||||
|
|
||||||
const out = run('nmcli -t -f SSID,SIGNAL,SECURITY device wifi list');
|
// 指定 ifname,避免 AP/多网卡场景下读取到非目标接口或旧缓存;带回频率便于诊断 2.4G/5G。
|
||||||
|
const out = run(`nmcli -t -f SSID,SIGNAL,SECURITY,FREQ device wifi list ifname ${iface}`);
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const line of out.split('\n')) {
|
for (const line of out.split('\n')) {
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
const parts = line.split(':');
|
const parts = _parseNmcliTerseLine(line);
|
||||||
const ssid = parts[0].trim().replace(/\\:/g, ':');
|
const ssid = (parts[0] || '').trim();
|
||||||
if (!ssid || seen.has(ssid)) continue;
|
if (!ssid || seen.has(ssid)) continue;
|
||||||
seen.add(ssid);
|
seen.add(ssid);
|
||||||
|
const freq = (parts[3] || '').trim();
|
||||||
|
const freqMhz = parseInt(freq, 10) || null;
|
||||||
results.push({
|
results.push({
|
||||||
ssid,
|
ssid,
|
||||||
signal: parseInt(parts[1], 10) || 0,
|
signal: parseInt(parts[1], 10) || 0,
|
||||||
security: parts.slice(2).join(':').trim() || 'Open',
|
security: (parts[2] || '').trim() || 'Open',
|
||||||
|
freq,
|
||||||
|
band: freqMhz ? (freqMhz >= 4900 ? '5G' : '2.4G') : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
results.sort((a, b) => b.signal - a.signal);
|
results.sort((a, b) => b.signal - a.signal);
|
||||||
@@ -275,6 +280,7 @@ async function connectWifi(ssid, password) {
|
|||||||
if (password) args.push('password', password);
|
if (password) args.push('password', password);
|
||||||
args.push('ifname', iface);
|
args.push('ifname', iface);
|
||||||
await nmcliAsync(args, 120000);
|
await nmcliAsync(args, 120000);
|
||||||
|
await _ensureActiveWifiAutoconnect();
|
||||||
|
|
||||||
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
|
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
@@ -324,6 +330,9 @@ function startAP(clawId) {
|
|||||||
cmd.push(`password "${AP_PASSWORD}"`);
|
cmd.push(`password "${AP_PASSWORD}"`);
|
||||||
}
|
}
|
||||||
run(cmd.join(' '));
|
run(cmd.join(' '));
|
||||||
|
try {
|
||||||
|
nmcliSync(['connection', 'modify', CON_NAME, 'connection.autoconnect', 'no'], 8000);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
// 等待 AP 启动
|
// 等待 AP 启动
|
||||||
sleep(2000);
|
sleep(2000);
|
||||||
@@ -361,20 +370,120 @@ function sleep(ms) {
|
|||||||
execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 });
|
execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _parseNmcliTerseLine(line) {
|
||||||
|
const fields = [];
|
||||||
|
let cur = '';
|
||||||
|
let escaped = false;
|
||||||
|
for (const ch of line) {
|
||||||
|
if (escaped) {
|
||||||
|
cur += ch;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '\\') {
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === ':') {
|
||||||
|
fields.push(cur);
|
||||||
|
cur = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur += ch;
|
||||||
|
}
|
||||||
|
fields.push(cur);
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出已保存的 WiFi STA 连接(排除自身热点),按 autoconnect-priority 从高到低排序。
|
||||||
|
*/
|
||||||
|
function listSavedWifiConnections() {
|
||||||
|
const profiles = [];
|
||||||
|
try {
|
||||||
|
const out = run('nmcli -t -f NAME,UUID,TYPE,AUTOCONNECT,AUTOCONNECT-PRIORITY connection show');
|
||||||
|
for (const line of out.split('\n')) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
const [name, uuid, type, autoconnect, priority] = _parseNmcliTerseLine(line);
|
||||||
|
if (type !== '802-11-wireless' || name === CON_NAME) continue;
|
||||||
|
profiles.push({
|
||||||
|
name,
|
||||||
|
uuid,
|
||||||
|
autoconnect: autoconnect === 'yes',
|
||||||
|
priority: parseInt(priority, 10) || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
profiles.sort((a, b) => {
|
||||||
|
if (b.priority !== a.priority) return b.priority - a.priority;
|
||||||
|
if (a.autoconnect !== b.autoconnect) return a.autoconnect ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测是否有已保存的 WiFi STA 连接(排除自身热点)
|
* 检测是否有已保存的 WiFi STA 连接(排除自身热点)
|
||||||
*/
|
*/
|
||||||
function hasSavedWifiConnection() {
|
function hasSavedWifiConnection() {
|
||||||
|
return listSavedWifiConnections().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWifiActiveConnectionName() {
|
||||||
|
const iface = getWifiIface();
|
||||||
try {
|
try {
|
||||||
const out = run('nmcli -t -f NAME,TYPE connection show');
|
const conn = nmcliSync(['-g', 'GENERAL.CONNECTION', 'device', 'show', iface], 8000).trim();
|
||||||
for (const line of out.split('\n')) {
|
return conn && conn !== '--' ? conn : null;
|
||||||
const [name, type] = line.split(':');
|
} catch (_) {
|
||||||
if (type === '802-11-wireless' && name !== CON_NAME) {
|
return null;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async function _ensureActiveWifiAutoconnect() {
|
||||||
|
const conn = getWifiActiveConnectionName();
|
||||||
|
if (!conn || conn === CON_NAME) return;
|
||||||
|
try {
|
||||||
|
await nmcliAsync(['connection', 'modify', conn, 'connection.autoconnect', 'yes'], 15000);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('network', `设置 WiFi 自动连接失败: ${conn}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动让 NetworkManager 尝试已保存 WiFi。
|
||||||
|
* clawd 只做调度;真正的认证、DHCP、重连细节仍交给 NM。
|
||||||
|
*/
|
||||||
|
async function connectSavedWifiConnections() {
|
||||||
|
const iface = getWifiIface();
|
||||||
|
const profiles = listSavedWifiConnections();
|
||||||
|
if (profiles.length === 0) {
|
||||||
|
return { success: false, error: '没有已保存的 WiFi 配置' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await nmcliAsync(['device', 'set', iface, 'managed', 'yes'], 8000);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return false;
|
|
||||||
|
let lastError = '';
|
||||||
|
for (const profile of profiles) {
|
||||||
|
const label = profile.name || profile.uuid;
|
||||||
|
try {
|
||||||
|
log.info('network', `尝试连接已保存 WiFi: ${label}(ifname=${iface})`);
|
||||||
|
const idArgs = profile.uuid ? ['uuid', profile.uuid] : ['id', profile.name];
|
||||||
|
await nmcliAsync(['connection', 'up', ...idArgs, 'ifname', iface], 90000);
|
||||||
|
if (isWifiStaConnected()) {
|
||||||
|
await _ensureActiveWifiAutoconnect();
|
||||||
|
log.info('network', `已保存 WiFi 连接成功: ${label}`);
|
||||||
|
return { success: true, profile };
|
||||||
|
}
|
||||||
|
lastError = '连接命令完成但网卡未进入 STA connected 状态';
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e.message;
|
||||||
|
log.warn('network', `已保存 WiFi 连接失败: ${label}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: lastError || '所有已保存 WiFi 均连接失败' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,22 +507,37 @@ function isWifiStaConnected() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _ifaceNetworkType(name) {
|
||||||
|
const wifi = getWifiIface();
|
||||||
|
if (name === wifi || name.startsWith('wl')) return 'wifi';
|
||||||
|
if (name === DEFAULT_ETH_IFACE || name.startsWith('en') || name.startsWith('eth')) return 'lan';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _localNetworkEntries() {
|
||||||
|
const ifaces = os.networkInterfaces();
|
||||||
|
const entries = [];
|
||||||
|
for (const [name, addrs] of Object.entries(ifaces)) {
|
||||||
|
if (!addrs) continue;
|
||||||
|
const type = _ifaceNetworkType(name);
|
||||||
|
if (!type) continue;
|
||||||
|
for (const addr of addrs) {
|
||||||
|
if (addr.family !== 'IPv4' || addr.internal) continue;
|
||||||
|
// clawd-hotspot 的 AP 管理网段只用于配网,不上报为 BOX 可访问地址。
|
||||||
|
if (addr.address.startsWith('10.42.')) continue;
|
||||||
|
entries.push({ ip: addr.address, type, iface: name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取本机所有非回环 IPv4 地址,逗号拼接返回
|
* 获取本机所有非回环 IPv4 地址,逗号拼接返回。
|
||||||
* 例:'192.168.1.100' 或 '192.168.1.100,10.0.0.5'
|
* 保持旧协议字段 local_ip 兼容:'192.168.1.100' 或 '192.168.1.100,10.0.0.5'。
|
||||||
*/
|
*/
|
||||||
function getLocalIps() {
|
function getLocalIps() {
|
||||||
try {
|
try {
|
||||||
const ifaces = os.networkInterfaces();
|
const ips = _localNetworkEntries().map((entry) => entry.ip);
|
||||||
const ips = [];
|
|
||||||
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
||||||
if (!addrs) continue;
|
|
||||||
for (const addr of addrs) {
|
|
||||||
if (addr.family === 'IPv4' && !addr.internal && !addr.address.startsWith('10.42.')) {
|
|
||||||
ips.push(addr.address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ips.length > 0 ? ips.join(',') : null;
|
return ips.length > 0 ? ips.join(',') : null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.warn('network', '获取本机 IP 失败:', e.message);
|
log.warn('network', '获取本机 IP 失败:', e.message);
|
||||||
@@ -421,13 +545,29 @@ function getLocalIps() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本机 IPv4 地址及网络类型,用于上报服务器。
|
||||||
|
* 例:[{ ip: '192.168.1.100', type: 'wifi', iface: 'wlan0' }]
|
||||||
|
*/
|
||||||
|
function getLocalNetworks() {
|
||||||
|
try {
|
||||||
|
const entries = _localNetworkEntries();
|
||||||
|
return entries.length > 0 ? entries : null;
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('network', '获取本机网络类型失败:', e.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hasInternet,
|
hasInternet,
|
||||||
hasWiredCarrier,
|
hasWiredCarrier,
|
||||||
hasLanCableCarrier,
|
hasLanCableCarrier,
|
||||||
hasWiredInternetProbe,
|
hasWiredInternetProbe,
|
||||||
getWiredIfaceWithCarrier,
|
getWiredIfaceWithCarrier,
|
||||||
|
listSavedWifiConnections,
|
||||||
hasSavedWifiConnection,
|
hasSavedWifiConnection,
|
||||||
|
connectSavedWifiConnections,
|
||||||
isWifiStaConnected,
|
isWifiStaConnected,
|
||||||
getWifiIface,
|
getWifiIface,
|
||||||
scanWifi,
|
scanWifi,
|
||||||
@@ -436,4 +576,5 @@ module.exports = {
|
|||||||
stopAP,
|
stopAP,
|
||||||
AP_IP,
|
AP_IP,
|
||||||
getLocalIps,
|
getLocalIps,
|
||||||
|
getLocalNetworks,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const crypto = require('crypto');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { resolveOpenclawConfigFile } = require('./frpc');
|
const { resolveOpenclawConfigFile } = require('./frpc');
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ function fetchModels(baseUrl, apiKey, callback) {
|
|||||||
try {
|
try {
|
||||||
const json = JSON.parse(data);
|
const json = JSON.parse(data);
|
||||||
if (json.data && Array.isArray(json.data)) {
|
if (json.data && Array.isArray(json.data)) {
|
||||||
callback(null, json.data.map((m) => ({ id: m.id, name: m.id })));
|
callback(null, json.data.map((m) => ({ id: m.id, name: m.id, input: ['text', 'image'] })));
|
||||||
} else if (json.error) {
|
} else if (json.error) {
|
||||||
callback(new Error(json.error.message || JSON.stringify(json.error)));
|
callback(new Error(json.error.message || JSON.stringify(json.error)));
|
||||||
} else {
|
} else {
|
||||||
@@ -223,6 +224,21 @@ function applyFullProviderFromVps(provider, onDone) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
log.warn('openclaw-provider', `拉模型失败,使用空列表: ${err.message}`);
|
log.warn('openclaw-provider', `拉模型失败,使用空列表: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验 apiKey + 模型列表是否有实际变化,无变化则跳过写盘,避免触发不必要的 gateway 重启
|
||||||
|
try {
|
||||||
|
const existing = readJsonFile(configFile);
|
||||||
|
const cur = existing.models?.providers?.[name] || {};
|
||||||
|
const curApiKey = cur.apiKey || '';
|
||||||
|
const curMd5 = computeModelsMd5(cur.models || []);
|
||||||
|
const newMd5 = computeModelsMd5(list);
|
||||||
|
if (curApiKey === apiKey && curMd5 === newMd5) {
|
||||||
|
log.info('openclaw-provider', `provider 无变化(apiKey + 模型列表相同),跳过写盘`);
|
||||||
|
if (typeof onDone === 'function') { try { onDone(); } catch (e) { log.warn('openclaw-provider', `onDone: ${e.message}`); } }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) { /* 读取失败则继续写盘 */ }
|
||||||
|
|
||||||
addProviderSync(configFile, name, baseUrl, apiKey, list, defaultModel);
|
addProviderSync(configFile, name, baseUrl, apiKey, list, defaultModel);
|
||||||
if (typeof onDone === 'function') {
|
if (typeof onDone === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -246,9 +262,86 @@ function isFullProvider(p) {
|
|||||||
|| Object.prototype.hasOwnProperty.call(p, 'baseUrl');
|
|| Object.prototype.hasOwnProperty.call(p, 'baseUrl');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对模型列表计算 MD5(按 id 排序后 JSON 序列化),用于变更检测。
|
||||||
|
*/
|
||||||
|
function computeModelsMd5(models) {
|
||||||
|
const ids = (models || []).map((m) => m.id).sort();
|
||||||
|
return crypto.createHash('md5').update(JSON.stringify(ids)).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重连时刷新模型列表:读取现有 openclaw.json 中第一个 provider 的 baseUrl/apiKey,
|
||||||
|
* 拉取最新模型,MD5 与现有模型对比,不一致才写盘(触发 gateway 自动重启)。
|
||||||
|
* 若模型未变则跳过,不写盘,不触发 gateway 重启。
|
||||||
|
* 完成后调用 onDone()(无论是否更新)。
|
||||||
|
*/
|
||||||
|
function refreshModelsIfChanged(onDone) {
|
||||||
|
if (_busy) {
|
||||||
|
log.info('openclaw-provider', 'refreshModels: 有操作进行中,跳过');
|
||||||
|
if (typeof onDone === 'function') onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configFile = resolveOpenclawConfigFile();
|
||||||
|
if (!configFile) {
|
||||||
|
if (typeof onDone === 'function') onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = readJsonFile(configFile);
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('openclaw-provider', `refreshModels: 读取配置失败: ${e.message}`);
|
||||||
|
if (typeof onDone === 'function') onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = config.models?.providers || {};
|
||||||
|
const providerId = Object.keys(providers)[0];
|
||||||
|
if (!providerId) {
|
||||||
|
log.info('openclaw-provider', 'refreshModels: 未找到已配置的 provider,跳过');
|
||||||
|
if (typeof onDone === 'function') onDone();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerCfg = providers[providerId];
|
||||||
|
const baseUrl = providerCfg.baseUrl || '';
|
||||||
|
const apiKey = providerCfg.apiKey || '';
|
||||||
|
const currentModels = providerCfg.models || [];
|
||||||
|
|
||||||
|
_busy = true;
|
||||||
|
fetchModels(baseUrl, apiKey, (err, newModels) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
log.warn('openclaw-provider', `refreshModels: 拉模型失败: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMd5 = computeModelsMd5(currentModels);
|
||||||
|
const newMd5 = computeModelsMd5(newModels);
|
||||||
|
|
||||||
|
if (currentMd5 === newMd5) {
|
||||||
|
log.info('openclaw-provider', `模型列表未变化(${newModels.length} 个),跳过更新`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('openclaw-provider', `模型列表已变化(${currentModels.length} → ${newModels.length} 个),更新 openclaw.json`);
|
||||||
|
addProviderSync(configFile, providerId, baseUrl, apiKey, newModels, null);
|
||||||
|
} catch (e) {
|
||||||
|
log.error('openclaw-provider', `refreshModels: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
_busy = false;
|
||||||
|
if (typeof onDone === 'function') onDone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
applyFullProviderFromVps,
|
applyFullProviderFromVps,
|
||||||
removeProviderByName,
|
removeProviderByName,
|
||||||
|
refreshModelsIfChanged,
|
||||||
isFullProvider,
|
isFullProvider,
|
||||||
DEFAULT_BASE_URL,
|
DEFAULT_BASE_URL,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,33 +2,38 @@
|
|||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { hasInternet, hasSavedWifiConnection, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
|
const { hasInternet, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
|
||||||
const { DnsHijack } = require('./dns-hijack');
|
const { DnsHijack } = require('./dns-hijack');
|
||||||
const { CaptiveServer } = require('./captive-server');
|
const { CaptiveServer } = require('./captive-server');
|
||||||
const led = require('./led');
|
const led = require('./led');
|
||||||
|
|
||||||
const MONITOR_INTERVAL_MS = 15_000;
|
const MONITOR_INTERVAL_MS = 15_000;
|
||||||
const BOOT_WAIT_MAX_MS = 20_000; // 等待 NM 自动连接的最大时间
|
const WIFI_RECONNECT_MAX_ROUNDS = 3;
|
||||||
const BOOT_POLL_MS = 2_000; // 轮询间隔
|
const WIFI_RECONNECT_ROUND_DELAY_MS = 5_000;
|
||||||
|
const AP_SAVED_WIFI_RETRY_INTERVAL_MS = 180_000;
|
||||||
|
const AP_MIN_UP_BEFORE_RETRY_MS = 60_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AP 常驻配网管理器。
|
* AP 常驻配网管理器。
|
||||||
*
|
*
|
||||||
* 规则:
|
* 规则:
|
||||||
* - 启动时:有已保存 WiFi 配置 → 等 NM 自动连接(最多 20 秒)
|
* - 启动时:WiFi STA 优先;有已保存 WiFi 时主动让 NM 重连,最多 3 轮
|
||||||
* - wlan0 没有以 STA 模式连接 WiFi → 开 AP + DNS 劫持 + HTTP 配网页
|
* - 有线网络可用时:通知网络就绪,但不自动开启 AP
|
||||||
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则重新开 AP
|
* - 自动开 AP 的唯一兜底:无有线/无 WiFi,且无 saved WiFi 或 saved WiFi 3 轮失败
|
||||||
* - 运行中 WiFi 断开 → 自动重新开 AP
|
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则按网络状态决定是否重新开 AP
|
||||||
* - WiFi 已连接 → AP 关闭
|
* - AP 状态下:若仍无有线网络,低频释放 wlan0 并尝试 saved WiFi
|
||||||
*/
|
*/
|
||||||
class ProvisionManager extends EventEmitter {
|
class ProvisionManager extends EventEmitter {
|
||||||
constructor(clawId) {
|
constructor(clawId) {
|
||||||
super();
|
super();
|
||||||
this._clawId = clawId || 'Setup';
|
this._clawId = clawId || 'Setup';
|
||||||
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta'
|
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta' | 'wired'
|
||||||
this._dns = null;
|
this._dns = null;
|
||||||
this._server = null;
|
this._server = null;
|
||||||
this._monitorTimer = null;
|
this._monitorTimer = null;
|
||||||
|
this._monitorBusy = false;
|
||||||
|
this._apStartedAt = 0;
|
||||||
|
this._lastApSavedWifiRetryAt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否正处于 AP 模式(WiFi 热点广播中) */
|
/** 是否正处于 AP 模式(WiFi 热点广播中) */
|
||||||
@@ -46,39 +51,41 @@ class ProvisionManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 有线有网 → 立即通知 WS 连接,后台异步设置 AP 供 WiFi 配网
|
// 网络已就绪时先启动 WS;hasInternet() 可能来自 WiFi,也可能来自有线,不能直接当作 wired。
|
||||||
if (hasInternet()) {
|
if (hasInternet()) {
|
||||||
log.info('provision', '有线网络就绪,立即启动 WS,AP 后台准备中...');
|
if (isWifiStaConnected()) {
|
||||||
this._emitNetworkReady();
|
this._state = 'sta';
|
||||||
setTimeout(() => {
|
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||||
this._enterAP();
|
this._emitNetworkReady();
|
||||||
this._startMonitor();
|
} else {
|
||||||
}, 0);
|
this._state = 'wired';
|
||||||
|
log.info('provision', '有线网络就绪,启动 WS;不自动开启 AP');
|
||||||
|
led.off();
|
||||||
|
this._emitNetworkReady();
|
||||||
|
}
|
||||||
|
this._startMonitor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无网:有已保存的 WiFi 配置 → 等 NM 自动连接(重启场景)
|
// 无有线可用时,有 saved WiFi 才主动让 NetworkManager 重连;不要只被动等待 NM autoconnect。
|
||||||
if (hasSavedWifiConnection()) {
|
if (hasSavedWifiConnection()) {
|
||||||
log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...');
|
log.info('provision', `发现已保存的 WiFi 配置,主动重连(最多 ${WIFI_RECONNECT_MAX_ROUNDS} 轮)...`);
|
||||||
led.blink(); // WiFi 灯:等待自动重连期间闪烁
|
this._state = 'connecting';
|
||||||
const connected = await this._waitForWifiConnect();
|
led.blink();
|
||||||
|
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||||
if (connected) {
|
if (connected) {
|
||||||
this._state = 'sta';
|
this._state = 'sta';
|
||||||
log.info('provision', 'WiFi 自动连接成功,AP 不启动');
|
log.info('provision', '已保存 WiFi 重连成功,AP 不启动');
|
||||||
this._emitNetworkReady();
|
this._emitNetworkReady();
|
||||||
this._startMonitor();
|
this._startMonitor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.warn('provision', 'WiFi 自动连接超时,启动 AP');
|
log.warn('provision', '已保存 WiFi 重连失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有已保存 WiFi 或等待超时 → 开 AP
|
// 无有线、无 WiFi;且无 saved WiFi 或 saved WiFi 3 轮失败 → 开 AP 兜底配网。
|
||||||
this._enterAP();
|
this._enterAP();
|
||||||
this._startMonitor();
|
this._startMonitor();
|
||||||
|
|
||||||
if (hasInternet()) {
|
|
||||||
this._emitNetworkReady();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_emitNetworkReady() {
|
_emitNetworkReady() {
|
||||||
@@ -91,23 +98,17 @@ class ProvisionManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async _trySavedWifiReconnectRounds(rounds = WIFI_RECONNECT_MAX_ROUNDS) {
|
||||||
* 轮询等待 NM 自动连接 WiFi,最多等 BOOT_WAIT_MAX_MS
|
for (let i = 1; i <= rounds; i++) {
|
||||||
*/
|
if (isWifiStaConnected()) return true;
|
||||||
_waitForWifiConnect() {
|
log.info('provision', `尝试已保存 WiFi 重连:第 ${i}/${rounds} 轮`);
|
||||||
return new Promise(resolve => {
|
const result = await connectSavedWifiConnections();
|
||||||
let elapsed = 0;
|
if (result.success || isWifiStaConnected()) return true;
|
||||||
const timer = setInterval(() => {
|
if (i < rounds) {
|
||||||
elapsed += BOOT_POLL_MS;
|
await new Promise((r) => setTimeout(r, WIFI_RECONNECT_ROUND_DELAY_MS));
|
||||||
if (isWifiStaConnected()) {
|
}
|
||||||
clearInterval(timer);
|
}
|
||||||
resolve(true);
|
return false;
|
||||||
} else if (elapsed >= BOOT_WAIT_MAX_MS) {
|
|
||||||
clearInterval(timer);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
}, BOOT_POLL_MS);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
@@ -126,10 +127,13 @@ class ProvisionManager extends EventEmitter {
|
|||||||
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP,有线时等 WS 连接后再定
|
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP,有线时等 WS 连接后再定
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 若上次进程退出前留下 clawd-hotspot,必须先释放 wlan0;否则会在 AP 模式下扫描,列表可能只剩 2.4G/自身热点。
|
||||||
|
stopAP();
|
||||||
|
|
||||||
// AP 模式下无法扫描 WiFi,必须在开 AP 之前扫描并缓存
|
// AP 模式下无法扫描 WiFi,必须在开 AP 之前扫描并缓存
|
||||||
log.info('provision', '扫描周边 WiFi...');
|
log.info('provision', '扫描周边 WiFi...');
|
||||||
this._cachedWifiList = scanWifi();
|
this._cachedWifiList = scanWifi();
|
||||||
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络`);
|
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络: ${this._cachedWifiList.map(w => `${w.ssid}${w.band ? `(${w.band})` : ''}`).join(', ')}`);
|
||||||
|
|
||||||
// 写 DNS 劫持配置(NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
|
// 写 DNS 劫持配置(NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
|
||||||
this._dns = new DnsHijack();
|
this._dns = new DnsHijack();
|
||||||
@@ -145,6 +149,8 @@ class ProvisionManager extends EventEmitter {
|
|||||||
this._server.startListening();
|
this._server.startListening();
|
||||||
|
|
||||||
this._state = 'ap';
|
this._state = 'ap';
|
||||||
|
this._apStartedAt = Date.now();
|
||||||
|
this._lastApSavedWifiRetryAt = 0;
|
||||||
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
|
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
|
||||||
log.info('provision', `配网地址: http://10.42.0.1`);
|
log.info('provision', `配网地址: http://10.42.0.1`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -178,16 +184,27 @@ class ProvisionManager extends EventEmitter {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
|
log.warn('provision', `WiFi 连接失败: ${result.error},按当前网络状态恢复`);
|
||||||
this._safeReenterAP();
|
this._recoverAfterWifiFailure();
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('provision', `配网过程异常: ${e.message}`);
|
log.error('provision', `配网过程异常: ${e.message}`);
|
||||||
this._safeReenterAP();
|
this._recoverAfterWifiFailure();
|
||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** WiFi 连接失败后:有线可用则保持 wired;否则开 AP 兜底。 */
|
||||||
|
_recoverAfterWifiFailure() {
|
||||||
|
if (hasInternet()) {
|
||||||
|
this._state = 'wired';
|
||||||
|
led.off();
|
||||||
|
this._emitNetworkReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._safeReenterAP();
|
||||||
|
}
|
||||||
|
|
||||||
/** 重新开 AP;失败时勿把 _state 永久卡在 connecting */
|
/** 重新开 AP;失败时勿把 _state 永久卡在 connecting */
|
||||||
_safeReenterAP() {
|
_safeReenterAP() {
|
||||||
try {
|
try {
|
||||||
@@ -202,34 +219,116 @@ class ProvisionManager extends EventEmitter {
|
|||||||
|
|
||||||
_startMonitor() {
|
_startMonitor() {
|
||||||
this._monitorTimer = setInterval(() => {
|
this._monitorTimer = setInterval(() => {
|
||||||
if (this._state === 'connecting') return;
|
if (this._monitorBusy) return;
|
||||||
|
this._monitorBusy = true;
|
||||||
|
this._monitorTick()
|
||||||
|
.catch((e) => log.error('provision', `WiFi 状态监控异常: ${e.message}`))
|
||||||
|
.finally(() => { this._monitorBusy = false; });
|
||||||
|
}, MONITOR_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
const wifiUp = isWifiStaConnected();
|
async _monitorTick() {
|
||||||
|
if (this._state === 'connecting') return;
|
||||||
|
|
||||||
if (this._state === 'sta' && !wifiUp) {
|
const wifiUp = isWifiStaConnected();
|
||||||
log.warn('provision', 'WiFi 连接已断开,重新启动 AP');
|
|
||||||
this._enterAP(); // 内部调用 led.off()
|
if (wifiUp && this._state !== 'sta') {
|
||||||
|
if (this._state === 'ap') {
|
||||||
|
log.info('provision', 'WiFi 已外部连接,关闭 AP');
|
||||||
|
this._stopAPServices();
|
||||||
|
}
|
||||||
|
this._state = 'sta';
|
||||||
|
this._emitNetworkReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._state === 'sta' && !wifiUp) {
|
||||||
|
log.warn('provision', 'WiFi 连接已断开,尝试恢复网络');
|
||||||
|
await this._recoverNetworkWithoutWifi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._state === 'wired') {
|
||||||
|
if (!hasInternet()) {
|
||||||
|
log.warn('provision', '有线网络不可用,尝试恢复 WiFi');
|
||||||
|
await this._recoverNetworkWithoutWifi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
led.off();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._state === 'ap') {
|
||||||
|
if (hasInternet()) {
|
||||||
|
log.info('provision', '检测到有线网络可用,关闭 AP');
|
||||||
|
this._stopAPServices();
|
||||||
|
this._state = 'wired';
|
||||||
|
this._emitNetworkReady();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._state === 'ap' && wifiUp) {
|
if (hasSavedWifiConnection() && this._shouldRetrySavedWifiFromAP()) {
|
||||||
log.info('provision', 'WiFi 已外部连接,关闭 AP');
|
await this._retrySavedWifiFromAP();
|
||||||
this._stopAPServices();
|
return;
|
||||||
this._state = 'sta';
|
|
||||||
this.emit('network-ready');
|
|
||||||
}
|
}
|
||||||
|
led.off();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 产品 WiFi 灯(OpenVFD wifi+eth):AP 全程强制熄灭,避免与其它逻辑竞态导致误亮
|
async _recoverNetworkWithoutWifi() {
|
||||||
if (this._state === 'ap') {
|
this._state = 'connecting';
|
||||||
led.off();
|
led.blink();
|
||||||
} else if (this._state === 'sta') {
|
|
||||||
if (hasInternet()) {
|
if (hasSavedWifiConnection()) {
|
||||||
led.on();
|
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||||
} else {
|
if (connected) {
|
||||||
led.off(); // STA 已连热点但无互联网
|
this._state = 'sta';
|
||||||
}
|
this._emitNetworkReady();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, MONITOR_INTERVAL_MS);
|
}
|
||||||
|
|
||||||
|
if (hasInternet()) {
|
||||||
|
this._state = 'wired';
|
||||||
|
led.off();
|
||||||
|
this._emitNetworkReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._safeReenterAP();
|
||||||
|
}
|
||||||
|
|
||||||
|
_shouldRetrySavedWifiFromAP() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (this._apStartedAt && now - this._apStartedAt < AP_MIN_UP_BEFORE_RETRY_MS) return false;
|
||||||
|
if (this._lastApSavedWifiRetryAt && now - this._lastApSavedWifiRetryAt < AP_SAVED_WIFI_RETRY_INTERVAL_MS) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _retrySavedWifiFromAP() {
|
||||||
|
this._lastApSavedWifiRetryAt = Date.now();
|
||||||
|
log.info('provision', 'AP 模式下定期尝试已保存 WiFi');
|
||||||
|
this._state = 'connecting';
|
||||||
|
led.blink();
|
||||||
|
this._stopAPServices();
|
||||||
|
await new Promise((r) => setTimeout(r, 3500));
|
||||||
|
|
||||||
|
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
|
||||||
|
if (connected) {
|
||||||
|
this._state = 'sta';
|
||||||
|
this._emitNetworkReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInternet()) {
|
||||||
|
this._state = 'wired';
|
||||||
|
led.off();
|
||||||
|
this._emitNetworkReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn('provision', 'AP 模式下重试已保存 WiFi 失败,恢复 AP');
|
||||||
|
this._safeReenterAP();
|
||||||
}
|
}
|
||||||
|
|
||||||
_stopMonitor() {
|
_stopMonitor() {
|
||||||
|
|||||||
Reference in New Issue
Block a user