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
This commit is contained in:
16
.commitmsg
16
.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:
|
After WiFi is configured and device reboots, NetworkManager needs
|
||||||
- AP hotspot starts at boot regardless of eth0 status
|
a few seconds to auto-connect to saved WiFi. Without waiting,
|
||||||
- Captive portal runs alongside AP for WiFi configuration
|
AP starts immediately and occupies wlan0, preventing NM reconnect.
|
||||||
- AP automatically shuts down only when WiFi STA connects
|
|
||||||
- WiFi drops at runtime -> AP auto-restarts
|
- Add hasSavedWifiConnection() to check for saved WiFi profiles
|
||||||
- WiFi connect fails -> AP auto-restarts for retry
|
- Wait up to 20s for NM auto-connect before falling back to AP
|
||||||
- client.js no longer blocks on network; connects WS when ready
|
- First boot (no saved WiFi) still starts AP immediately
|
||||||
|
|||||||
@@ -60,21 +60,25 @@ class ClawClient {
|
|||||||
async start() {
|
async start() {
|
||||||
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
||||||
|
|
||||||
// 后台启动 AP 配网管理器(WiFi 未连接时常驻热点)
|
// 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP)
|
||||||
this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
|
this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
|
||||||
this._provisionMgr.start();
|
|
||||||
|
|
||||||
// 有网络时直接连接云端
|
// 网络就绪时连接云端
|
||||||
if (hasInternet()) {
|
this._provisionMgr.once('network-ready', () => {
|
||||||
await this._proceedWithConnection();
|
if (!this._ws) {
|
||||||
} else {
|
|
||||||
// 等待配网完成后再连
|
|
||||||
log.info('clawd', '等待网络就绪...');
|
|
||||||
this._provisionMgr.once('network-ready', () => {
|
|
||||||
this._proceedWithConnection().catch(e => {
|
this._proceedWithConnection().catch(e => {
|
||||||
log.error('clawd', '连接启动失败:', e.message);
|
log.error('clawd', '连接启动失败:', e.message);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._provisionMgr.start();
|
||||||
|
|
||||||
|
// start() 返回后,如果已有网络,直接连
|
||||||
|
if (hasInternet() && !this._ws) {
|
||||||
|
await this._proceedWithConnection();
|
||||||
|
} else if (!hasInternet()) {
|
||||||
|
log.info('clawd', '等待网络就绪(WiFi 配网或网线接入)...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,22 @@ function sleep(ms) {
|
|||||||
execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 });
|
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(排除自身热点)
|
* 检测 wlan0 是否以 STA 模式连接了 WiFi(排除自身热点)
|
||||||
*/
|
*/
|
||||||
@@ -192,6 +208,7 @@ function isWifiStaConnected() {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hasInternet,
|
hasInternet,
|
||||||
|
hasSavedWifiConnection,
|
||||||
isWifiStaConnected,
|
isWifiStaConnected,
|
||||||
getWifiIface,
|
getWifiIface,
|
||||||
scanWifi,
|
scanWifi,
|
||||||
|
|||||||
@@ -2,16 +2,19 @@
|
|||||||
|
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const log = require('./logger');
|
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 { DnsHijack } = require('./dns-hijack');
|
||||||
const { CaptiveServer } = require('./captive-server');
|
const { CaptiveServer } = require('./captive-server');
|
||||||
|
|
||||||
const MONITOR_INTERVAL_MS = 30_000;
|
const MONITOR_INTERVAL_MS = 30_000;
|
||||||
|
const BOOT_WAIT_MAX_MS = 20_000; // 等待 NM 自动连接的最大时间
|
||||||
|
const BOOT_POLL_MS = 2_000; // 轮询间隔
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AP 常驻配网管理器。
|
* AP 常驻配网管理器。
|
||||||
*
|
*
|
||||||
* 规则:
|
* 规则:
|
||||||
|
* - 启动时:有已保存 WiFi 配置 → 等 NM 自动连接(最多 20 秒)
|
||||||
* - wlan0 没有以 STA 模式连接 WiFi → 开 AP + DNS 劫持 + HTTP 配网页
|
* - wlan0 没有以 STA 模式连接 WiFi → 开 AP + DNS 劫持 + HTTP 配网页
|
||||||
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则重新开 AP
|
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则重新开 AP
|
||||||
* - 运行中 WiFi 断开 → 自动重新开 AP
|
* - 运行中 WiFi 断开 → 自动重新开 AP
|
||||||
@@ -27,20 +30,62 @@ class ProvisionManager extends EventEmitter {
|
|||||||
this._monitorTimer = null;
|
this._monitorTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
async start() {
|
||||||
|
// WiFi 已连接 → 直接进入 STA 模式
|
||||||
if (isWifiStaConnected()) {
|
if (isWifiStaConnected()) {
|
||||||
this._state = 'sta';
|
this._state = 'sta';
|
||||||
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||||
} else {
|
this._emitNetworkReady();
|
||||||
this._enterAP();
|
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();
|
this._startMonitor();
|
||||||
|
|
||||||
if (hasInternet()) {
|
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() {
|
stop() {
|
||||||
this._stopMonitor();
|
this._stopMonitor();
|
||||||
this._stopAll();
|
this._stopAll();
|
||||||
|
|||||||
Reference in New Issue
Block a user