feat: AP always-on mode - hotspot stays until WiFi STA connects
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 Made-with: Cursor
This commit is contained in:
12
.commitmsg
12
.commitmsg
@@ -1,5 +1,9 @@
|
|||||||
fix: clawd-rfkill.service failed due to systemd $ expansion
|
feat: AP always-on mode - hotspot stays until WiFi STA connects
|
||||||
|
|
||||||
systemd expands $rf as env var (empty), breaking the inline script.
|
Redesign provisioning from one-shot blocking to persistent background manager:
|
||||||
Move rfkill logic to standalone script to avoid escaping issues.
|
- AP hotspot starts at boot regardless of eth0 status
|
||||||
Also use quoted heredoc to prevent bash expansion in install.sh.
|
- 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
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { scanWifi, connectWifi } = require('./network');
|
const { scanWifi } = require('./network');
|
||||||
const { CAPTIVE_DOMAIN } = require('./dns-hijack');
|
const { CAPTIVE_DOMAIN } = require('./dns-hijack');
|
||||||
|
|
||||||
const PORT = 80;
|
const PORT = 80;
|
||||||
|
|
||||||
// iOS / Android / Windows Captive Portal 检测路径
|
|
||||||
const CAPTIVE_DETECT_PATHS = new Set([
|
const CAPTIVE_DETECT_PATHS = new Set([
|
||||||
'/hotspot-detect.html', // iOS
|
'/hotspot-detect.html', // iOS
|
||||||
'/library/test/success.html', // iOS older
|
'/library/test/success.html', // iOS older
|
||||||
@@ -20,12 +19,12 @@ const CAPTIVE_DETECT_PATHS = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配网 HTTP 服务器。
|
* 配网 HTTP 服务器(回调模式)。
|
||||||
*
|
*
|
||||||
* 路由:
|
* 路由:
|
||||||
* GET / → 配网页面(HTML)
|
* GET / → 配网页面(HTML)
|
||||||
* GET /api/scan → WiFi 扫描结果 JSON
|
* GET /api/scan → WiFi 扫描结果 JSON
|
||||||
* POST /api/connect → 提交 WiFi 凭证,尝试连接
|
* POST /api/connect → 提交 WiFi 凭证,触发 onConnect 回调
|
||||||
* GET /api/status → 当前连接状态
|
* GET /api/status → 当前连接状态
|
||||||
* Captive Portal 检测 → 302 重定向到配网页
|
* Captive Portal 检测 → 302 重定向到配网页
|
||||||
*/
|
*/
|
||||||
@@ -33,16 +32,10 @@ class CaptiveServer {
|
|||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
this._server = null;
|
this._server = null;
|
||||||
this._clawId = opts.clawId || '???';
|
this._clawId = opts.clawId || '???';
|
||||||
this._resolve = null; // provisioning 等待配网完成的 resolve
|
this._onConnect = opts.onConnect || null; // (ssid, password) => Promise<{success, error?}>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
startListening() {
|
||||||
* 启动 HTTP 服务器,返回 Promise,配网成功后 resolve。
|
|
||||||
*/
|
|
||||||
start() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this._resolve = resolve;
|
|
||||||
|
|
||||||
this._server = http.createServer((req, res) => {
|
this._server = http.createServer((req, res) => {
|
||||||
this._handle(req, res).catch(e => {
|
this._handle(req, res).catch(e => {
|
||||||
log.error('http', `${req.method} ${req.url} 异常:`, e.message);
|
log.error('http', `${req.method} ${req.url} 异常:`, e.message);
|
||||||
@@ -57,12 +50,13 @@ class CaptiveServer {
|
|||||||
|
|
||||||
this._server.on('error', (e) => {
|
this._server.on('error', (e) => {
|
||||||
if (e.code === 'EACCES') {
|
if (e.code === 'EACCES') {
|
||||||
log.error('http', `端口 ${PORT} 无权限,请以 root 运行或改用高端口`);
|
log.error('http', `端口 ${PORT} 无权限,请以 root 运行`);
|
||||||
|
} else if (e.code === 'EADDRINUSE') {
|
||||||
|
log.error('http', `端口 ${PORT} 已被占用`);
|
||||||
} else {
|
} else {
|
||||||
log.error('http', '服务器错误:', e.message);
|
log.error('http', '服务器错误:', e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
@@ -76,14 +70,12 @@ class CaptiveServer {
|
|||||||
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
|
|
||||||
// Captive Portal 检测请求 → 302 到配网页
|
|
||||||
if (CAPTIVE_DETECT_PATHS.has(pathname)) {
|
if (CAPTIVE_DETECT_PATHS.has(pathname)) {
|
||||||
res.writeHead(302, { Location: `http://${CAPTIVE_DOMAIN}/` });
|
res.writeHead(302, { Location: `http://${CAPTIVE_DOMAIN}/` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 路由
|
|
||||||
if (pathname === '/api/scan' && req.method === 'GET') {
|
if (pathname === '/api/scan' && req.method === 'GET') {
|
||||||
return this._apiScan(req, res);
|
return this._apiScan(req, res);
|
||||||
}
|
}
|
||||||
@@ -94,7 +86,6 @@ class CaptiveServer {
|
|||||||
return this._apiStatus(req, res);
|
return this._apiStatus(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认返回配网页面
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'text/html; charset=utf-8',
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
@@ -125,21 +116,16 @@ class CaptiveServer {
|
|||||||
|
|
||||||
log.info('http', `用户提交配网: ssid=${ssid}`);
|
log.info('http', `用户提交配网: ssid=${ssid}`);
|
||||||
|
|
||||||
// 先返回 "尝试中" 让前端轮询 /api/status
|
// 先返回响应,让手机端知道设备收到了请求
|
||||||
this._json(res, { success: true, message: '正在连接...' });
|
// (AP 即将关闭,手机会断开连接)
|
||||||
|
this._json(res, { success: true, message: '正在连接,AP 将临时关闭...' });
|
||||||
|
|
||||||
// 异步连接 WiFi(会关闭 AP,客户端会断开)
|
// 延迟执行,确保 HTTP 响应送达
|
||||||
setTimeout(async () => {
|
if (this._onConnect) {
|
||||||
const result = connectWifi(ssid, password || '');
|
setTimeout(() => {
|
||||||
if (result.success && this._resolve) {
|
this._onConnect(ssid, password || '');
|
||||||
log.info('http', '配网成功,退出配网模式');
|
}, 1000);
|
||||||
this._resolve({ ssid });
|
|
||||||
this._resolve = null;
|
|
||||||
} else {
|
|
||||||
log.warn('http', `配网失败: ${result.error},重新启动 AP`);
|
|
||||||
// provisioning.js 会处理重新进入 AP
|
|
||||||
}
|
}
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_apiStatus(req, res) {
|
_apiStatus(req, res) {
|
||||||
@@ -257,19 +243,18 @@ async function doConnect(){
|
|||||||
var pw=$('password').value;
|
var pw=$('password').value;
|
||||||
if(!ssid){setStatus('请选择或输入 WiFi','err');return}
|
if(!ssid){setStatus('请选择或输入 WiFi','err');return}
|
||||||
$('connectBtn').disabled=true;
|
$('connectBtn').disabled=true;
|
||||||
setStatus('正在连接 '+ssid+' ...','info');
|
setStatus('正在连接 '+ssid+' ... 热点将临时关闭','info');
|
||||||
try{
|
try{
|
||||||
var r=await fetch('/api/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ssid:ssid,password:pw})});
|
var r=await fetch('/api/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ssid:ssid,password:pw})});
|
||||||
var d=await r.json();
|
var d=await r.json();
|
||||||
if(d.success){
|
if(d.success){
|
||||||
setStatus('✓ 正在连接,请稍候... 设备将自动重启网络','ok');
|
setStatus('✓ 设备正在连接 WiFi,热点将关闭。如连接失败,热点会自动恢复,请重新连接配网。','ok');
|
||||||
setTimeout(function(){setStatus('✓ 配网成功!您可以断开此热点','ok')},8000);
|
|
||||||
}else{
|
}else{
|
||||||
setStatus('连接失败: '+(d.error||'未知错误'),'err');
|
setStatus('失败: '+(d.error||'未知错误'),'err');
|
||||||
$('connectBtn').disabled=false;
|
$('connectBtn').disabled=false;
|
||||||
}
|
}
|
||||||
}catch(e){
|
}catch(e){
|
||||||
setStatus('连接失败: '+e.message,'err');
|
setStatus('请求失败: '+e.message,'err');
|
||||||
$('connectBtn').disabled=false;
|
$('connectBtn').disabled=false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const log = require('./logger');
|
|||||||
const { getBoxId } = require('./fingerprint');
|
const { getBoxId } = require('./fingerprint');
|
||||||
const { collect } = require('./metrics');
|
const { collect } = require('./metrics');
|
||||||
const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc');
|
const { getDashboardInfo, startTtyd, FrpcManager } = require('./frpc');
|
||||||
const { ensureNetwork } = require('./provisioning');
|
const { ProvisionManager } = require('./provisioning');
|
||||||
|
const { hasInternet } = require('./network');
|
||||||
|
|
||||||
const MAX_BACKOFF_MS = 60_000;
|
const MAX_BACKOFF_MS = 60_000;
|
||||||
const PONG_TIMEOUT_MS = 15_000;
|
const PONG_TIMEOUT_MS = 15_000;
|
||||||
@@ -59,12 +60,27 @@ class ClawClient {
|
|||||||
async start() {
|
async start() {
|
||||||
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
||||||
|
|
||||||
// 第一步:确保网络可用(无网时进入 AP 配网模式)
|
// 后台启动 AP 配网管理器(WiFi 未连接时常驻热点)
|
||||||
await ensureNetwork({ clawId: this._cfg.claw_id });
|
this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
|
||||||
|
this._provisionMgr.start();
|
||||||
|
|
||||||
// 第二步:并行获取 dashboard 信息 + 启动 ttyd
|
// 有网络时直接连接云端
|
||||||
|
if (hasInternet()) {
|
||||||
|
await this._proceedWithConnection();
|
||||||
|
} else {
|
||||||
|
// 等待配网完成后再连
|
||||||
|
log.info('clawd', '等待网络就绪...');
|
||||||
|
this._provisionMgr.once('network-ready', () => {
|
||||||
|
this._proceedWithConnection().catch(e => {
|
||||||
|
log.error('clawd', '连接启动失败:', e.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _proceedWithConnection() {
|
||||||
const [dashInfo] = await Promise.all([
|
const [dashInfo] = await Promise.all([
|
||||||
getDashboardInfo(),
|
getDashboardInfo().catch(e => { log.warn('clawd', 'dashboard 信息获取失败:', e.message); return null; }),
|
||||||
startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)),
|
startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)),
|
||||||
]);
|
]);
|
||||||
this._dashInfo = dashInfo || {};
|
this._dashInfo = dashInfo || {};
|
||||||
@@ -77,6 +93,7 @@ class ClawClient {
|
|||||||
this._clearHeartbeat();
|
this._clearHeartbeat();
|
||||||
this._clearPing();
|
this._clearPing();
|
||||||
if (this._sdTimer) { clearInterval(this._sdTimer); this._sdTimer = null; }
|
if (this._sdTimer) { clearInterval(this._sdTimer); this._sdTimer = null; }
|
||||||
|
if (this._provisionMgr) { this._provisionMgr.stop(); this._provisionMgr = null; }
|
||||||
this._frpc.stop();
|
this._frpc.stop();
|
||||||
if (this._ws) this._ws.terminate();
|
if (this._ws) this._ws.terminate();
|
||||||
this._sdNotify('STOPPING=1');
|
this._sdNotify('STOPPING=1');
|
||||||
|
|||||||
@@ -173,8 +173,26 @@ function sleep(ms) {
|
|||||||
execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 });
|
execSync(`sleep ${ms / 1000}`, { timeout: ms + 2000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测 wlan0 是否以 STA 模式连接了 WiFi(排除自身热点)
|
||||||
|
*/
|
||||||
|
function isWifiStaConnected() {
|
||||||
|
const iface = getWifiIface();
|
||||||
|
try {
|
||||||
|
const out = run('nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device');
|
||||||
|
for (const line of out.split('\n')) {
|
||||||
|
const parts = line.split(':');
|
||||||
|
if (parts[0] === iface && parts[1] === 'wifi' && parts[2] === 'connected') {
|
||||||
|
return parts[3] !== CON_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hasInternet,
|
hasInternet,
|
||||||
|
isWifiStaConnected,
|
||||||
getWifiIface,
|
getWifiIface,
|
||||||
scanWifi,
|
scanWifi,
|
||||||
connectWifi,
|
connectWifi,
|
||||||
|
|||||||
@@ -1,93 +1,147 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { hasInternet, startAP, stopAP, AP_IP } = require('./network');
|
const { hasInternet, 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 config = require('./config');
|
const MONITOR_INTERVAL_MS = 30_000;
|
||||||
|
|
||||||
const MAX_RETRIES = 3; // 配网连接失败后最多重新进入 AP 模式次数
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确保设备有互联网连接。
|
* AP 常驻配网管理器。
|
||||||
* 已联网 → 直接返回
|
|
||||||
* 未联网 → 进入 AP 配网模式 → 等待用户配网 → 成功后返回
|
|
||||||
*
|
*
|
||||||
* @param {object} opts
|
* 规则:
|
||||||
* @param {string|number} opts.clawId - 设备 ID(用于 AP SSID)
|
* - wlan0 没有以 STA 模式连接 WiFi → 开 AP + DNS 劫持 + HTTP 配网页
|
||||||
* @returns {Promise<void>}
|
* - 用户提交 WiFi 凭证 → 关 AP → 尝试连接 → 失败则重新开 AP
|
||||||
|
* - 运行中 WiFi 断开 → 自动重新开 AP
|
||||||
|
* - WiFi 已连接 → AP 关闭
|
||||||
*/
|
*/
|
||||||
async function ensureNetwork(opts = {}) {
|
class ProvisionManager extends EventEmitter {
|
||||||
// 先检测是否已联网
|
constructor(clawId) {
|
||||||
if (hasInternet()) {
|
super();
|
||||||
log.info('provision', '网络已就绪,跳过配网');
|
this._clawId = clawId || 'Setup';
|
||||||
return;
|
this._state = 'idle'; // 'idle' | 'ap' | 'connecting' | 'sta'
|
||||||
|
this._dns = null;
|
||||||
|
this._server = null;
|
||||||
|
this._monitorTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn('provision', '未检测到网络,进入配网模式...');
|
start() {
|
||||||
|
if (isWifiStaConnected()) {
|
||||||
|
this._state = 'sta';
|
||||||
|
log.info('provision', 'WiFi STA 已连接,AP 不启动');
|
||||||
|
} else {
|
||||||
|
this._enterAP();
|
||||||
|
}
|
||||||
|
this._startMonitor();
|
||||||
|
|
||||||
const cfg = config.load();
|
if (hasInternet()) {
|
||||||
const clawId = opts.clawId || cfg.claw_id || 'Setup';
|
this.emit('network-ready');
|
||||||
let retries = 0;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this._stopMonitor();
|
||||||
|
this._stopAll();
|
||||||
|
this._state = 'idle';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 进入 AP 模式 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_enterAP() {
|
||||||
|
if (this._state === 'ap') return;
|
||||||
|
|
||||||
while (retries < MAX_RETRIES) {
|
|
||||||
try {
|
try {
|
||||||
await runProvisioningRound(clawId);
|
const ap = startAP(this._clawId);
|
||||||
|
|
||||||
// 配网成功,再验证一次
|
this._dns = new DnsHijack();
|
||||||
if (hasInternet()) {
|
this._dns.start(ap.iface, AP_IP);
|
||||||
log.info('provision', '配网完成,网络已就绪');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn('provision', '配网后仍无网络,重新进入配网模式...');
|
this._server = new CaptiveServer({
|
||||||
|
clawId: this._clawId,
|
||||||
|
onConnect: (ssid, password) => this._handleWifiConnect(ssid, password),
|
||||||
|
});
|
||||||
|
this._server.startListening();
|
||||||
|
|
||||||
|
this._state = 'ap';
|
||||||
|
log.info('provision', `AP 常驻模式已启动: ${ap.ssid}, 密码 12345678`);
|
||||||
|
log.info('provision', `配网地址: http://ap.cutos.ai`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('provision', `配网异常: ${e.message}`);
|
log.error('provision', `AP 启动失败: ${e.message}`);
|
||||||
}
|
|
||||||
|
|
||||||
retries++;
|
|
||||||
if (retries < MAX_RETRIES) {
|
|
||||||
log.info('provision', `重试配网 (${retries}/${MAX_RETRIES})...`);
|
|
||||||
// 等一会再重试,避免过快循环
|
|
||||||
await sleep(3000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error('provision', `配网失败 ${MAX_RETRIES} 次,将以离线模式继续启动(等待网络恢复后重连)`);
|
// ── 用户提交 WiFi 凭证 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async _handleWifiConnect(ssid, password) {
|
||||||
|
if (this._state === 'connecting') return { success: false, error: '正在连接中,请稍候' };
|
||||||
|
|
||||||
|
this._state = 'connecting';
|
||||||
|
log.info('provision', `用户请求连接 WiFi: ${ssid}`);
|
||||||
|
|
||||||
|
this._stopAPServices();
|
||||||
|
|
||||||
|
const result = connectWifi(ssid, password);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this._state = 'sta';
|
||||||
|
log.info('provision', `WiFi 已连接: ${ssid}`);
|
||||||
|
this.emit('network-ready');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
log.warn('provision', `WiFi 连接失败: ${result.error},重新启动 AP`);
|
||||||
* 单轮配网流程:开 AP → 启动 DNS + HTTP → 等待用户配网 → 清理
|
this._enterAP();
|
||||||
*/
|
return result;
|
||||||
async function runProvisioningRound(clawId) {
|
}
|
||||||
const dns = new DnsHijack();
|
|
||||||
const server = new CaptiveServer({ clawId });
|
|
||||||
|
|
||||||
try {
|
// ── WiFi 状态监控 ─────────────────────────────────────────────────────────
|
||||||
// 1. 启动 WiFi AP
|
|
||||||
const ap = startAP(clawId);
|
|
||||||
|
|
||||||
// 2. 启动 DNS 劫持
|
_startMonitor() {
|
||||||
dns.start(ap.iface, AP_IP);
|
this._monitorTimer = setInterval(() => {
|
||||||
|
if (this._state === 'connecting') return;
|
||||||
|
|
||||||
// 3. 启动 HTTP 配网页面,等待用户完成配网
|
const wifiUp = isWifiStaConnected();
|
||||||
// server.start() 返回 Promise,配网成功时 resolve
|
|
||||||
log.info('provision', '配网页面已就绪,等待用户操作...');
|
|
||||||
log.info('provision', `用户请连接 WiFi "${ap.ssid}" 并访问 http://ap.cutos.ai`);
|
|
||||||
|
|
||||||
const result = await server.start();
|
if (this._state === 'sta' && !wifiUp) {
|
||||||
log.info('provision', `用户已连接 WiFi: ${result.ssid}`);
|
log.warn('provision', 'WiFi 连接已断开,重新启动 AP');
|
||||||
} finally {
|
this._enterAP();
|
||||||
// 清理:无论成功失败都关闭 AP / DNS / HTTP
|
}
|
||||||
server.stop();
|
|
||||||
dns.stop();
|
if (this._state === 'ap' && wifiUp) {
|
||||||
|
log.info('provision', 'WiFi 已外部连接,关闭 AP');
|
||||||
|
this._stopAPServices();
|
||||||
|
this._state = 'sta';
|
||||||
|
this.emit('network-ready');
|
||||||
|
}
|
||||||
|
}, MONITOR_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopMonitor() {
|
||||||
|
if (this._monitorTimer) {
|
||||||
|
clearInterval(this._monitorTimer);
|
||||||
|
this._monitorTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 清理 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_stopAPServices() {
|
||||||
|
if (this._server) {
|
||||||
|
this._server.stop();
|
||||||
|
this._server = null;
|
||||||
|
}
|
||||||
|
if (this._dns) {
|
||||||
|
this._dns.stop();
|
||||||
|
this._dns = null;
|
||||||
|
}
|
||||||
stopAP();
|
stopAP();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_stopAll() {
|
||||||
|
this._stopAPServices();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
module.exports = { ProvisionManager };
|
||||||
return new Promise(r => setTimeout(r, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { ensureNetwork };
|
|
||||||
|
|||||||
Reference in New Issue
Block a user