fix(network): robust AP->STA connect (nmcli argv, device show state, wifi iface for DNS)

This commit is contained in:
stswangzhiping
2026-03-29 07:46:22 +08:00
parent 012ad90335
commit 4c16483ee7
2 changed files with 439 additions and 389 deletions

View File

@@ -1,374 +1,407 @@
'use strict'; 'use strict';
const { execSync } = require('child_process'); const { execSync, spawnSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const log = require('./logger'); const log = require('./logger');
const AP_SSID_PREFIX = 'ClawBox-'; const AP_SSID_PREFIX = 'ClawBox-';
const AP_IP = '10.42.0.1'; const AP_IP = '10.42.0.1';
const AP_PASSWORD = '12345678'; const AP_PASSWORD = '12345678';
const AP_IFACE = process.env.CLAWD_WIFI_IFACE || ''; const AP_IFACE = process.env.CLAWD_WIFI_IFACE || '';
const CON_NAME = 'clawd-hotspot'; const CON_NAME = 'clawd-hotspot';
/** 产品 RJ45 在 sysfs 中的默认名;等价于检测 `cat /sys/class/net/end0/carrier` */ /** 产品 RJ45 在 sysfs 中的默认名;等价于检测 `cat /sys/class/net/end0/carrier` */
const DEFAULT_ETH_IFACE = 'end0'; const DEFAULT_ETH_IFACE = 'end0';
function _ethIfaceEnvOrDefault() { function _ethIfaceEnvOrDefault() {
return process.env.CLAWD_ETH_IFACE || DEFAULT_ETH_IFACE; return process.env.CLAWD_ETH_IFACE || DEFAULT_ETH_IFACE;
} }
function _netIfaceExists(name) { function _netIfaceExists(name) {
try { try {
return fs.existsSync(`/sys/class/net/${name}`); return fs.existsSync(`/sys/class/net/${name}`);
} catch (_) { } catch (_) {
return false; return false;
} }
} }
/** 读取 `/sys/class/net/<iface>/carrier``1` 为链路 up缺失或异常视为 down */ /** 读取 `/sys/class/net/<iface>/carrier``1` 为链路 up缺失或异常视为 down */
function _sysfsCarrierUp(iface) { function _sysfsCarrierUp(iface) {
try { try {
return fs.readFileSync(`/sys/class/net/${iface}/carrier`, 'utf8').trim() === '1'; return fs.readFileSync(`/sys/class/net/${iface}/carrier`, 'utf8').trim() === '1';
} catch (_) { } catch (_) {
return false; return false;
} }
} }
/** 非 WiFi、非典型虚拟接口用于开发机扫描有线口enp* 等) */ /** 非 WiFi、非典型虚拟接口用于开发机扫描有线口enp* 等) */
function _isExcludedVirtualIface(name) { function _isExcludedVirtualIface(name) {
if (name === 'lo' || name === 'bonding_masters') return true; if (name === 'lo' || name === 'bonding_masters') return true;
if (name.startsWith('wl')) return true; if (name.startsWith('wl')) return true;
if (name.startsWith('docker')) return true; if (name.startsWith('docker')) return true;
if (name.startsWith('veth')) return true; if (name.startsWith('veth')) return true;
if (name.startsWith('virbr')) return true; if (name.startsWith('virbr')) return true;
if (name.startsWith('br-')) return true; if (name.startsWith('br-')) return true;
if (name.startsWith('tun') || name.startsWith('tap')) return true; if (name.startsWith('tun') || name.startsWith('tap')) return true;
if (name.startsWith('wg') || name.startsWith('bond')) return true; if (name.startsWith('wg') || name.startsWith('bond')) return true;
if (name.startsWith('can')) return true; if (name.startsWith('can')) return true;
return false; return false;
} }
/** /**
* 开发机:无 CLAWD_ETH_IFACE 且无 end0 时,扫描 sysfs 找第一个 carrier=1 的有线口。 * 开发机:无 CLAWD_ETH_IFACE 且无 end0 时,扫描 sysfs 找第一个 carrier=1 的有线口。
*/ */
function _firstScanWiredIfaceWithCarrier() { function _firstScanWiredIfaceWithCarrier() {
try { try {
const names = fs.readdirSync('/sys/class/net'); const names = fs.readdirSync('/sys/class/net');
for (const name of names.sort()) { for (const name of names.sort()) {
if (_isExcludedVirtualIface(name)) continue; if (_isExcludedVirtualIface(name)) continue;
if (_sysfsCarrierUp(name)) return name; if (_sysfsCarrierUp(name)) return name;
} }
} catch (_) {} } catch (_) {}
return null; return null;
} }
/** /**
* 返回当前可用于「有线 ping / 路由」的网卡名。 * 返回当前可用于「有线 ping / 路由」的网卡名。
* 优先级CLAWD_ETH_IFACE → 存在 end0 则只用 end0 → 否则扫描 sysfs。 * 优先级CLAWD_ETH_IFACE → 存在 end0 则只用 end0 → 否则扫描 sysfs。
*/ */
function getWiredIfaceWithCarrier() { function getWiredIfaceWithCarrier() {
const explicit = process.env.CLAWD_ETH_IFACE; const explicit = process.env.CLAWD_ETH_IFACE;
if (explicit) { if (explicit) {
return _netIfaceExists(explicit) && _sysfsCarrierUp(explicit) ? explicit : null; return _netIfaceExists(explicit) && _sysfsCarrierUp(explicit) ? explicit : null;
} }
if (_netIfaceExists(DEFAULT_ETH_IFACE)) { if (_netIfaceExists(DEFAULT_ETH_IFACE)) {
return _sysfsCarrierUp(DEFAULT_ETH_IFACE) ? DEFAULT_ETH_IFACE : null; return _sysfsCarrierUp(DEFAULT_ETH_IFACE) ? DEFAULT_ETH_IFACE : null;
} }
return _firstScanWiredIfaceWithCarrier(); return _firstScanWiredIfaceWithCarrier();
} }
function hasWiredCarrier() { function hasWiredCarrier() {
return getWiredIfaceWithCarrier() !== null; return getWiredIfaceWithCarrier() !== null;
} }
/** /**
* LAN 面板灯:只反映 RJ45 对应口,与 `cat /sys/class/net/end0/carrier 2>/dev/null` 同源(仅读 carrier * LAN 面板灯:只反映 RJ45 对应口,与 `cat /sys/class/net/end0/carrier 2>/dev/null` 同源(仅读 carrier
* 若配置的接口在 sysfs 中不存在(常见为开发机无 end0则退回与 hasWiredCarrier() 一致,避免灯永远灭。 * 若配置的接口在 sysfs 中不存在(常见为开发机无 end0则退回与 hasWiredCarrier() 一致,避免灯永远灭。
*/ */
function hasLanCableCarrier() { function hasLanCableCarrier() {
const iface = _ethIfaceEnvOrDefault(); const iface = _ethIfaceEnvOrDefault();
if (_netIfaceExists(iface)) return _sysfsCarrierUp(iface); if (_netIfaceExists(iface)) return _sysfsCarrierUp(iface);
return hasWiredCarrier(); return hasWiredCarrier();
} }
function _tryPingInternet() { function _tryPingInternet() {
try { try {
run('ping -c 1 -W 3 8.8.8.8'); run('ping -c 1 -W 3 8.8.8.8');
return true; return true;
} catch (_) {} } catch (_) {}
// 开热点时默认路由可能走 wlan无 -I 的 ping 会误判;指定有线口再试 // 开热点时默认路由可能走 wlan无 -I 的 ping 会误判;指定有线口再试
const wired = getWiredIfaceWithCarrier(); const wired = getWiredIfaceWithCarrier();
if (wired) { if (wired) {
try { try {
run(`ping -c 1 -W 3 -I ${wired} 8.8.8.8`); run(`ping -c 1 -W 3 -I ${wired} 8.8.8.8`);
return true; return true;
} catch (_) {} } catch (_) {}
} }
return false; return false;
} }
/** /**
* 仅经有线口 ping 公网(不依赖默认路由)。 * 仅经有线口 ping 公网(不依赖默认路由)。
* AP 开启时 hasInternet() 易误判;维持 WS / 网络监视时用此兜底。 * AP 开启时 hasInternet() 易误判;维持 WS / 网络监视时用此兜底。
*/ */
function hasWiredInternetProbe() { function hasWiredInternetProbe() {
const wired = getWiredIfaceWithCarrier(); const wired = getWiredIfaceWithCarrier();
if (!wired) return false; if (!wired) return false;
try { try {
run(`ping -c 1 -W 3 -I ${wired} 8.8.8.8`); run(`ping -c 1 -W 3 -I ${wired} 8.8.8.8`);
return true; return true;
} catch (_) {} } catch (_) {}
return false; return false;
} }
/** /**
* 检测是否有互联网连接nmcli 连通性 + ping 兜底) * 检测是否有互联网连接nmcli 连通性 + ping 兜底)
*/ */
function hasInternet() { function hasInternet() {
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 falsenmcli 有缓存,不可信) // 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 falsenmcli 有缓存,不可信)
if (!isWifiStaConnected() && !hasWiredCarrier()) return false; if (!isWifiStaConnected() && !hasWiredCarrier()) return false;
try { try {
const out = run('nmcli networking connectivity check').trim(); const out = run('nmcli networking connectivity check').trim();
if (out === 'full' || out === 'limited') return true; if (out === 'full' || out === 'limited') return true;
} catch (_) {} } catch (_) {}
return _tryPingInternet(); return _tryPingInternet();
} }
/** /**
* 获取默认 WiFi 接口名wlan0 等) * 获取默认 WiFi 接口名wlan0 等)
*/ * 必须 TYPE 精确为 wifi不能用 grep wifi会误匹配 wifi-p2p导致选到 p2p-dev-wlan0STA/热点均失败)。
function getWifiIface() { */
if (AP_IFACE) return AP_IFACE; function getWifiIface() {
try { if (AP_IFACE) return AP_IFACE;
const out = run('nmcli -t -f DEVICE,TYPE device | grep wifi | head -1'); try {
const iface = out.split(':')[0].trim(); const out = run('nmcli -t -f DEVICE,TYPE device');
if (iface) return iface; let fallback = '';
} catch (_) {} for (const line of out.split('\n')) {
if (!line.trim()) continue;
// 兜底 const parts = line.split(':');
try { const dev = (parts[0] || '').trim();
const out = run("ls /sys/class/net | grep -E '^wl'"); const type = (parts[1] || '').trim();
const iface = out.split('\n')[0].trim(); if (type !== 'wifi' || !dev) continue;
if (iface) return iface; if (dev.startsWith('p2p-dev-')) continue;
} catch (_) {} if (dev.startsWith('wlan')) return dev;
if (!fallback) fallback = dev;
return 'wlan0'; }
} if (fallback) return fallback;
} catch (_) {}
/**
* 扫描周围 WiFi返回 [{ ssid, signal, security }] try {
*/ const out = run("ls /sys/class/net | grep -E '^wl'");
function scanWifi() { const iface = out.split('\n')[0].trim();
const iface = getWifiIface(); if (iface) return iface;
try { } catch (_) {}
// 先触发一次扫描
try { run(`nmcli device wifi rescan ifname ${iface}`); } catch (_) {} return 'wlan0';
// 等扫描完成 }
sleep(2000);
/**
const out = run('nmcli -t -f SSID,SIGNAL,SECURITY device wifi list'); * 扫描周围 WiFi返回 [{ ssid, signal, security }]
const seen = new Set(); */
const results = []; function scanWifi() {
for (const line of out.split('\n')) { const iface = getWifiIface();
if (!line.trim()) continue; try {
const parts = line.split(':'); // 先触发一次扫描
const ssid = parts[0].trim().replace(/\\:/g, ':'); try { run(`nmcli device wifi rescan ifname ${iface}`); } catch (_) {}
if (!ssid || seen.has(ssid)) continue; // 等扫描完成
seen.add(ssid); sleep(2000);
results.push({
ssid, const out = run('nmcli -t -f SSID,SIGNAL,SECURITY device wifi list');
signal: parseInt(parts[1], 10) || 0, const seen = new Set();
security: parts.slice(2).join(':').trim() || 'Open', const results = [];
}); for (const line of out.split('\n')) {
} if (!line.trim()) continue;
results.sort((a, b) => b.signal - a.signal); const parts = line.split(':');
return results; const ssid = parts[0].trim().replace(/\\:/g, ':');
} catch (e) { if (!ssid || seen.has(ssid)) continue;
log.error('network', 'WiFi 扫描失败:', e.message); seen.add(ssid);
return []; results.push({
} ssid,
} signal: parseInt(parts[1], 10) || 0,
security: parts.slice(2).join(':').trim() || 'Open',
/** AP 切 STA 后等待网卡进入 connected 的最长时间(不依赖外网探测) */ });
const CONNECT_WIFI_STA_WAIT_MS = 25_000; }
const CONNECT_WIFI_STA_POLL_MS = 1_000; results.sort((a, b) => b.signal - a.signal);
return results;
/** } catch (e) {
* 连接指定 WiFi配网场景成功 = NM 显示 STA 已连上目标网,不要求一定能 ping 通 8.8.8.8 log.error('network', 'WiFi 扫描失败:', e.message);
* @returns {{ success: boolean, error?: string }} return [];
*/ }
function connectWifi(ssid, password) { }
const iface = getWifiIface();
log.info('network', `尝试连接 WiFi: ${ssid}`); /** AP 切 STA 后等待网卡进入 connected 的最长时间(不依赖外网探测) */
try { const CONNECT_WIFI_STA_WAIT_MS = 25_000;
// 先删除可能残留的同名连接 const CONNECT_WIFI_STA_POLL_MS = 1_000;
try { run(`nmcli connection delete "${ssid}"`); } catch (_) {}
/** 不走 shell避免 SSID/密码中的引号、空格、$ 等破坏命令 */
const pwdArg = password ? `password "${password}"` : ''; function nmcliSync(args, timeoutMs = 60000) {
run(`nmcli device wifi connect "${ssid}" ${pwdArg} ifname ${iface}`, 60000); const r = spawnSync('nmcli', args, {
encoding: 'utf8',
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS; timeout: timeoutMs,
while (Date.now() < deadline) { maxBuffer: 2 * 1024 * 1024,
if (isWifiStaConnected()) { });
if (hasInternet()) { if (r.error) throw r.error;
log.info('network', `WiFi 已连接且有外网: ${ssid}`); if (r.status !== 0) {
} else { const msg = (r.stderr || '').trim() || (r.stdout || '').trim() || `nmcli exit ${r.status}`;
log.warn( throw new Error(msg);
'network', }
`WiFi STA 已连接(${ssid}),暂未检测到外网;配网仍视为成功(内网/防火墙/国内 DNS 常见)`, return (r.stdout || '').trim();
); }
}
return { success: true }; /**
} * 连接指定 WiFi配网场景成功 = NM 显示 STA 已连上目标网,不要求一定能 ping 通 8.8.8.8
sleep(CONNECT_WIFI_STA_POLL_MS); * @returns {{ success: boolean, error?: string }}
} */
return { success: false, error: '超时:网卡未进入已连接状态' }; function connectWifi(ssid, password) {
} catch (e) { const iface = getWifiIface();
log.error('network', `WiFi 连接失败: ${e.message}`); log.info('network', `尝试连接 WiFi: ${ssid}ifname=${iface}`);
return { success: false, error: e.message }; try {
} try {
} nmcliSync(['connection', 'delete', ssid], 15000);
} catch (_) {}
/**
* 启动 WiFi AP 热点 // 关热点后部分固件需显式保证由 NM 管理、再关联
*/ try {
function startAP(clawId) { nmcliSync(['device', 'set', iface, 'managed', 'yes'], 8000);
const iface = getWifiIface(); } catch (_) {}
const ssid = `${AP_SSID_PREFIX}${clawId || 'Setup'}`;
const args = ['device', 'wifi', 'connect', ssid];
log.info('network', `启动 AP 热点: ${ssid} (${iface})`); if (password) args.push('password', password);
args.push('ifname', iface);
// 关闭已有热点 nmcliSync(args, 120000);
stopAP();
const deadline = Date.now() + CONNECT_WIFI_STA_WAIT_MS;
try { while (Date.now() < deadline) {
// nmcli 创建热点(开放网络) if (isWifiStaConnected()) {
const cmd = [ if (hasInternet()) {
'nmcli device wifi hotspot', log.info('network', `WiFi 已连接且有外网: ${ssid}`);
`ifname ${iface}`, } else {
`con-name ${CON_NAME}`, log.warn(
`ssid "${ssid}"`, 'network',
'band bg', `WiFi STA 已连接(${ssid}),暂未检测到外网;配网仍视为成功(内网/防火墙/国内 DNS 常见)`,
]; );
// 如果需要密码 }
if (AP_PASSWORD) { return { success: true };
cmd.push(`password "${AP_PASSWORD}"`); }
} sleep(CONNECT_WIFI_STA_POLL_MS);
run(cmd.join(' ')); }
return { success: false, error: '超时:网卡未进入已连接状态' };
// 等待 AP 启动 } catch (e) {
sleep(2000); log.error('network', `WiFi 连接失败: ${e.message}`);
log.info('network', `AP 已启动: ${ssid}, 网关 ${AP_IP}`); return { success: false, error: e.message };
return { ssid, ip: AP_IP, iface }; }
} catch (e) { }
log.error('network', `AP 启动失败: ${e.message}`);
throw e; /**
} * 启动 WiFi AP 热点
} */
function startAP(clawId) {
/** const iface = getWifiIface();
* 关闭热点,恢复普通 WiFi 模式 const ssid = `${AP_SSID_PREFIX}${clawId || 'Setup'}`;
*/
function stopAP() { log.info('network', `启动 AP 热点: ${ssid} (${iface})`);
try {
run(`nmcli connection down ${CON_NAME}`); // 关闭已有热点
} catch (_) {} stopAP();
try {
run(`nmcli connection delete ${CON_NAME}`); try {
} catch (_) {} // nmcli 创建热点(开放网络)
} const cmd = [
'nmcli device wifi hotspot',
// ── 工具 ───────────────────────────────────────────────────────────────────── `ifname ${iface}`,
`con-name ${CON_NAME}`,
function run(cmd, timeout = 10000) { `ssid "${ssid}"`,
return execSync(cmd, { 'band bg',
encoding: 'utf8', ];
timeout, // 如果需要密码
stdio: ['ignore', 'pipe', 'pipe'], if (AP_PASSWORD) {
}); cmd.push(`password "${AP_PASSWORD}"`);
} }
run(cmd.join(' '));
function sleep(ms) {
execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 }); // 等待 AP 启动
} sleep(2000);
log.info('network', `AP 已启动: ${ssid}, 网关 ${AP_IP}`);
/** return { ssid, ip: AP_IP, iface };
* 检测是否有已保存的 WiFi STA 连接(排除自身热点) } catch (e) {
*/ log.error('network', `AP 启动失败: ${e.message}`);
function hasSavedWifiConnection() { throw e;
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) { * 关闭热点,恢复普通 WiFi 模式
return true; */
} function stopAP() {
} try {
} catch (_) {} run(`nmcli connection down ${CON_NAME}`);
return false; } catch (_) {}
} try {
run(`nmcli connection delete ${CON_NAME}`);
/** } catch (_) {}
* 检测 wlan0 是否以 STA 模式连接了 WiFi排除自身热点 }
*/
function isWifiStaConnected() { // ── 工具 ─────────────────────────────────────────────────────────────────────
const iface = getWifiIface();
try { function run(cmd, timeout = 10000) {
const out = run('nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device'); return execSync(cmd, {
for (const line of out.split('\n')) { encoding: 'utf8',
const parts = line.split(':'); timeout,
const dev = (parts[0] || '').trim(); stdio: ['ignore', 'pipe', 'pipe'],
const type = (parts[1] || '').trim(); });
const state = (parts[2] || '').trim(); }
const conn = (parts[3] || '').trim();
if (dev === iface && type === 'wifi' && state === 'connected') { function sleep(ms) {
return conn !== CON_NAME; execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 });
} }
}
} catch (_) {} /**
return false; * 检测是否有已保存的 WiFi STA 连接(排除自身热点)
} */
function hasSavedWifiConnection() {
/** try {
* 获取本机所有非回环 IPv4 地址,逗号拼接返回 const out = run('nmcli -t -f NAME,TYPE connection show');
* 例:'192.168.1.100' 或 '192.168.1.100,10.0.0.5' for (const line of out.split('\n')) {
*/ const [name, type] = line.split(':');
function getLocalIps() { if (type === '802-11-wireless' && name !== CON_NAME) {
try { return true;
const ifaces = os.networkInterfaces(); }
const ips = []; }
for (const [name, addrs] of Object.entries(ifaces)) { } catch (_) {}
if (!addrs) continue; return false;
for (const addr of addrs) { }
if (addr.family === 'IPv4' && !addr.internal && !addr.address.startsWith('10.42.')) {
ips.push(addr.address); /**
} * 是否已以 STA 连上某 WiFi排除自身热点
} * 不用 device 列表按 `:` 拆字段连接名含冒号会错state 含 connecting 勿误匹配 connected
} */
return ips.length > 0 ? ips.join(',') : null; function isWifiStaConnected() {
} catch (e) { const iface = getWifiIface();
log.warn('network', '获取本机 IP 失败:', e.message); let state;
return null; let conn;
} try {
} state = nmcliSync(['-g', 'GENERAL.STATE', 'device', 'show', iface], 8000);
conn = nmcliSync(['-g', 'GENERAL.CONNECTION', 'device', 'show', iface], 8000);
module.exports = { } catch (_) {
hasInternet, return false;
hasWiredCarrier, }
hasLanCableCarrier, const s = (state || '').trim();
hasWiredInternetProbe, const c = (conn || '').trim();
getWiredIfaceWithCarrier, if (!/\(connected\)/.test(s)) return false;
hasSavedWifiConnection, if (!c || c === CON_NAME) return false;
isWifiStaConnected, return true;
getWifiIface, }
scanWifi,
connectWifi, /**
startAP, * 获取本机所有非回环 IPv4 地址,逗号拼接返回
stopAP, * 例:'192.168.1.100' 或 '192.168.1.100,10.0.0.5'
AP_IP, */
getLocalIps, function getLocalIps() {
}; try {
const ifaces = os.networkInterfaces();
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;
} catch (e) {
log.warn('network', '获取本机 IP 失败:', e.message);
return null;
}
}
module.exports = {
hasInternet,
hasWiredCarrier,
hasLanCableCarrier,
hasWiredInternetProbe,
getWiredIfaceWithCarrier,
hasSavedWifiConnection,
isWifiStaConnected,
getWifiIface,
scanWifi,
connectWifi,
startAP,
stopAP,
AP_IP,
getLocalIps,
};

View File

@@ -2,7 +2,7 @@
const EventEmitter = require('events'); const EventEmitter = require('events');
const log = require('./logger'); const log = require('./logger');
const { hasInternet, hasSavedWifiConnection, isWifiStaConnected, scanWifi, startAP, stopAP, connectWifi, AP_IP } = require('./network'); const { hasInternet, hasSavedWifiConnection, 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');
@@ -131,9 +131,9 @@ class ProvisionManager extends EventEmitter {
this._cachedWifiList = scanWifi(); this._cachedWifiList = scanWifi();
log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络`); log.info('provision', `扫描到 ${this._cachedWifiList.length} 个网络`);
// 写 DNS 劫持配置NM 启动热点时加载) // 写 DNS 劫持配置NM 启动热点时加载);接口名与热点一致,勿写死 wlan0
this._dns = new DnsHijack(); this._dns = new DnsHijack();
this._dns.start('wlan0', AP_IP); this._dns.start(getWifiIface(), AP_IP);
const ap = startAP(this._clawId); const ap = startAP(this._clawId);
@@ -149,6 +149,7 @@ class ProvisionManager extends EventEmitter {
log.info('provision', `配网地址: http://10.42.0.1`); log.info('provision', `配网地址: http://10.42.0.1`);
} catch (e) { } catch (e) {
log.error('provision', `AP 启动失败: ${e.message}`); log.error('provision', `AP 启动失败: ${e.message}`);
if (this._state !== 'sta') this._state = 'idle';
} }
} }
@@ -161,24 +162,40 @@ class ProvisionManager extends EventEmitter {
log.info('provision', `用户请求连接 WiFi: ${ssid}`); log.info('provision', `用户请求连接 WiFi: ${ssid}`);
led.blink(); // 正在连接 → 闪烁 led.blink(); // 正在连接 → 闪烁
this._stopAPServices(); try {
this._stopAPServices();
// 关热点后射频/模式切换需要时间,立刻 connect 在部分板子上会失败 // 关热点后射频/模式切换需要时间,立刻 connect 在部分板子上会失败
await new Promise((r) => setTimeout(r, 2500)); await new Promise((r) => setTimeout(r, 3500));
const result = connectWifi(ssid, password); const result = connectWifi(ssid, password);
if (result.success) { if (result.success) {
this._state = 'sta'; this._state = 'sta';
log.info('provision', `WiFi 已连接: ${ssid}`); log.info('provision', `WiFi 已连接: ${ssid}`);
led.on(); // WiFi 灯:连接成功 → 常亮 led.on(); // WiFi 灯:连接成功 → 常亮
this.emit('network-ready'); this.emit('network-ready');
return result;
}
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
this._safeReenterAP();
return result; return result;
} catch (e) {
log.error('provision', `配网过程异常: ${e.message}`);
this._safeReenterAP();
return { success: false, error: e.message };
} }
}
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`); /** 重新开 AP失败时勿把 _state 永久卡在 connecting */
this._enterAP(); _safeReenterAP() {
return result; try {
this._enterAP();
} catch (e) {
log.error('provision', `重新启动 AP 失败: ${e.message}`);
this._state = 'idle';
}
} }
// ── WiFi 状态监控 ───────────────────────────────────────────────────────── // ── WiFi 状态监控 ─────────────────────────────────────────────────────────