最近在开发过程中遇到物联网设备配网的需求,整理了一下,主要是实现多协议兼容的配网流程:优先尝试高成功率的方法,失败时自动切换到备用方案,从而显著提升配网成功率。

概述

目标:为家庭或工业物联网设备提供一套健壮的配网流程,让用户在微信小程序里完成设备联网。支持的配网方式包括:

  • 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 接收)

总体流程与自动切换策略

流程要点(带优先级):

  1. 检测并获取当前 Wi‑Fi 信息(SSID、BSSID 等),检查是否为 2.4GHz 网络。

  2. 优先尝试插件级“智能配网”(如果设备固件支持 SmartConfig/Esptouch 或厂商插件),优点:无需连接设备热点、兼容性高。

  3. 若智能配网在指定超时时间内失败,自动切换到 AirKiss(如果设备支持),AirKiss 适合微信生态的设备监听广播配网。

  4. 若仍失败,切换到 SoftAP 模式:引导用户手动连接设备热点,然后通过 UDP 发送 SSID/密码并等待设备返回成功确认。

  5. 可选:如果设备支持 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 或语音提示,便于用户判断设备状态。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐