fix(network): robust AP->STA connect (nmcli argv, device show state, wifi iface for DNS)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'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');
|
||||||
@@ -139,17 +139,27 @@ function hasInternet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取默认 WiFi 接口名(wlan0 等)
|
* 获取默认 WiFi 接口名(wlan0 等)。
|
||||||
|
* 必须 TYPE 精确为 wifi,不能用 grep wifi(会误匹配 wifi-p2p,导致选到 p2p-dev-wlan0,STA/热点均失败)。
|
||||||
*/
|
*/
|
||||||
function getWifiIface() {
|
function getWifiIface() {
|
||||||
if (AP_IFACE) return AP_IFACE;
|
if (AP_IFACE) return AP_IFACE;
|
||||||
try {
|
try {
|
||||||
const out = run('nmcli -t -f DEVICE,TYPE device | grep wifi | head -1');
|
const out = run('nmcli -t -f DEVICE,TYPE device');
|
||||||
const iface = out.split(':')[0].trim();
|
let fallback = '';
|
||||||
if (iface) return iface;
|
for (const line of out.split('\n')) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
const parts = line.split(':');
|
||||||
|
const dev = (parts[0] || '').trim();
|
||||||
|
const type = (parts[1] || '').trim();
|
||||||
|
if (type !== 'wifi' || !dev) continue;
|
||||||
|
if (dev.startsWith('p2p-dev-')) continue;
|
||||||
|
if (dev.startsWith('wlan')) return dev;
|
||||||
|
if (!fallback) fallback = dev;
|
||||||
|
}
|
||||||
|
if (fallback) return fallback;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// 兜底
|
|
||||||
try {
|
try {
|
||||||
const out = run("ls /sys/class/net | grep -E '^wl'");
|
const out = run("ls /sys/class/net | grep -E '^wl'");
|
||||||
const iface = out.split('\n')[0].trim();
|
const iface = out.split('\n')[0].trim();
|
||||||
@@ -197,19 +207,42 @@ function scanWifi() {
|
|||||||
const CONNECT_WIFI_STA_WAIT_MS = 25_000;
|
const CONNECT_WIFI_STA_WAIT_MS = 25_000;
|
||||||
const CONNECT_WIFI_STA_POLL_MS = 1_000;
|
const CONNECT_WIFI_STA_POLL_MS = 1_000;
|
||||||
|
|
||||||
|
/** 不走 shell,避免 SSID/密码中的引号、空格、$ 等破坏命令 */
|
||||||
|
function nmcliSync(args, timeoutMs = 60000) {
|
||||||
|
const r = spawnSync('nmcli', args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: timeoutMs,
|
||||||
|
maxBuffer: 2 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (r.error) throw r.error;
|
||||||
|
if (r.status !== 0) {
|
||||||
|
const msg = (r.stderr || '').trim() || (r.stdout || '').trim() || `nmcli exit ${r.status}`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return (r.stdout || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接指定 WiFi(配网场景:成功 = NM 显示 STA 已连上目标网,不要求一定能 ping 通 8.8.8.8)
|
* 连接指定 WiFi(配网场景:成功 = NM 显示 STA 已连上目标网,不要求一定能 ping 通 8.8.8.8)
|
||||||
* @returns {{ success: boolean, error?: string }}
|
* @returns {{ success: boolean, error?: string }}
|
||||||
*/
|
*/
|
||||||
function connectWifi(ssid, password) {
|
function connectWifi(ssid, password) {
|
||||||
const iface = getWifiIface();
|
const iface = getWifiIface();
|
||||||
log.info('network', `尝试连接 WiFi: ${ssid}`);
|
log.info('network', `尝试连接 WiFi: ${ssid}(ifname=${iface})`);
|
||||||
try {
|
try {
|
||||||
// 先删除可能残留的同名连接
|
try {
|
||||||
try { run(`nmcli connection delete "${ssid}"`); } catch (_) {}
|
nmcliSync(['connection', 'delete', ssid], 15000);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
const pwdArg = password ? `password "${password}"` : '';
|
// 关热点后部分固件需显式保证由 NM 管理、再关联
|
||||||
run(`nmcli device wifi connect "${ssid}" ${pwdArg} ifname ${iface}`, 60000);
|
try {
|
||||||
|
nmcliSync(['device', 'set', iface, 'managed', 'yes'], 8000);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
const args = ['device', 'wifi', 'connect', ssid];
|
||||||
|
if (password) args.push('password', password);
|
||||||
|
args.push('ifname', iface);
|
||||||
|
nmcliSync(args, 120000);
|
||||||
|
|
||||||
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) {
|
||||||
@@ -313,24 +346,24 @@ function hasSavedWifiConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测 wlan0 是否以 STA 模式连接了 WiFi(排除自身热点)
|
* 是否已以 STA 连上某 WiFi(排除自身热点)。
|
||||||
|
* 不用 device 列表按 `:` 拆字段(连接名含冒号会错;state 含 connecting 勿误匹配 connected)。
|
||||||
*/
|
*/
|
||||||
function isWifiStaConnected() {
|
function isWifiStaConnected() {
|
||||||
const iface = getWifiIface();
|
const iface = getWifiIface();
|
||||||
|
let state;
|
||||||
|
let conn;
|
||||||
try {
|
try {
|
||||||
const out = run('nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device');
|
state = nmcliSync(['-g', 'GENERAL.STATE', 'device', 'show', iface], 8000);
|
||||||
for (const line of out.split('\n')) {
|
conn = nmcliSync(['-g', 'GENERAL.CONNECTION', 'device', 'show', iface], 8000);
|
||||||
const parts = line.split(':');
|
} catch (_) {
|
||||||
const dev = (parts[0] || '').trim();
|
|
||||||
const type = (parts[1] || '').trim();
|
|
||||||
const state = (parts[2] || '').trim();
|
|
||||||
const conn = (parts[3] || '').trim();
|
|
||||||
if (dev === iface && type === 'wifi' && state === 'connected') {
|
|
||||||
return conn !== CON_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
const s = (state || '').trim();
|
||||||
|
const c = (conn || '').trim();
|
||||||
|
if (!/\(connected\)/.test(s)) return false;
|
||||||
|
if (!c || c === CON_NAME) return false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,10 +162,11 @@ class ProvisionManager extends EventEmitter {
|
|||||||
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
|
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
|
||||||
led.blink(); // 正在连接 → 闪烁
|
led.blink(); // 正在连接 → 闪烁
|
||||||
|
|
||||||
|
try {
|
||||||
this._stopAPServices();
|
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);
|
||||||
|
|
||||||
@@ -177,8 +179,23 @@ class ProvisionManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
|
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
|
||||||
this._enterAP();
|
this._safeReenterAP();
|
||||||
return result;
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
log.error('provision', `配网过程异常: ${e.message}`);
|
||||||
|
this._safeReenterAP();
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重新开 AP;失败时勿把 _state 永久卡在 connecting */
|
||||||
|
_safeReenterAP() {
|
||||||
|
try {
|
||||||
|
this._enterAP();
|
||||||
|
} catch (e) {
|
||||||
|
log.error('provision', `重新启动 AP 失败: ${e.message}`);
|
||||||
|
this._state = 'idle';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
|
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user