Compare commits

..

12 Commits

1338 changed files with 379387 additions and 1215 deletions

View File

@@ -250,9 +250,7 @@ MemoryMax=256M
CPUQuota=50%
TasksMax=64
# Sandbox (allow writes to config, tmp, and network files)
ProtectSystem=full
ReadWritePaths=$CONFIG_DIR /tmp /etc/hosts /etc/hostname
# Sandbox disabled: clawd needs to write system/config files on some devices
# Logging
StandardOutput=journal

View File

@@ -20,7 +20,7 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync } = require('child_process');
const log = require('../logger');
// ── Constants (from reference script) ────────────────────────────────────────
@@ -45,11 +45,15 @@ function _buildClientVersion(version) {
// ── State-dir helpers (mirrors reference script) ─────────────────────────────
function _resolveStateDir() {
return (
(process.env.OPENCLAW_STATE_DIR || '').trim() ||
(process.env.CLAWDBOT_STATE_DIR || '').trim() ||
path.join(os.homedir(), '.openclaw')
);
return path.join('/home/sts', '.openclaw');
}
function _ensureStsOwnership(filePath) {
try {
execFileSync('chown', ['sts:sts', filePath], { stdio: 'ignore' });
} catch (err) {
log.warn('weixin', `chown sts:sts failed for ${filePath}: ${err.message}`);
}
}
function _resolveWeixinStateDir() { return path.join(_resolveStateDir(), 'openclaw-weixin'); }
@@ -81,7 +85,9 @@ function _registerAccountId(accountId) {
fs.mkdirSync(_resolveWeixinStateDir(), { recursive: true });
const existing = _listIndexedAccountIds();
if (existing.includes(accountId)) return;
fs.writeFileSync(_resolveAccountIndexPath(), JSON.stringify([...existing, accountId], null, 2), 'utf8');
const indexPath = _resolveAccountIndexPath();
fs.writeFileSync(indexPath, JSON.stringify([...existing, accountId], null, 2), 'utf8');
_ensureStsOwnership(indexPath);
}
function _loadAccount(accountId) {
@@ -113,6 +119,7 @@ function _saveAccount(accountId, update) {
};
const filePath = _resolveAccountPath(accountId);
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
_ensureStsOwnership(filePath);
try { fs.chmodSync(filePath, 0o600); } catch (_) {}
return filePath;
}
@@ -126,7 +133,9 @@ function _clearStaleAccountsForUserId(currentAccountId, userId) {
log.info('weixin', `removing stale account with same userId: ${id}`);
try { fs.unlinkSync(_resolveAccountPath(id)); } catch (_) {}
const existing = _listIndexedAccountIds();
fs.writeFileSync(_resolveAccountIndexPath(), JSON.stringify(existing.filter(x => x !== id), null, 2), 'utf8');
const indexPath = _resolveAccountIndexPath();
fs.writeFileSync(indexPath, JSON.stringify(existing.filter(x => x !== id), null, 2), 'utf8');
_ensureStsOwnership(indexPath);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,19 +36,19 @@ class StatusLed {
class Display {
showAP() {
if (writeLvglCommand('show_text:AP')) {
log.info('display', '显示屏 → AP');
if (writeLvglCommand('show_ap')) {
log.info('display', '显示屏 → AP(闪烁)');
}
}
showConn() {
if (writeLvglCommand('show_text:Conn')) {
log.info('display', '显示屏 → Conn');
if (writeLvglCommand('show_conn')) {
log.info('display', '显示屏 → Conn(闪烁)');
}
}
showErr0() {
if (writeLvglCommand('show_text:Err0')) {
if (writeLvglCommand('show_err0')) {
log.info('display', '显示屏 → Err0');
}
}
@@ -61,8 +61,8 @@ class Display {
showPin(pin) {
const s = String(pin || '').padStart(4, '0').slice(-4);
if (writeLvglCommand(`show_text:${s}`)) {
log.info('display', `显示屏 → PIN: ${s}`);
if (writeLvglCommand(`show_pin:${s}`)) {
log.info('display', `显示屏 → PIN: ${s}(闪烁)`);
}
}
}

View File

@@ -10,6 +10,7 @@ const AP_IP = '10.42.0.1';
const AP_PASSWORD = '12345678';
const AP_IFACE = process.env.CLAWD_WIFI_IFACE || '';
const CON_NAME = 'clawd-hotspot';
const AP_RETRY_TOKEN_FILE = '/run/clawd-ap-retry.token';
/** 产品 RJ45 在 sysfs 中的默认名;等价于检测 `cat /sys/class/net/end0/carrier` */
const DEFAULT_ETH_IFACE = 'end0';
@@ -92,28 +93,15 @@ function hasLanCableCarrier() {
return hasWiredCarrier();
}
function _tryPingInternet() {
function _tryPingDefaultInternet() {
try {
run('ping -c 1 -W 3 8.8.8.8');
return true;
} catch (_) {}
// 开热点时默认路由可能走 wlan无 -I 的 ping 会误判;指定有线口再试
const wired = getWiredIfaceWithCarrier();
if (wired) {
try {
run(`ping -c 1 -W 3 -I ${wired} 8.8.8.8`);
return true;
} catch (_) {}
}
return false;
}
/**
* 仅经有线口 ping 公网(不依赖默认路由)。
* AP 开启时 hasInternet() 易误判;维持 WS / 网络监视时用此兜底。
*/
function hasWiredInternetProbe() {
function _tryPingWiredInternet() {
const wired = getWiredIfaceWithCarrier();
if (!wired) return false;
try {
@@ -124,18 +112,31 @@ function hasWiredInternetProbe() {
}
/**
* 检测是否有互联网连接nmcli 连通性 + ping 兜底)
* 仅经有线口 ping 公网(不依赖默认路由)。
*/
function hasWiredInternetProbe() {
return _tryPingWiredInternet();
}
/**
* 检测是否有真实互联网连接。
* 注意NetworkManager 的 limited/local 可能只是 AP 本地网络或 captive 状态,不能当公网可用。
*/
function hasInternet() {
const wifiSta = isWifiStaConnected();
const wired = getWiredIfaceWithCarrier();
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 falsenmcli 有缓存,不可信)
if (!isWifiStaConnected() && !hasWiredCarrier()) return false;
if (!wifiSta && !wired) return false;
try {
const out = run('nmcli networking connectivity check').trim();
if (out === 'full' || out === 'limited') return true;
if (out === 'full') return true;
} catch (_) {}
return _tryPingInternet();
if (wifiSta) return _tryPingDefaultInternet();
if (wired) return _tryPingWiredInternet();
return false;
}
/**
@@ -265,6 +266,7 @@ function nmcliAsync(args, timeoutMs = 60000) {
* @returns {Promise<{ success: boolean, error?: string }>}
*/
async function connectWifi(ssid, password) {
cancelHotspotRadioRetry(`准备连接 WiFi: ${ssid}`);
const iface = getWifiIface();
log.info('network', `尝试连接 WiFi: ${ssid}ifname=${iface}`);
try {
@@ -272,14 +274,36 @@ async function connectWifi(ssid, password) {
await nmcliAsync(['connection', 'delete', ssid], 15000);
} catch (_) {}
try {
await nmcliAsync(['device', 'set', iface, 'managed', 'yes'], 8000);
} catch (_) {}
await _resetWifiRadioForSTA(iface);
const args = ['device', 'wifi', 'connect', ssid];
if (password) args.push('password', password);
args.push('ifname', iface);
await nmcliAsync(args, 120000);
if (password) {
// 显式创建 STA profile并固定为 WPA2-PSK only。
// RK3588/Broadcom DHD 对 NetworkManager 默认生成的 SAE/FT/WPA-PSK-SHA256 混合参数不稳定,
// 可能表现为一直 associating -> disconnected最后误报“需要密钥”。
await nmcliAsync([
'connection', 'add',
'type', 'wifi',
'ifname', iface,
'con-name', ssid,
'ssid', ssid,
], 15000);
await nmcliAsync([
'connection', 'modify', ssid,
// 连接成功前先禁止自动连接,避免失败恢复 AP 时 NM 又自动抢占 wlan0。
'connection.autoconnect', 'no',
'802-11-wireless-security.key-mgmt', 'wpa-psk',
'802-11-wireless-security.proto', 'rsn',
'802-11-wireless-security.pairwise', 'ccmp',
'802-11-wireless-security.group', 'ccmp',
'802-11-wireless-security.pmf', 'disable',
'802-11-wireless-security.psk', password,
], 15000);
await nmcliAsync(['connection', 'up', 'id', ssid, 'ifname', iface], 120000);
} else {
await nmcliAsync(['device', 'wifi', 'connect', ssid, 'ifname', iface], 120000);
}
await _ensureActiveWifiAutoconnect();
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
@@ -299,11 +323,201 @@ async function connectWifi(ssid, password) {
}
return { success: false, error: '超时:网卡未进入已连接状态' };
} catch (e) {
try { await nmcliAsync(['connection', 'modify', ssid, 'connection.autoconnect', 'no'], 8000); } catch (_) {}
log.error('network', `WiFi 连接失败: ${e.message}`);
return { success: false, error: e.message };
}
}
function _newHotspotRetryToken() {
const token = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
try {
fs.writeFileSync(AP_RETRY_TOKEN_FILE, token, { mode: 0o600 });
} catch (e) {
log.warn('network', `写入 AP retry token 失败: ${e.message}`);
}
return token;
}
function cancelHotspotRadioRetry(reason = 'cancel') {
try {
fs.unlinkSync(AP_RETRY_TOKEN_FILE);
log.info('network', `已取消后台 AP retry: ${reason}`);
} catch (_) {}
}
async function _resetWifiRadioForSTA(iface, reason = '准备连接 STA 前重置 WiFi radio') {
log.warn('network', `${reason}: ${iface}`);
try { await nmcliAsync(['connection', 'down', CON_NAME], 8000); } catch (_) {}
try { await nmcliAsync(['connection', 'delete', CON_NAME], 8000); } catch (_) {}
try { await nmcliAsync(['device', 'disconnect', iface], 8000); } catch (_) {}
try {
await nmcliAsync(['radio', 'wifi', 'off'], 10000);
} catch (e) {
log.warn('network', `关闭 WiFi radio 失败: ${e.message}`);
}
await _delay(2500);
try {
await nmcliAsync(['radio', 'wifi', 'on'], 10000);
} catch (e) {
log.warn('network', `开启 WiFi radio 失败: ${e.message}`);
}
await _delay(5000);
try { await nmcliAsync(['device', 'set', iface, 'managed', 'yes'], 8000); } catch (_) {}
try { await nmcliAsync(['device', 'wifi', 'rescan', 'ifname', iface], 15000); } catch (_) {}
await _delay(1500);
}
function _resetWifiRadioForAP(iface, reason = '准备 AP 前重置 WiFi radio') {
log.warn('network', `${reason}: ${iface}`);
try { nmcliSync(['connection', 'down', CON_NAME], 8000); } catch (_) {}
try { nmcliSync(['connection', 'delete', CON_NAME], 8000); } catch (_) {}
try { nmcliSync(['device', 'disconnect', iface], 8000); } catch (_) {}
try {
nmcliSync(['radio', 'wifi', 'off'], 10000);
} catch (e) {
log.warn('network', `关闭 WiFi radio 失败: ${e.message}`);
}
sleep(2500);
try {
nmcliSync(['radio', 'wifi', 'on'], 10000);
} catch (e) {
log.warn('network', `开启 WiFi radio 失败: ${e.message}`);
}
sleep(5000);
try { nmcliSync(['device', 'set', iface, 'managed', 'yes'], 8000); } catch (_) {}
}
function _spawnHotspotRadioRetry(ssid, iface) {
const token = _newHotspotRetryToken();
const script = `
set -u
log() { logger -t clawd-ap-retry "$*"; }
check_token() {
if [ ! -f "$TOKEN_FILE" ] || [ "$(cat "$TOKEN_FILE" 2>/dev/null || true)" != "$TOKEN" ]; then
log "AP retry canceled"
exit 0
fi
}
log "AP retry started: ssid=$SSID iface=$IFACE"
check_token
nmcli connection down "$CON_NAME" >/dev/null 2>&1 || true
nmcli connection delete "$CON_NAME" >/dev/null 2>&1 || true
nmcli device disconnect "$IFACE" >/dev/null 2>&1 || true
check_token
nmcli radio wifi off >/dev/null 2>&1 || true
sleep 2.5
# If canceled while radio is off, always turn it back on before exiting.
nmcli radio wifi on >/dev/null 2>&1 || true
sleep 5
check_token
nmcli device set "$IFACE" managed yes >/dev/null 2>&1 || true
check_token
if ! nmcli connection add type wifi ifname "$IFACE" con-name "$CON_NAME" ssid "$SSID" >/dev/null 2>&1; then
log "AP retry failed: connection add failed"
exit 1
fi
args=(
connection modify "$CON_NAME"
connection.autoconnect no
802-11-wireless.mode ap
802-11-wireless.band bg
802-11-wireless.channel 1
802-11-wireless-security.key-mgmt wpa-psk
802-11-wireless-security.proto rsn
802-11-wireless-security.pairwise ccmp
802-11-wireless-security.group ccmp
802-11-wireless-security.pmf disable
ipv4.method shared
ipv4.addresses "$AP_IP/24"
ipv6.method ignore
)
if [ -n "\${AP_PASSWORD:-}" ]; then
args+=(802-11-wireless-security.psk "$AP_PASSWORD")
fi
check_token
if ! nmcli "\${args[@]}" >/dev/null 2>&1; then
log "AP retry failed: connection modify failed"
exit 1
fi
check_token
if nmcli connection up "$CON_NAME" >/dev/null 2>&1; then
if [ ! -f "$TOKEN_FILE" ] || [ "$(cat "$TOKEN_FILE" 2>/dev/null || true)" != "$TOKEN" ]; then
log "AP retry canceled after connection up; tearing hotspot down"
nmcli connection down "$CON_NAME" >/dev/null 2>&1 || true
nmcli connection delete "$CON_NAME" >/dev/null 2>&1 || true
exit 0
fi
log "AP retry success: $SSID"
rm -f "$TOKEN_FILE"
else
log "AP retry failed: connection up failed"
exit 1
fi
`;
const child = spawn('/bin/bash', ['-lc', script], {
detached: true,
stdio: 'ignore',
env: {
...process.env,
SSID: ssid,
IFACE: iface,
CON_NAME,
AP_IP,
AP_PASSWORD: AP_PASSWORD || '',
TOKEN_FILE: AP_RETRY_TOKEN_FILE,
TOKEN: token,
},
});
child.unref();
}
function _createHotspotProfile(ssid, iface) {
nmcliSync([
'connection', 'add',
'type', 'wifi',
'ifname', iface,
'con-name', CON_NAME,
'ssid', ssid,
], 15000);
const modifyArgs = [
'connection', 'modify', CON_NAME,
'connection.autoconnect', 'no',
'802-11-wireless.mode', 'ap',
'802-11-wireless.band', 'bg',
'802-11-wireless.channel', '1',
'802-11-wireless-security.key-mgmt', 'wpa-psk',
'802-11-wireless-security.proto', 'rsn',
'802-11-wireless-security.pairwise', 'ccmp',
'802-11-wireless-security.group', 'ccmp',
'802-11-wireless-security.pmf', 'disable',
'ipv4.method', 'shared',
'ipv4.addresses', `${AP_IP}/24`,
'ipv6.method', 'ignore',
];
if (AP_PASSWORD) {
modifyArgs.push('802-11-wireless-security.psk', AP_PASSWORD);
}
nmcliSync(modifyArgs, 15000);
}
function _activateHotspot(ssid, iface, timeoutMs = 8000) {
_createHotspotProfile(ssid, iface);
nmcliSync(['connection', 'up', CON_NAME], timeoutMs);
}
/**
* 启动 WiFi AP 热点
*/
@@ -313,29 +527,24 @@ function startAP(clawId) {
log.info('network', `启动 AP 热点: ${ssid} (${iface})`);
// 关闭已有热点
// 关闭已有热点,并在重新拉起 AP 前真正 power-cycle WiFi 芯片。
// RK3588/Broadcom DHD 在 LAN 断开后切 AP 时,单纯 ip link down/up 不一定清掉固件残留状态。
stopAP();
_resetWifiRadioForAP(iface, '准备 AP 前重置 WiFi radio');
try {
// nmcli 创建热点(开放网络)
const cmd = [
'nmcli device wifi hotspot',
`ifname ${iface}`,
`con-name ${CON_NAME}`,
`ssid "${ssid}"`,
'band bg',
];
// 如果需要密码
if (AP_PASSWORD) {
cmd.push(`password "${AP_PASSWORD}"`);
}
run(cmd.join(' '));
// 显式创建并激活热点,固定为 WPA2-PSK only。
// 避免 NetworkManager 自动生成 WPA-PSK-SHA256/SAE 混合配置,部分 3588 Broadcom DHD 驱动重启 AP 时会拒绝该 RSN 参数。
try {
nmcliSync(['connection', 'modify', CON_NAME, 'connection.autoconnect', 'no'], 8000);
} catch (_) {}
_activateHotspot(ssid, iface, 8000);
} catch (firstError) {
log.warn('network', `AP 启动未在短超时内完成,后台再次重置 WiFi radio 后重试;避免阻塞 watchdog: ${firstError.message}`);
_spawnHotspotRadioRetry(ssid, iface);
return { ssid, ip: AP_IP, iface, pending: true };
}
// 等待 AP 启动
sleep(2000);
sleep(1000);
log.info('network', `AP 已启动: ${ssid}, 网关 ${AP_IP}`);
return { ssid, ip: AP_IP, iface };
} catch (e) {
@@ -348,6 +557,7 @@ function startAP(clawId) {
* 关闭热点,恢复普通 WiFi 模式
*/
function stopAP() {
cancelHotspotRadioRetry('停止 AP');
try {
run(`nmcli connection down ${CON_NAME}`);
} catch (_) {}
@@ -454,6 +664,7 @@ async function _ensureActiveWifiAutoconnect() {
* clawd 只做调度真正的认证、DHCP、重连细节仍交给 NM。
*/
async function connectSavedWifiConnections() {
cancelHotspotRadioRetry('准备连接已保存 WiFi');
const iface = getWifiIface();
const profiles = listSavedWifiConnections();
if (profiles.length === 0) {
@@ -574,6 +785,7 @@ module.exports = {
connectWifi,
startAP,
stopAP,
cancelHotspotRadioRetry,
AP_IP,
getLocalIps,
getLocalNetworks,

View File

@@ -1,360 +1,362 @@
'use strict';
const EventEmitter = require('events');
const log = require('./logger');
const { hasInternet, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
const { DnsHijack } = require('./dns-hijack');
const { CaptiveServer } = require('./captive-server');
const led = require('./led');
const MONITOR_INTERVAL_MS = 15_000;
const WIFI_RECONNECT_MAX_ROUNDS = 3;
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 常驻配网管理器。
*
* 规则:
* - 启动时WiFi STA 优先;有已保存 WiFi 时主动让 NM 重连,最多 3 轮
* - 有线网络可用时:通知网络就绪,但不自动开启 AP
* - 自动开 AP 的唯一兜底:无有线/无 WiFi且无 saved WiFi 或 saved WiFi 3 轮失败
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则按网络状态决定是否重新开 AP
* - AP 状态下:若仍无有线网络,低频释放 wlan0 并尝试 saved WiFi
*/
class ProvisionManager extends EventEmitter {
constructor(clawId) {
super();
this._clawId = clawId || 'Setup';
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta' | 'wired'
this._dns = null;
this._server = null;
this._monitorTimer = null;
this._monitorBusy = false;
this._apStartedAt = 0;
this._lastApSavedWifiRetryAt = 0;
}
/** 是否正处于 AP 模式WiFi 热点广播中) */
isApMode() { return this._state === 'ap'; }
async start() {
led.off(); // WiFi 灯初始状态:熄灭
// WiFi STA 已连接 → 直接进入 STA 模式
if (isWifiStaConnected()) {
this._state = 'sta';
log.info('provision', 'WiFi STA 已连接AP 不启动');
this._emitNetworkReady();
this._startMonitor();
return;
}
// 网络已就绪时先启动 WShasInternet() 可能来自 WiFi也可能来自有线不能直接当作 wired。
if (hasInternet()) {
if (isWifiStaConnected()) {
this._state = 'sta';
log.info('provision', 'WiFi STA 已连接AP 不启动');
this._emitNetworkReady();
} else {
this._state = 'wired';
log.info('provision', '有线网络就绪,启动 WS不自动开启 AP');
led.off();
this._emitNetworkReady();
}
this._startMonitor();
return;
}
// 无有线可用时,有 saved WiFi 才主动让 NetworkManager 重连;不要只被动等待 NM autoconnect。
if (hasSavedWifiConnection()) {
log.info('provision', `发现已保存的 WiFi 配置,主动重连(最多 ${WIFI_RECONNECT_MAX_ROUNDS} 轮)...`);
this._state = 'connecting';
led.blink();
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
if (connected) {
this._state = 'sta';
log.info('provision', '已保存 WiFi 重连成功AP 不启动');
this._emitNetworkReady();
this._startMonitor();
return;
}
log.warn('provision', '已保存 WiFi 重连失败');
}
// 无有线、无 WiFi且无 saved WiFi 或 saved WiFi 3 轮失败 → 开 AP 兜底配网。
this._enterAP();
this._startMonitor();
}
_emitNetworkReady() {
if (hasInternet()) {
// WiFi 灯只在 STA 模式下亮(有线有网而 WiFi 在 AP 模式时不亮)
if (this._state === 'sta') led.on();
this.emit('network-ready');
} else {
log.warn('provision', 'hasInternet() 返回 falseLED 保持熄灭');
}
}
async _trySavedWifiReconnectRounds(rounds = WIFI_RECONNECT_MAX_ROUNDS) {
for (let i = 1; i <= rounds; i++) {
if (isWifiStaConnected()) return true;
log.info('provision', `尝试已保存 WiFi 重连:第 ${i}/${rounds}`);
const result = await connectSavedWifiConnections();
if (result.success || isWifiStaConnected()) return true;
if (i < rounds) {
await new Promise((r) => setTimeout(r, WIFI_RECONNECT_ROUND_DELAY_MS));
}
}
return false;
}
stop() {
this._stopMonitor();
this._stopAll();
this._state = 'idle';
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
}
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
_enterAP() {
if (this._state === 'ap') return;
led.off(); // AP 模式WiFi 未连接WiFi 灯熄灭
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP有线时等 WS 连接后再定
try {
// 若上次进程退出前留下 clawd-hotspot必须先释放 wlan0否则会在 AP 模式下扫描,列表可能只剩 2.4G/自身热点。
stopAP();
// AP 模式下无法扫描 WiFi必须在开 AP 之前扫描并缓存
log.info('provision', '扫描周边 WiFi...');
this._cachedWifiList = scanWifi();
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络: ${this._cachedWifiList.map(w => `${w.ssid}${w.band ? `(${w.band})` : ''}`).join(', ')}`);
// 写 DNS 劫持配置NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
this._dns = new DnsHijack();
this._dns.start(getWifiIface(), AP_IP);
const ap = startAP(this._clawId);
this._server = new CaptiveServer({
clawId: this._clawId,
cachedWifiList: this._cachedWifiList,
onConnect: (ssid, password) => this._handleWifiConnect(ssid, password),
});
this._server.startListening();
this._state = 'ap';
this._apStartedAt = Date.now();
this._lastApSavedWifiRetryAt = 0;
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
log.info('provision', `配网地址: http://10.42.0.1`);
} catch (e) {
log.error('provision', `AP 启动失败: ${e.message}`);
if (this._state !== 'sta') this._state = 'idle';
}
}
// ── 用户提交 WiFi 凭证 ───────────────────────────────────────────────────
async _handleWifiConnect(ssid, password) {
if (this._state === 'connecting') return { success: false, error: '正在连接中,请稍候' };
this._state = 'connecting';
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
led.blink(); // 正在连接 → 闪烁
try {
this._stopAPServices();
// 关热点后射频/模式切换需要时间,立刻 connect 在部分板子上会失败
await new Promise((r) => setTimeout(r, 3500));
const result = await connectWifi(ssid, password);
if (result.success) {
this._state = 'sta';
log.info('provision', `WiFi 已连接: ${ssid}`);
led.on(); // WiFi 灯:连接成功 → 常亮
this.emit('network-ready');
return result;
}
log.warn('provision', `WiFi 连接失败: ${result.error},按当前网络状态恢复`);
this._recoverAfterWifiFailure();
return result;
} catch (e) {
log.error('provision', `配网过程异常: ${e.message}`);
this._recoverAfterWifiFailure();
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 */
_safeReenterAP() {
try {
this._enterAP();
} catch (e) {
log.error('provision', `重新启动 AP 失败: ${e.message}`);
this._state = 'idle';
}
}
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
_startMonitor() {
this._monitorTimer = setInterval(() => {
if (this._monitorBusy) return;
this._monitorBusy = true;
this._monitorTick()
.catch((e) => log.error('provision', `WiFi 状态监控异常: ${e.message}`))
.finally(() => { this._monitorBusy = false; });
}, MONITOR_INTERVAL_MS);
}
async _monitorTick() {
if (this._state === 'connecting') return;
const wifiUp = isWifiStaConnected();
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;
}
if (hasSavedWifiConnection() && this._shouldRetrySavedWifiFromAP()) {
await this._retrySavedWifiFromAP();
return;
}
led.off();
return;
}
}
async _recoverNetworkWithoutWifi() {
this._state = 'connecting';
led.blink();
if (hasSavedWifiConnection()) {
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;
}
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() {
if (this._monitorTimer) {
clearInterval(this._monitorTimer);
this._monitorTimer = null;
}
}
// ── 清理 ──────────────────────────────────────────────────────────────────
_stopAPServices() {
if (this._server) {
this._server.stop();
this._server = null;
}
if (this._dns) {
this._dns.stop();
this._dns = null;
}
stopAP();
}
_stopAll() {
this._stopAPServices();
}
}
module.exports = { ProvisionManager };
'use strict';
const EventEmitter = require('events');
const log = require('./logger');
const { hasInternet, hasWiredInternetProbe, hasSavedWifiConnection, connectSavedWifiConnections, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, getWifiIface, AP_IP } = require('./network');
const { DnsHijack } = require('./dns-hijack');
const { CaptiveServer } = require('./captive-server');
const led = require('./led');
const MONITOR_INTERVAL_MS = 15_000;
const WIFI_RECONNECT_MAX_ROUNDS = 3;
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 常驻配网管理器。
*
* 规则:
* - 启动时WiFi STA 优先;有已保存 WiFi 时主动让 NM 重连,最多 3 轮
* - 有线网络可用时:通知网络就绪,但不自动开启 AP
* - 自动开 AP 的唯一兜底:无有线/无 WiFi且无 saved WiFi 或 saved WiFi 3 轮失败
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则按网络状态决定是否重新开 AP
* - AP 状态下:若仍无有线网络,低频释放 wlan0 并尝试 saved WiFi
*/
class ProvisionManager extends EventEmitter {
constructor(clawId) {
super();
this._clawId = clawId || 'Setup';
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta' | 'wired'
this._dns = null;
this._server = null;
this._monitorTimer = null;
this._monitorBusy = false;
this._apStartedAt = 0;
this._lastApSavedWifiRetryAt = 0;
}
/** 是否正处于 AP 模式WiFi 热点广播中) */
isApMode() { return this._state === 'ap'; }
async start() {
led.off(); // WiFi 灯初始状态:熄灭
// WiFi STA 已连接 → 直接进入 STA 模式
if (isWifiStaConnected()) {
this._state = 'sta';
log.info('provision', 'WiFi STA 已连接AP 不启动');
this._emitNetworkReady();
this._startMonitor();
return;
}
// 网络已就绪时先启动 WShasInternet() 可能来自 WiFi也可能来自有线不能直接当作 wired。
if (hasInternet()) {
if (isWifiStaConnected()) {
this._state = 'sta';
log.info('provision', 'WiFi STA 已连接AP 不启动');
this._emitNetworkReady();
} else {
this._state = 'wired';
log.info('provision', '有线网络就绪,启动 WS不自动开启 AP');
led.off();
this._emitNetworkReady();
}
this._startMonitor();
return;
}
// 无有线可用时,有 saved WiFi 才主动让 NetworkManager 重连;不要只被动等待 NM autoconnect。
if (hasSavedWifiConnection()) {
log.info('provision', `发现已保存的 WiFi 配置,主动重连(最多 ${WIFI_RECONNECT_MAX_ROUNDS} 轮)...`);
this._state = 'connecting';
led.blink();
const connected = await this._trySavedWifiReconnectRounds(WIFI_RECONNECT_MAX_ROUNDS);
if (connected) {
this._state = 'sta';
log.info('provision', '已保存 WiFi 重连成功AP 不启动');
this._emitNetworkReady();
this._startMonitor();
return;
}
log.warn('provision', '已保存 WiFi 重连失败');
}
// 无有线、无 WiFi且无 saved WiFi 或 saved WiFi 3 轮失败 → 开 AP 兜底配网。
this._enterAP();
this._startMonitor();
}
_emitNetworkReady() {
if (hasInternet()) {
// WiFi 灯只在 STA 模式下亮(有线有网而 WiFi 在 AP 模式时不亮)
if (this._state === 'sta') led.on();
this.emit('network-ready');
} else {
log.warn('provision', 'hasInternet() 返回 falseLED 保持熄灭');
}
}
async _trySavedWifiReconnectRounds(rounds = WIFI_RECONNECT_MAX_ROUNDS) {
for (let i = 1; i <= rounds; i++) {
if (isWifiStaConnected()) return true;
log.info('provision', `尝试已保存 WiFi 重连:第 ${i}/${rounds}`);
const result = await connectSavedWifiConnections();
if (result.success || isWifiStaConnected()) return true;
if (i < rounds) {
await new Promise((r) => setTimeout(r, WIFI_RECONNECT_ROUND_DELAY_MS));
}
}
return false;
}
stop() {
this._stopMonitor();
this._stopAll();
this._state = 'idle';
led.destroy(); // WiFi 灯:停止时关灯、释放闪烁定时器
}
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
_enterAP() {
if (this._state === 'ap') return;
led.off(); // AP 模式WiFi 未连接WiFi 灯熄灭
if (!hasInternet()) led.display.showAP(); // 无网时立即显示 AP有线时等 WS 连接后再定
try {
// 若上次进程退出前留下 clawd-hotspot必须先释放 wlan0否则会在 AP 模式下扫描,列表可能只剩 2.4G/自身热点。
stopAP();
// AP 模式下无法扫描 WiFi必须在开 AP 之前扫描并缓存
log.info('provision', '扫描周边 WiFi...');
this._cachedWifiList = scanWifi();
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络: ${this._cachedWifiList.map(w => `${w.ssid}${w.band ? `(${w.band})` : ''}`).join(', ')}`);
// 写 DNS 劫持配置NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
this._dns = new DnsHijack();
this._dns.start(getWifiIface(), AP_IP);
const ap = startAP(this._clawId);
this._server = new CaptiveServer({
clawId: this._clawId,
cachedWifiList: this._cachedWifiList,
onConnect: (ssid, password) => this._handleWifiConnect(ssid, password),
});
this._server.startListening();
this._state = 'ap';
this._apStartedAt = Date.now();
this._lastApSavedWifiRetryAt = 0;
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
log.info('provision', `配网地址: http://10.42.0.1`);
} catch (e) {
log.error('provision', `AP 启动失败: ${e.message}`);
if (this._state !== 'sta') this._state = 'idle';
}
}
// ── 用户提交 WiFi 凭证 ───────────────────────────────────────────────────
async _handleWifiConnect(ssid, password) {
if (this._state === 'connecting') return { success: false, error: '正在连接中,请稍候' };
this._state = 'connecting';
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
led.blink(); // 正在连接 → 闪烁
try {
this._stopAPServices();
// 关热点后射频/模式切换需要时间,立刻 connect 在部分板子上会失败
await new Promise((r) => setTimeout(r, 3500));
const result = await connectWifi(ssid, password);
if (result.success) {
this._state = 'sta';
log.info('provision', `WiFi 已连接: ${ssid}`);
led.on(); // WiFi 灯:连接成功 → 常亮
this.emit('network-ready');
return result;
}
log.warn('provision', `WiFi 连接失败: ${result.error},按当前网络状态恢复`);
this._recoverAfterWifiFailure();
return result;
} catch (e) {
log.error('provision', `配网过程异常: ${e.message}`);
this._recoverAfterWifiFailure();
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 */
_safeReenterAP() {
try {
this._enterAP();
} catch (e) {
log.error('provision', `重新启动 AP 失败: ${e.message}`);
this._state = 'idle';
}
}
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
_startMonitor() {
this._monitorTimer = setInterval(() => {
if (this._monitorBusy) return;
this._monitorBusy = true;
this._monitorTick()
.catch((e) => log.error('provision', `WiFi 状态监控异常: ${e.message}`))
.finally(() => { this._monitorBusy = false; });
}, MONITOR_INTERVAL_MS);
}
async _monitorTick() {
if (this._state === 'connecting') return;
const wifiUp = isWifiStaConnected();
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') {
// AP 模式下 hasInternet() 可能被热点本地网络 / NetworkManager limited 状态误判。
// 只有明确探测到有线口可访问公网时,才关闭配网 AP。
if (hasWiredInternetProbe()) {
log.info('provision', '检测到有线网络可用,关闭 AP');
this._stopAPServices();
this._state = 'wired';
this._emitNetworkReady();
return;
}
if (hasSavedWifiConnection() && this._shouldRetrySavedWifiFromAP()) {
await this._retrySavedWifiFromAP();
return;
}
led.off();
return;
}
}
async _recoverNetworkWithoutWifi() {
this._state = 'connecting';
led.blink();
if (hasSavedWifiConnection()) {
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;
}
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() {
if (this._monitorTimer) {
clearInterval(this._monitorTimer);
this._monitorTimer = null;
}
}
// ── 清理 ──────────────────────────────────────────────────────────────────
_stopAPServices() {
if (this._server) {
this._server.stop();
this._server = null;
}
if (this._dns) {
this._dns.stop();
this._dns = null;
}
stopAP();
}
_stopAll() {
this._stopAPServices();
}
}
module.exports = { ProvisionManager };

Binary file not shown.

21
lib/resource/3588s/src/LICENSE Executable file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Littlev Graphics Library
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

105
lib/resource/3588s/src/Makefile Executable file
View File

@@ -0,0 +1,105 @@
ROOT_DIR := $(shell pwd)
LVGL_DIR_NAME ?= lvgl
CC ?= gcc
TARGET_NAME := demo
BUILD_DIR := build
OBJ_DIR := $(BUILD_DIR)/obj
BIN_DIR := $(BUILD_DIR)/bin
TARGET := $(BIN_DIR)/$(TARGET_NAME)
IMG_SRC_DIR := user/images
# 图片文件名固定为 logo后缀可以是 png / jpg / jpeg / bmp
LOGO_NAME := logo
LOGO_IMG_EXTS := png jpg jpeg bmp
LOGO_IMG_FILES := $(foreach ext,$(LOGO_IMG_EXTS),$(IMG_SRC_DIR)/$(LOGO_NAME).$(ext))
LOGO_IMG := $(firstword $(wildcard $(LOGO_IMG_FILES)))
LOGO_C := $(LOGO_NAME).c
LVGL_IMG_CONV := lv_img_conv
LVGL_IMG_CF := CF_TRUE_COLOR_ALPHA
APP_CSRCS := $(filter-out logo.c,$(wildcard *.c))
APP_CSRCS += $(shell if [ -d src ]; then find src -type f -name "*.c"; fi)
LVGL_CSRCS := $(shell if [ -d $(LVGL_DIR_NAME)/src ]; then find $(LVGL_DIR_NAME)/src -type f -name "*.c"; fi)
LV_DRIVERS_CSRCS := $(shell if [ -d lv_drivers ]; then find lv_drivers -type f -name "*.c"; fi)
CSRCS := $(APP_CSRCS)
CSRCS += $(LVGL_CSRCS)
CSRCS += $(LV_DRIVERS_CSRCS)
CSRCS += $(LOGO_C)
OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(CSRCS))
CFLAGS ?= -O3 -g0
CFLAGS += -Wall
CFLAGS += -I.
CFLAGS += -I$(LVGL_DIR_NAME)
CFLAGS += -I$(LVGL_DIR_NAME)/src
CFLAGS += -Ilv_drivers
CFLAGS += -Iinclude
LDFLAGS += -lm
.PHONY: all
all: $(TARGET)
$(BIN_DIR):
mkdir -p $(BIN_DIR)
$(OBJ_DIR):
mkdir -p $(OBJ_DIR)
$(LOGO_C): $(LOGO_IMG)
@if [ -z "$(LOGO_IMG)" ]; then \
echo "Error: 未找到图片文件"; \
echo "请将图片命名为以下任意一种格式,并放入 $(IMG_SRC_DIR) 目录:"; \
echo " logo.png"; \
echo " logo.jpg"; \
echo " logo.jpeg"; \
echo " logo.bmp"; \
exit 1; \
fi
@echo "Use image: $(LOGO_IMG)"
rm -f $(LOGO_C)
cp $(LOGO_IMG) ./$(notdir $(LOGO_IMG))
$(LVGL_IMG_CONV) $(notdir $(LOGO_IMG)) -f -c $(LVGL_IMG_CF)
rm -f ./$(notdir $(LOGO_IMG))
$(OBJ_DIR)/%.o: %.c
mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(TARGET): $(LOGO_C) $(OBJS) | $(BIN_DIR)
$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
@echo "Build success: $(TARGET)"
.PHONY: clean
clean:
rm -rf $(OBJ_DIR)
rm -f $(TARGET)
.PHONY: imgclean
imgclean:
rm -f $(LOGO_C)
.PHONY: distclean
distclean:
rm -rf $(BUILD_DIR)
rm -f $(LOGO_C)
.PHONY: info
info:
@echo "TARGET = $(TARGET)"
@echo "IMG_SRC_DIR = $(IMG_SRC_DIR)"
@echo "LOGO_IMG = $(LOGO_IMG)"
@echo "LOGO_C = $(LOGO_C)"
@echo "Support image formats: $(LOGO_IMG_EXTS)"
@echo "CSRCS = $(CSRCS)"

View File

@@ -0,0 +1,8 @@
# LVGL for frame buffer device
LVGL configured to work with /dev/fb0 on Linux.
When cloning this repository, also make sure to download submodules (`git submodule update --init --recursive`) otherwise you will be missing key components.
Check out this blog post for a step by step tutorial:
https://blog.lvgl.io/2018-01-03/linux_fb

View File

@@ -0,0 +1,16 @@
#! /bin/sh
start() {
demo &
}
case "$1" in
start)
start
;;
*)
echo "Usage: $0 {start}"
exit 1
esac
exit $?

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More