使用 HBuilder X + uni-app 在微信小程序中实现高可靠物联网设备配网(AirKiss / SmartConfig / SoftAP / UDP 自动切换)
本文介绍了一种物联网设备多协议兼容的配网方案设计,通过优先级策略和自动切换机制提高配网成功率。方案支持AirKiss、SmartConfig/Esptouch、SoftAP+UDP等多种配网方式,并可选蓝牙BLE配网。
最近在开发过程中遇到物联网设备配网的需求,整理了一下,主要是实现多协议兼容的配网流程:优先尝试高成功率的方法,失败时自动切换到备用方案,从而显著提升配网成功率。
概述
目标:为家庭或工业物联网设备提供一套健壮的配网流程,让用户在微信小程序里完成设备联网。支持的配网方式包括:
-
AirKiss(微信生态常用,基于监听局域网广播与报文特征)
-
SmartConfig / Esptouch(ESP 系列芯片常用)
-
SoftAP + UDP(设备开热点,手机连接设备热点并通过 UDP 发送配网参数)
-
可选:蓝牙 BLE 配网(适用于支持 BLE 的设备)。
设计原则:
-
优先使用对目标芯片/设备最可靠的方案。
-
失败时按顺序自动切换,避免人工干预。
-
将网络权限与连接管理集中,界面只负责交互与状态展示。
-
对关键步骤做超时与错误分类,便于统计和优化。
适用场景:ESP8266 / ESP32 / 自研模块、智能家居设备(灯、插座、传感器)、需要高成功率的消费类硬件。
使用到的技术与 API
-
HBuilder X + uni-app 框架
-
微信小程序原生 API(wx.startWifi / wx.getWifiList / wx.connectWifi / wx.createUDPSocket / wx.openBluetoothAdapter 等)
-
原生插件或第三方配网插件(AirKiss 插件、Esptouch 插件、或封装好的 Gl-WiFi 插件)
-
设备端需支持对应协议(AirKiss、SmartConfig、或 SoftAP UDP 接收)
总体流程与自动切换策略
流程要点(带优先级):
-
检测并获取当前 Wi‑Fi 信息(SSID、BSSID 等),检查是否为 2.4GHz 网络。
-
优先尝试插件级“智能配网”(如果设备固件支持 SmartConfig/Esptouch 或厂商插件),优点:无需连接设备热点、兼容性高。
-
若智能配网在指定超时时间内失败,自动切换到 AirKiss(如果设备支持),AirKiss 适合微信生态的设备监听广播配网。
-
若仍失败,切换到 SoftAP 模式:引导用户手动连接设备热点,然后通过 UDP 发送 SSID/密码并等待设备返回成功确认。
-
可选:如果设备支持 BLE,作为最后手段通过蓝牙发送配网凭据。
每个阶段都应设置合理超时(例如智能配网 20–30s,AirKiss 15–20s,SoftAP 等待用户连接 60–120s),并记录失败原因用于迭代优化。
模块划分(代码结构)
-
pages/配网页:UI、交互、展示当前策略状态与日志
-
services/wifiService.js:封装 Wi‑Fi 权限、获取列表、连接热点、检测 Wi‑Fi 是否 2.4G
-
services/configService.js:实现 SmartConfig / AirKiss / SoftAP 的启动、停止、结果回调
-
utils/timers.js:统一超时与重试策略
关键代码片段(uni-app / HBuilder X)
1、全局配网控制器(services/provisionController.js)
// services/provisionController.js
import configService from './configService'; // 实现 start/stop/onResult 的封装
import wifiService from './wifiService';
import { timeoutPromise, retryWithBackoff, startTimeout, clearTimeoutById, clearAllTimers } from '@/utils/timers';
const DEFAULTS = {
timeouts: {
smartconfigSingle: 8000, // 单次 smartconfig 请求超时
airkissSingle: 6000,
softapSingle: 8000, // 单次 UDP 发送等待确认
softapTotal: 120000 // softap 总超时
},
retries: {
smartconfig: 3,
airkiss: 2,
softapSendAttempts: 6
},
backoff: {
baseDelay: 700,
factor: 2,
maxDelay: 10000
}
};
// 简单事件机制
const _events = {};
function emit(evt, payload) { (_events[evt] || []).forEach(fn => { try { fn(payload); } catch (e) {} }); }
function on(evt, fn) { _events[evt] = _events[evt] || []; _events[evt].push(fn); return () => off(evt, fn); }
function off(evt, fn) { if (!_events[evt]) return; _events[evt] = _events[evt].filter(f => f !== fn); }
let _running = false;
let _cancelled = false;
let _internalCancel = null;
async function provision({ ssid, password, options = {} } = {}) {
if (_running) throw new Error('provision_already_running');
_running = true;
_cancelled = false;
const cfg = {
timeouts: { ...DEFAULTS.timeouts, ...(options.timeouts || {}) },
retries: { ...DEFAULTS.retries, ...(options.retries || {}) },
backoff: { ...DEFAULTS.backoff, ...(options.backoff || {}) }
};
emit('log', `开始配网:${ssid}`);
// 全局可取消 token
let cancelled = false;
_internalCancel = () => { cancelled = true; _cancelled = true; };
try {
// 1. 检查 wifi 环境(可选)
const curWifi = await wifiService.ensureWifiReady();
if (!curWifi) {
emit('log', '未检测到已连接 Wi-Fi,请确认手机已连接家庭 2.4G 网络(或进入 SoftAP)');
} else {
emit('log', `当前 Wi-Fi: ${curWifi.SSID || curWifi.ssid || 'unknown'}`);
}
// Helper: respect cancellation
function checkCancelled() {
if (cancelled) throw { cancelled: true };
}
// Strategy: SmartConfig with retry
emit('stage', 'smartconfig');
emit('log', '尝试 SmartConfig(Esptouch)');
const smartRes = await tryWithRetries(
() => trySmartConfigOnce({ ssid, password, timeout: cfg.timeouts.smartconfigSingle }),
cfg.retries.smartconfig,
cfg.backoff,
(attempt, err) => emit('log', `SmartConfig 第${attempt}次尝试失败:${err && err.message || JSON.stringify(err)}`)
);
checkCancelled();
if (smartRes && smartRes.success) {
emit('log', `SmartConfig 成功: ${JSON.stringify(smartRes.data)}`);
emit('result', { success: true, method: 'smartconfig', data: smartRes.data });
return { success: true, method: 'smartconfig', data: smartRes.data };
}
emit('log', 'SmartConfig 最终失败,切换 AirKiss');
// Strategy: AirKiss with retry
emit('stage', 'airkiss');
emit('log', '尝试 AirKiss');
const airRes = await tryWithRetries(
() => tryAirkissOnce({ ssid, password, timeout: cfg.timeouts.airkissSingle }),
cfg.retries.airkiss,
cfg.backoff,
(attempt, err) => emit('log', `AirKiss 第${attempt}次尝试失败:${err && err.message || JSON.stringify(err)}`)
);
checkCancelled();
if (airRes && airRes.success) {
emit('log', `AirKiss 成功: ${JSON.stringify(airRes.data)}`);
emit('result', { success: true, method: 'airkiss', data: airRes.data });
return { success: true, method: 'airkiss', data: airRes.data };
}
emit('log', 'AirKiss 失败,切换 SoftAP');
// Strategy: SoftAP (用户连接设备热点后通过 UDP 发包)
emit('stage', 'softap');
emit('log', '进入 SoftAP 流程,等待用户连接设备热点(总超时 ' + (cfg.timeouts.softapTotal / 1000) + 's)');
// SoftAP: 等待用户确认连接热点的外部事件(UI 触发 setUserConnectedToAP(true))
const userConnected = await waitForUserConnectOrTimeout(cfg.timeouts.softapTotal);
checkCancelled();
if (!userConnected) {
emit('log', 'SoftAP 等待用户连接超时');
throw new Error('softap_user_timeout');
}
emit('log', '用户确认已连接设备热点,开始 UDP 发送并等待设备确认');
// SoftAP: 多次发送 UDP(重试)直到设备回复或尝试耗尽
const softRes = await trySoftAPRepeat({ ssid, password, attempts: cfg.retries.softapSendAttempts, singleTimeout: cfg.timeouts.softapSingle, backoff: cfg.backoff });
checkCancelled();
if (softRes && softRes.success) {
emit('log', `SoftAP 配网成功: ${JSON.stringify(softRes.data)}`);
emit('result', { success: true, method: 'softap', data: softRes.data });
return { success: true, method: 'softap', data: softRes.data };
}
// 所有策略失败
emit('log', '所有配网策略均已失败');
emit('result', { success: false, reason: 'all_failed' });
return { success: false, reason: 'all_failed' };
} catch (err) {
if (err && err.cancelled) {
emit('log', '配网被取消');
emit('result', { success: false, reason: 'cancelled' });
return { success: false, reason: 'cancelled' };
}
emit('log', '配网异常结束:' + (err && (err.message || JSON.stringify(err)) || 'unknown'));
emit('result', { success: false, reason: err && err.message || err });
return { success: false, reason: err && err.message || err };
} finally {
_running = false;
_cancelled = false;
_internalCancel = null;
clearAllTimers();
}
}
/* ---------- helper utils ---------- */
async function tryWithRetries(fn, retries, backoffCfg, onRetry) {
const controller = retryWithBackoff(async (attempt) => {
return await fn();
}, {
retries,
baseDelay: backoffCfg.baseDelay,
factor: backoffCfg.factor,
maxDelay: backoffCfg.maxDelay,
onRetry
});
try {
const res = await controller.start();
return res;
} catch (err) {
if (err && err.cancelled) throw err;
return null;
}
}
/* SmartConfig 一次性尝试(单次超时保护) */
function trySmartConfigOnce({ ssid, password, timeout = 8000 }) {
return new Promise((resolve, reject) => {
let finished = false;
emit('log', `SmartConfig 单次尝试超时 ${timeout}ms`);
// 绑定回调
const onResult = (res) => {
if (finished) return;
finished = true;
configService.stopSmartConfig();
if (res && res.success) resolve({ success: true, data: res.data });
else resolve({ success: false, data: res });
};
configService.onSmartConfigResult(onResult);
try {
configService.startSmartConfig({ ssid, password });
} catch (e) {
finished = true;
configService.stopSmartConfig();
return reject(e);
}
// 超时保护
const tid = startTimeout(() => {
if (finished) return;
finished = true;
configService.stopSmartConfig();
emit('log', 'SmartConfig 单次超时触发');
resolve({ success: false, reason: 'timeout' });
}, timeout);
// 清理函数(如果外部取消)
// Note: timers will be cleared in finally by clearAllTimers
});
}
/* AirKiss 单次尝试 */
function tryAirkissOnce({ ssid, password, timeout = 6000 }) {
return new Promise((resolve, reject) => {
let finished = false;
emit('log', `AirKiss 单次尝试超时 ${timeout}ms`);
const onResult = (res) => {
if (finished) return;
finished = true;
configService.stopAirkiss();
if (res && res.success) resolve({ success: true, data: res.data });
else resolve({ success: false, data: res });
};
configService.onAirkissResult(onResult);
try {
configService.startAirkiss({ ssid, password });
} catch (e) {
finished = true;
configService.stopAirkiss();
return reject(e);
}
const tid = startTimeout(() => {
if (finished) return;
finished = true;
configService.stopAirkiss();
emit('log', 'AirKiss 单次超时触发');
resolve({ success: false, reason: 'timeout' });
}, timeout);
});
}
/* SoftAP: 等待 UI 标记用户已连接到 device AP(示例:UI 调用 ProvisionController.setUserConnected(true))
也可替换为检测当前 wifi SSID 是否以设备热点前缀开头(如果小程序可获取)。 */
let _userConnectedResolver = null;
let _userConnectedTimerId = null;
function waitForUserConnectOrTimeout(totalTimeout) {
return new Promise((resolve) => {
// 如果在其他地方已设置 true,可以直接 resolve
// 这里暴露 setUserConnectedToAP 方法供 UI 调用
_userConnectedResolver = resolve;
// 超时保护
_userConnectedTimerId = startTimeout(() => {
_userConnectedResolver = null;
resolve(false);
}, totalTimeout);
});
}
function setUserConnectedToAP(flag = true) {
if (_userConnectedTimerId) { clearTimeoutById(_userConnectedTimerId); _userConnectedTimerId = null; }
if (_userConnectedResolver) {
const r = _userConnectedResolver;
_userConnectedResolver = null;
r(flag);
}
}
/* SoftAP: 多次发送 UDP 并等待设备回复 */
function trySoftAPRepeat({ ssid, password, attempts = 6, singleTimeout = 8000, backoff }) {
return new Promise(async (resolve) => {
// onMessage 回调会在 configService 内触发并执行回调函数
let finished = false;
// subscribe once
const onMsg = (res) => {
if (finished) return;
finished = true;
configService.stopUDPProvision();
resolve({ success: true, data: res.data || res });
};
configService.onUDPProvisionResult(onMsg);
// Use retryWithBackoff pattern manually to control send intervals
const rctrl = retryWithBackoff(async (attempt) => {
// send single UDP and wait for singleTimeout for response
emit('log', `SoftAP 发送第 ${attempt} 次 UDP(等待 ${singleTimeout}ms)`);
try {
// startUDPProvision会同时监听 onMessage 并在回调时触发 onUDPProvisionResult
configService.startUDPProvision({ ssid, password });
} catch (e) {
throw e;
}
// Wait for singleTimeout; if message arrives earlier, onMsg will resolve
await timeoutPromise(new Promise((resolveInner) => {
// resolveInner never called here; timeoutPromise will time out after singleTimeout
}), singleTimeout, () => {
// on timeout, we stop UDP (if possible) to prepare next attempt
try { configService.stopUDPProvision(); } catch (_) {}
}).catch((e) => { /* swallow timeout error to let retry loop continue */ });
// If no response, throw to trigger retry
if (finished) return { success: true }; // already resolved by onMsg
throw new Error('softap_attempt_timeout');
}, {
retries: attempts,
baseDelay: backoff.baseDelay,
factor: backoff.factor,
maxDelay: backoff.maxDelay,
onRetry: (attempt, err) => emit('log', `SoftAP 重试第${attempt}次失败: ${err && err.message || err}`)
});
try {
await rctrl.start();
if (!finished) {
// all attempts exhausted
configService.stopUDPProvision();
resolve({ success: false, reason: 'softap_all_attempts_failed' });
}
} catch (err) {
if (!finished) {
configService.stopUDPProvision();
resolve({ success: false, reason: err && err.message || err });
}
}
});
}
/* 外部调用取消 */
function cancel() {
if (_internalCancel) _internalCancel();
}
/* 额外暴露的 API */
export default {
provision,
cancel,
on,
off,
setUserConnectedToAP
};
2、wifiService.js(封装 Wi‑Fi 权限、获取当前 Wi‑Fi、判断 2.4G)
export default {
startWifi() {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
wx.startWifi({
success: (res) => resolve(res),
fail: (err) => reject(err)
});
// #endif
});
},
getConnectedWifi() {
return new Promise((resolve) => {
// #ifdef MP-WEIXIN
wx.getConnectedWifi({
success(res) {
resolve(res.wifi || null);
},
fail() {
resolve(null);
}
});
// #endif
});
},
is2_4G(wifi) {
if (!wifi || !wifi.SSID) return false;
// 简单判断:排除 5G SSID 标志或直接依赖设备端信息
return !/5[gG]/.test(wifi.SSID) && !/5GHz|5G/.test(wifi.SSID);
},
async ensureWifiReady() {
try {
await this.startWifi();
const wifi = await this.getConnectedWifi();
return wifi;
} catch (e) {
return null;
}
}
};
3、configService.js(插件与原生接口封装)
let smartCallback = null;
let airkissCallback = null;
let udpCallback = null;
export default {
startSmartConfig({ ssid, password }) {
// 插件调用示例,实际根据接入的插件 API 调整
const glWiFi = uni.requireNativePlugin('Gl-WiFi');
glWiFi.startEsptouch({
ssid,
pwd: password
});
},
stopSmartConfig() {
const glWiFi = uni.requireNativePlugin('Gl-WiFi');
if (glWiFi && glWiFi.cancel) glWiFi.cancel();
},
onSmartConfigResult(cb) {
// 插件回调挂载点
smartCallback = cb;
// 假设插件通过事件返回
// 在真实项目里把插件回调绑定到这里并调用 smartCallback(result)
},
startAirkiss({ ssid, password }) {
const airkiss = uni.requirePlugin('airkiss');
airkiss.startAirkiss(ssid, password, (res) => {
if (airkissCallback) airkissCallback(res);
});
},
stopAirkiss() {
const airkiss = uni.requirePlugin && uni.requirePlugin('airkiss');
if (airkiss && airkiss.stop) airkiss.stop();
},
onAirkissResult(cb) {
airkissCallback = cb;
},
startUDPProvision({ ssid, password }) {
// 使用 wx.createUDPSocket 在小程序中发送 UDP 配网包到设备固定 IP(通常为 192.168.4.1)
// #ifdef MP-WEIXIN
const socket = wx.createUDPSocket();
socket.bind();
const payload = JSON.stringify({ cmd: 'provision', ssid, password });
socket.send({
address: '192.168.4.1',
port: 8266,
message: payload,
success() {}
});
socket.onMessage((res) => {
// 解析返回并回调
const arr = new Uint8Array(res.message);
const txt = decodeURIComponent(escape(String.fromCharCode.apply(null, arr)));
let obj = null;
try { obj = JSON.parse(txt); } catch (e) { obj = { msg: txt }; }
if (udpCallback) udpCallback({ success: true, data: obj });
});
// #endif
},
stopUDPProvision() {
// 小程序 UDP 无 stop 接口,关闭 socket 或重建页面即可
},
onUDPProvisionResult(cb) {
udpCallback = cb;
}
};
4、UI 页面示例(pages/provision.vue):展示状态、日志、操作按钮
<template>
<view class="container">
<view class="header">设备配网</view>
<view class="form">
<input v-model="ssid" placeholder="家庭 WiFi 名称" />
<input v-model="password" placeholder="WiFi 密码" />
<button @click="startProvision">开始配网</button>
</view>
<view class="stage">
<text>当前阶段:{{ stage }}</text>
<text>日志:</text>
<scroll-view style="height:200px">
<view v-for="(l,i) in logs" :key="i">{{ l }}</view>
</scroll-view>
</view>
<view v-if="stage === 'softap'">
<button @click="confirmAP">我已连接设备热点,开始发送配网</button>
</view>
</view>
</template>
<script>
import ProvisionController from '@/services/provisionController';
export default {
data() {
return { ssid: '', password: '', logs: [], stage: '' };
},
onUnload() {
ProvisionController.cancel();
},
methods: {
startProvision() {
this.logs = [];
ProvisionController.on('log', (t) => { this.logs.unshift(t); });
ProvisionController.on('stage', (s) => { this.stage = s; });
ProvisionController.on('result', (r) => {
if (r.success) uni.showToast({ title: '配网成功' });
else uni.showModal({ title: '配网结果', content: '失败:' + (r.reason || '未知') });
});
ProvisionController.provision({ ssid: this.ssid, password: this.password });
},
confirmAP() {
ProvisionController.setUserConnectedToAP(true);
}
}
};
</script>
5、timers.js
// timers.js
// 兼容运行在小程序或浏览器环境,使用标准的 setTimeout / setInterval
const _timers = {
timeouts: new Map(),
intervals: new Map(),
nextId: 1
};
function _genId() {
return `t_${Date.now()}_${_timers.nextId++}`;
}
/**
* startTimeout(fn, delay) -> id
* 返回 id,可用于 clearTimeoutById(id)
*/
export function startTimeout(fn, delay = 1000) {
const id = _genId();
const raw = setTimeout(() => {
try { fn(); } finally { _timers.timeouts.delete(id); }
}, delay);
_timers.timeouts.set(id, raw);
return id;
}
/**
* clearTimeoutById(id)
*/
export function clearTimeoutById(id) {
const raw = _timers.timeouts.get(id);
if (raw !== undefined) {
clearTimeout(raw);
_timers.timeouts.delete(id);
return true;
}
return false;
}
/**
* startInterval(fn, interval) -> id
* 返回 id,可用于 clearIntervalById(id)
*/
export function startInterval(fn, interval = 1000) {
const id = _genId();
const raw = setInterval(fn, interval);
_timers.intervals.set(id, raw);
return id;
}
/**
* clearIntervalById(id)
*/
export function clearIntervalById(id) {
const raw = _timers.intervals.get(id);
if (raw !== undefined) {
clearInterval(raw);
_timers.intervals.delete(id);
return true;
}
return false;
}
/**
* clearAllTimers()
* 清理所有本模块创建的定时器(页面卸载或切换场景时调用)
*/
export function clearAllTimers() {
for (const id of Array.from(_timers.timeouts.keys())) clearTimeoutById(id);
for (const id of Array.from(_timers.intervals.keys())) clearIntervalById(id);
}
/**
* timeoutPromise(promiseOrFn, ms, onTimeout)
* 将一个 Promise 包装成带超时的 Promise。
* - promiseOrFn 可以是 Promise 或返回 Promise 的函数(延迟执行场景)
* - onTimeout 可选,超时发生时会被调用(用于清理外部资源)
* 使用示例:
* await timeoutPromise(() => someAsync(), 15000, () => cleanup());
*/
export function timeoutPromise(promiseOrFn, ms = 15000, onTimeout) {
let p;
try {
p = (typeof promiseOrFn === 'function') ? promiseOrFn() : promiseOrFn;
} catch (err) {
return Promise.reject(err);
}
let timeoutId;
const timeoutP = new Promise((_, reject) => {
timeoutId = startTimeout(() => {
if (typeof onTimeout === 'function') {
try { onTimeout(); } catch (_) {}
}
reject(new Error('timeout'));
}, ms);
});
return Promise.race([p.then(v => { clearTimeoutById(timeoutId); return v; }), timeoutP]);
}
/**
* retryWithBackoff(taskFn, options) -> controller
* taskFn: () => Promise<any>
* options:
* - retries: 最大尝试次数(默认 3)
* - baseDelay: 初始延迟 ms(默认 800)
* - maxDelay: 最大延迟 ms(默认 10000)
* - factor: 指数因子(默认 2)
* - onRetry(attempt, err): 每次失败回调
*
* 返回一个 controller 对象:
* - start(): 返回 Promise,resolve 成功结果或 reject 最后错误
* - cancel(): 取消重试,Promise 会 reject 一个 { cancelled: true } 错误
*
* 使用场景:配网中对 UDP 或 SmartConfig 的重试逻辑
*/
export function retryWithBackoff(taskFn, options = {}) {
const cfg = {
retries: options.retries ?? 3,
baseDelay: options.baseDelay ?? 800,
maxDelay: options.maxDelay ?? 10000,
factor: options.factor ?? 2,
onRetry: options.onRetry
};
let cancelled = false;
let currentTimeoutId = null;
function cancelPendingTimeout() {
if (currentTimeoutId) {
clearTimeoutById(currentTimeoutId);
currentTimeoutId = null;
}
}
function cancel() {
cancelled = true;
cancelPendingTimeout();
}
async function start() {
let attempt = 0;
let lastErr;
while (attempt < cfg.retries && !cancelled) {
try {
attempt++;
const res = await taskFn(attempt);
return res;
} catch (err) {
lastErr = err;
if (typeof cfg.onRetry === 'function') {
try { cfg.onRetry(attempt, err); } catch (_) {}
}
if (attempt >= cfg.retries) break;
const delay = Math.min(cfg.baseDelay * Math.pow(cfg.factor, attempt - 1), cfg.maxDelay);
await new Promise((resolve) => {
currentTimeoutId = startTimeout(() => {
currentTimeoutId = null;
resolve();
}, delay);
});
}
}
if (cancelled) throw { cancelled: true };
throw lastErr;
}
return { start, cancel };
}
/**
* createPausableInterval(fn, interval)
* 返回对象 { start, pause, resume, stop }
* - start 会立即触发一次 fn 并开始循环
* - pause 暂停(保留状态)
* - resume 继续
* - stop 停止并清理
* 用于需要在页面可见性或用户交互中暂停的定时任务
*/
export function createPausableInterval(fn, interval = 1000) {
let running = false;
let paused = false;
let timeoutId = null;
function _tick() {
if (!running || paused) return;
try { fn(); } catch (_) {}
timeoutId = startTimeout(_tick, interval);
}
function start() {
if (running) return;
running = true;
paused = false;
try { fn(); } catch (_) {}
timeoutId = startTimeout(_tick, interval);
}
function pause() {
if (!running || paused) return;
paused = true;
if (timeoutId) { clearTimeoutById(timeoutId); timeoutId = null; }
}
function resume() {
if (!running || !paused) return;
paused = false;
timeoutId = startTimeout(_tick, interval);
}
function stop() {
running = false;
paused = false;
if (timeoutId) { clearTimeoutById(timeoutId); timeoutId = null; }
}
return { start, pause, resume, stop, isRunning: () => running && !paused };
}
自动切换与容错细节
-
每个方案均必须实现:start、stop、onResult 回调与超时处理。
-
为每次尝试记录详细日志:开始时间、结束时间、失败原因、设备返回数据。用于后台统计与迭代。
-
失败分类示例:
-
超时无应答(常见于信号弱、设备未启动)
-
插件返回错误码(如权限或本地网络限制)
-
用户未连接到设备热点(SoftAP 阶段)
-
-
自动重试策略:
-
对 SmartConfig 和 AirKiss 类方案在短时间内最多重试 1-2 次,避免网络拥塞和重复广播干扰。
-
SoftAP 可提示用户检查热点密码或重启设备,并允许人工重试或导向 BLE 配网。
-
-
并发控制:同一时间只允许启动一个配网流程;在更底层限制 UDP 广播频率,避免被系统限制。
设备端配合要点(固件要求)
-
明确支持哪些配网协议:AirKiss、Esptouch、SoftAP。固件中需要对应协议解析实现。
-
SoftAP 模式下设备应监听固定端口(典型 8266、8267),并在接收到配网 payload 后返回确认报文(包含设备 IP、MAC)。
-
在 SmartConfig / AirKiss 成功后,设备优先用 DHCP 获取 IP 并向云端注册,然后再向手机回传成功信息(多种实现:UDP 广播、TCP 反连、MQTT 上报)。
-
为配网阶段提供 LED 或语音提示,便于用户判断设备状态。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)