fix(network): robust AP->STA connect (nmcli argv, device show state, wifi iface for DNS)
This commit is contained in:
781
lib/network.js
781
lib/network.js
@@ -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 → 立即 false(nmcli 有缓存,不可信)
|
// 物理层快检:无 WiFi STA 且无任何有线 carrier → 立即 false(nmcli 有缓存,不可信)
|
||||||
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-wlan0,STA/热点均失败)。
|
||||||
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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 状态监控 ─────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user