From f58db93b645afe18666bafe80518b99614ec077e Mon Sep 17 00:00:00 2001 From: stswangzhiping <59632378+stswangzhiping@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:27:25 +0800 Subject: [PATCH] fix: wait for NM auto-reconnect before starting AP on reboot After WiFi is configured and device reboots, NetworkManager needs a few seconds to auto-connect to saved WiFi. Without waiting, AP starts immediately and occupies wlan0, preventing NM reconnect. - Add hasSavedWifiConnection() to check for saved WiFi profiles - Wait up to 20s for NM auto-connect before falling back to AP - First boot (no saved WiFi) still starts AP immediately Made-with: Cursor --- .commitmsg | 16 ++++++------- lib/client.js | 24 +++++++++++--------- lib/network.js | 17 ++++++++++++++ lib/provisioning.js | 55 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 89 insertions(+), 23 deletions(-) diff --git a/.commitmsg b/.commitmsg index 98e3132..c6fc108 100644 --- a/.commitmsg +++ b/.commitmsg @@ -1,9 +1,9 @@ -feat: AP always-on mode - hotspot stays until WiFi STA connects +fix: wait for NM auto-reconnect before starting AP on reboot -Redesign provisioning from one-shot blocking to persistent background manager: -- AP hotspot starts at boot regardless of eth0 status -- Captive portal runs alongside AP for WiFi configuration -- AP automatically shuts down only when WiFi STA connects -- WiFi drops at runtime -> AP auto-restarts -- WiFi connect fails -> AP auto-restarts for retry -- client.js no longer blocks on network; connects WS when ready +After WiFi is configured and device reboots, NetworkManager needs +a few seconds to auto-connect to saved WiFi. Without waiting, +AP starts immediately and occupies wlan0, preventing NM reconnect. + +- Add hasSavedWifiConnection() to check for saved WiFi profiles +- Wait up to 20s for NM auto-connect before falling back to AP +- First boot (no saved WiFi) still starts AP immediately diff --git a/lib/client.js b/lib/client.js index 070a74f..40d3b92 100644 --- a/lib/client.js +++ b/lib/client.js @@ -60,21 +60,25 @@ class ClawClient { async start() { log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`); - // 后台启动 AP 配网管理器(WiFi 未连接时常驻热点) + // 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP) this._provisionMgr = new ProvisionManager(this._cfg.claw_id); - this._provisionMgr.start(); - // 有网络时直接连接云端 - if (hasInternet()) { - await this._proceedWithConnection(); - } else { - // 等待配网完成后再连 - log.info('clawd', '等待网络就绪...'); - this._provisionMgr.once('network-ready', () => { + // 网络就绪时连接云端 + this._provisionMgr.once('network-ready', () => { + if (!this._ws) { this._proceedWithConnection().catch(e => { log.error('clawd', '连接启动失败:', e.message); }); - }); + } + }); + + await this._provisionMgr.start(); + + // start() 返回后,如果已有网络,直接连 + if (hasInternet() && !this._ws) { + await this._proceedWithConnection(); + } else if (!hasInternet()) { + log.info('clawd', '等待网络就绪(WiFi 配网或网线接入)...'); } } diff --git a/lib/network.js b/lib/network.js index 9154956..510df6a 100644 --- a/lib/network.js +++ b/lib/network.js @@ -173,6 +173,22 @@ function sleep(ms) { execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 }); } +/** + * 检测是否有已保存的 WiFi STA 连接(排除自身热点) + */ +function hasSavedWifiConnection() { + try { + const out = run('nmcli -t -f NAME,TYPE connection show'); + for (const line of out.split('\n')) { + const [name, type] = line.split(':'); + if (type === '802-11-wireless' && name !== CON_NAME) { + return true; + } + } + } catch (_) {} + return false; +} + /** * 检测 wlan0 是否以 STA 模式连接了 WiFi(排除自身热点) */ @@ -192,6 +208,7 @@ function isWifiStaConnected() { module.exports = { hasInternet, + hasSavedWifiConnection, isWifiStaConnected, getWifiIface, scanWifi, diff --git a/lib/provisioning.js b/lib/provisioning.js index 0c083b3..4d3710a 100644 --- a/lib/provisioning.js +++ b/lib/provisioning.js @@ -2,16 +2,19 @@ const EventEmitter = require('events'); const log = require('./logger'); -const { hasInternet, isWifiStaConnected, startAP, stopAP, connectWifi, AP_IP } = require('./network'); +const { hasInternet, hasSavedWifiConnection, isWifiStaConnected, startAP, stopAP, connectWifi, AP_IP } = require('./network'); const { DnsHijack } = require('./dns-hijack'); const { CaptiveServer } = require('./captive-server'); const MONITOR_INTERVAL_MS = 30_000; +const BOOT_WAIT_MAX_MS = 20_000; // 等待 NM 自动连接的最大时间 +const BOOT_POLL_MS = 2_000; // 轮询间隔 /** * AP 常驻配网管理器。 * * 规则: + * - 启动时:有已保存 WiFi 配置 → 等 NM 自动连接(最多 20 秒) * - wlan0 没有以 STA 模式连接 WiFi → 开 AP + DNS 劫持 + HTTP 配网页 * - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则重新开 AP * - 运行中 WiFi 断开 → 自动重新开 AP @@ -27,20 +30,62 @@ class ProvisionManager extends EventEmitter { this._monitorTimer = null; } - start() { + async start() { + // WiFi 已连接 → 直接进入 STA 模式 if (isWifiStaConnected()) { this._state = 'sta'; log.info('provision', 'WiFi STA 已连接,AP 不启动'); - } else { - this._enterAP(); + this._emitNetworkReady(); + this._startMonitor(); + return; } + + // 有已保存的 WiFi 配置 → 等 NM 自动连接(重启场景) + if (hasSavedWifiConnection()) { + log.info('provision', '发现已保存的 WiFi 配置,等待 NetworkManager 自动连接...'); + const connected = await this._waitForWifiConnect(); + if (connected) { + this._state = 'sta'; + log.info('provision', 'WiFi 自动连接成功,AP 不启动'); + this._emitNetworkReady(); + this._startMonitor(); + return; + } + log.warn('provision', 'WiFi 自动连接超时,启动 AP'); + } + + // 没有已保存 WiFi 或等待超时 → 开 AP + this._enterAP(); this._startMonitor(); if (hasInternet()) { - this.emit('network-ready'); + this._emitNetworkReady(); } } + _emitNetworkReady() { + if (hasInternet()) this.emit('network-ready'); + } + + /** + * 轮询等待 NM 自动连接 WiFi,最多等 BOOT_WAIT_MAX_MS + */ + _waitForWifiConnect() { + return new Promise(resolve => { + let elapsed = 0; + const timer = setInterval(() => { + elapsed += BOOT_POLL_MS; + if (isWifiStaConnected()) { + clearInterval(timer); + resolve(true); + } else if (elapsed >= BOOT_WAIT_MAX_MS) { + clearInterval(timer); + resolve(false); + } + }, BOOT_POLL_MS); + }); + } + stop() { this._stopMonitor(); this._stopAll();