联网与数据交换 / 联网实践发布 内容更新

ESP32 自己连 Wi-Fi:连接状态、超时和重试怎么写

用10秒应用连接窗口和2到30秒退避代替无限等待;日志保留真实断开原因,本地按钮与串口输入继续运行。公开包只含占位配置,不声称已连上无线网络。

读完能做什么
编写自己的STA联网应用,能区分配置、连接窗口、入网和退避,验证错口令与AP关闭恢复时本地输入仍工作。
开始之前
已会用时间与状态替代delay,会打开115200串口;准备可管理的2.4GHz测试AP。
所需器材
官方DevKitC-1 v1.1 N8R8、USB数据线、自己的2.4GHz WPA2-Personal测试AP与管理界面、可选常开按钮GPIO5接GND;无按钮可发送串口t
版本组合
开发板:ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8工具/框架:Arduino CLI1.5.1 / Arduino-ESP32 3.3.12(IDF5.5.5)FQBN:esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc
适用范围

官方ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8;Arduino CLI1.5.1、Arduino-ESP32 3.3.12、USB-to-UART口;未实板。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/networking-arduino.md验证日期:
构建 / 配置已通过
docs/esp32-evidence/arduino-builds.json;实际Exit0,源码SHA匹配;日志logs/esp32-wifi-connect-retry-esp32-wifi-connect-retry.log对象:ESP32-S3 / N8R8版本:Arduino CLI1.5.1 / core3.3.12验证日期:
仿真不适用
未运行芯片/外设仿真;软件逻辑检查如执行将另列范围
本站实板待验证
未在本站实板执行;正文与下载模板保留逐场景真实记录要求

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 联网实践

从配网已有固件,转到自己管理连接状态

现成固件的配网页、账号绑定与本篇的Arduino应用是不同入口。现在由你写代码给出网络配置、发起STA连接、判断何时失败与何时再试。目标不是启动后一直打印小圆点,而是在网络不工作时也能继续处理本地输入。

参考网络限定为2.4GHz WPA2-Personal测试AP;S3不支持5GHz。企业认证、Portal与产品配网另有任务,不用这份入门代码推断所有AP都兼容。ONLINE也只代表本地入网条件,后面的HTTP/网页任务仍要单独验证。

做到这里,你应该看到
用一句话区分“已配置”“正在尝试”“有本地IP”和“外部服务可用”。

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[2] Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开

02 / 联网实践

凭据只在本机填写,本地输入可先独立测试

公开示例里只有wifi_secrets.example.h占位值。复制成同目录wifi_secrets.h后填写自己的测试SSID/口令,该私有文件被.gitignore忽略。代码优先包含私有头文件,未替换占位时进入CONFIG_MISSING,不发起连接;这时仍能用串口t检查LOCAL计数。

不要将真实口令提交或放入截图/日志,也不要分享含秘密的固件。凭据会编进二进制,示例不是加密存储方案。WiFi.persistent(false)在本例初始化前选择RAM配置策略,不代表擦掉设备里所有历史凭据。

可选物理按钮只需GPIO5(J1-5)经常开触点接GND,内部上拉,沿P02的30毫秒消抖。没有LED输出,localToggle只是软件状态;没有按钮时发送小写t即可完成输入检查。

代码 / 配置
// SPDX-License-Identifier: MIT
#pragma once
// Copy to wifi_secrets.h and edit only that untracked file. Do not publish it.
constexpr char WIFI_SSID[] = "REPLACE_WITH_2G4_SSID";
constexpr char WIFI_PASSWORD[] = "REPLACE_WITH_PASSWORD";

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[6] 固定DevKitC-1 v1.1:GPIO与桥接串口[8] Arduino3.3.12 WiFiGeneric:persistent与WIFI_STORAGE_RAM

03 / 联网实践

把10秒连接窗口和退避等待写成明确状态

CONNECTING从一次应用尝试开始计10秒,不用waitForConnectResult,也不在loop写等待连接的while。只有WL_CONNECTED且本地IPv4非零才转ONLINE;这没有检测互联网或云服务。超时后主动disconnect,再进入BACKOFF,等待2、4、8、16、30秒,后续封顶30秒。

成功入网后退避重置;ONLINE失联从2秒起步。设备会持续尝试,但每一轮都有期限,而不是困在同一个等待中。恢复AP后可能仍需等当前退避与扫描,不能承诺立即恢复。

固定3.3.12的STA.cpp即使setAutoReconnect(false),首次失败仍可能进入一次first_connect内部重试。因此日志attempt只表示本程序新开的窗口,不等于射频关联次数,也不能据此计算对AP的全部请求量。本例超时主动断开也可能产生DISCONNECTED事件,要按STATE和时间理解reason来源。

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[2] Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开[3] Arduino3.3.12 WiFiSTA.cpp:兼容begin/disconnect包装[7] IDF5.5.5 WiFi:原始断开reason枚举

04 / 联网实践

WiFi等待留在worker,回调只投递通知

WiFi驱动的初始化、配置或断开内部可能有有限等待,所以本例把WiFi API和连接状态机放在一个worker任务。Arduino loop只负责按钮、串口与日志,主循环不调用联网等待函数。这样“没有无限while”与“输入不由同一段网络调用卡住”都有具体结构。

网络事件回调在另一个任务上下文运行。它只取原始断开reason或GOT_IP时间,非等待发送到16槽队列;不在回调内调用WiFi重连或打印,也不共享可变字符串。loop每轮最多处理4条通知,溢出报告NOTICE_OVERFLOW;状态另用单槽队列保留最新值,快速中间状态可能合并。

每5秒打印max_input_gap_ms,用它观察输入处理是否变慢。任务创建失败时仍保留本地输入。后台任务、队列与低日志频率不能保证硬实时响应,串口缓冲和系统调度仍需真实检查。

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[4] Arduino3.3.12 NetworkEvents:独立事件任务与类型[5] IDF5.5.5 / S3:任务、队列和调度边界

05 / 联网实践

完整程序:超时、掉线与输入各有自己的路径

程序只用core内置WiFi/Network/FreeRTOS,不增加第三方库。公开默认配置可以编译检查,但不会主动联网;只有你自己的私有头文件才改变测试网络。下载包含完整源码、profile、占位模板、许可和空白observations.csv。

状态打印中的status是驱动快照,reason是事件原码,二者不属于同一种枚举。不要拿WL_CONNECT_FAILED的数值去查WiFi reason表,也不要预填“错口令必然返回某一个原因”。

代码 / 配置
// SPDX-License-Identifier: MIT
// N01: application retry windows, while input stays in Arduino loop().
#include <Arduino.h>
#include <WiFi.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <atomic>
#include <cstring>
#if __has_include("wifi_secrets.h")
#include "wifi_secrets.h"
#else
#include "wifi_secrets.example.h"
#endif
constexpr uint8_t BUTTON_PIN = 5;
constexpr uint32_t CONNECT_WINDOW_MS = 10000, FIRST_BACKOFF_MS = 2000, MAX_BACKOFF_MS = 30000;
enum class Phase : uint8_t { CONFIG_MISSING, START_FAILED, CONNECTING, ONLINE, BACKOFF };
struct Report { Phase phase; uint32_t attempt, windowAt, backoffMs, ip; int status; };
const char *phaseName(Phase phase);
struct Notice { uint32_t at; uint16_t event; uint8_t reason; };
QueueHandle_t reportQueue = nullptr, noticeQueue = nullptr;
std::atomic<uint32_t> droppedNotices{0};
bool wifiTaskReady = false;
const char *phaseName(Phase phase) {
  switch (phase) {
    case Phase::CONFIG_MISSING: return "CONFIG_MISSING";
    case Phase::START_FAILED: return "START_FAILED";
    case Phase::CONNECTING: return "CONNECTING";
    case Phase::ONLINE: return "ONLINE";
    case Phase::BACKOFF: return "BACKOFF";
  }
  return "UNKNOWN";
}
void onWifiEvent(arduino_event_id_t event, arduino_event_info_t info) {
  if (event != ARDUINO_EVENT_WIFI_STA_DISCONNECTED && event != ARDUINO_EVENT_WIFI_STA_GOT_IP) return;
  Notice notice{millis(), uint16_t(event), uint8_t(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED ? info.wifi_sta_disconnected.reason : 0)};
  if (xQueueSend(noticeQueue, &notice, 0) != pdTRUE) ++droppedNotices;
}
void wifiTask(void *) {
  Report r{Phase::CONFIG_MISSING, 0, millis(), 0, 0, WL_DISCONNECTED};
  if (!std::strcmp(WIFI_SSID, "REPLACE_WITH_2G4_SSID") || !std::strcmp(WIFI_PASSWORD, "REPLACE_WITH_PASSWORD") || !*WIFI_SSID) {
    xQueueOverwrite(reportQueue, &r);
    vTaskDelete(nullptr);
    return;
  }
  WiFi.persistent(false);
  WiFi.onEvent(onWifiEvent);
  if (!WiFi.mode(WIFI_STA)) {
    r.phase = Phase::START_FAILED;
    xQueueOverwrite(reportQueue, &r);
    vTaskDelete(nullptr);
    return;
  }
  WiFi.setAutoReconnect(false);  // core 3.3.12 still has one internal first-connect retry
  uint32_t nextBackoff = FIRST_BACKOFF_MS;
  bool startAttempt = true;
  for (;;) {
    uint32_t now = millis();
    if (startAttempt) {
      startAttempt = false;
      ++r.attempt;
      r.phase = Phase::CONNECTING;
      r.backoffMs = 0;
      r.ip = 0;
      r.windowAt = now;
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);  // no waitForConnectResult / while waiting
      r.status = WiFi.status();
      xQueueOverwrite(reportQueue, &r);
    }
    r.status = WiFi.status();
    now = millis();
    if (r.phase == Phase::CONNECTING) {
      if (r.status == WL_CONNECTED && uint32_t(WiFi.localIP()) != 0) {
        r.phase = Phase::ONLINE;
        r.ip = uint32_t(WiFi.localIP());
        nextBackoff = FIRST_BACKOFF_MS;
        xQueueOverwrite(reportQueue, &r);
      } else if (uint32_t(now - r.windowAt) >= CONNECT_WINDOW_MS) {
        WiFi.disconnect(false, false, 0);  // no deliberate connection-wait timeout
        r.phase = Phase::BACKOFF;
        r.windowAt = millis();
        r.backoffMs = nextBackoff;
        nextBackoff = min(nextBackoff * 2, MAX_BACKOFF_MS);
        xQueueOverwrite(reportQueue, &r);
      }
    } else if (r.phase == Phase::ONLINE && r.status != WL_CONNECTED) {
      WiFi.disconnect(false, false, 0);
      r.phase = Phase::BACKOFF;
      r.ip = 0;
      r.windowAt = millis();
      r.backoffMs = FIRST_BACKOFF_MS;
      nextBackoff = min(FIRST_BACKOFF_MS * 2, MAX_BACKOFF_MS);
      xQueueOverwrite(reportQueue, &r);
    } else if (r.phase == Phase::BACKOFF && uint32_t(now - r.windowAt) >= r.backoffMs) {
      startAttempt = true;
    }
    vTaskDelay(pdMS_TO_TICKS(10));
  }
}
void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  reportQueue = xQueueCreate(1, sizeof(Report));
  noticeQueue = xQueueCreate(16, sizeof(Notice));
  if (reportQueue && noticeQueue)
    wifiTaskReady = xTaskCreate(wifiTask, "n01-wifi", 6144, nullptr, 1, nullptr) == pdPASS;
  Serial.printf("N01 core=3.3.12 task=%u; GPIO5 button or serial 't' remains local\n", wifiTaskReady);
}
void loop() {
  static bool initialized = false, candidate, stable;
  static bool localToggle = false;
  static uint32_t changedAt = 0, presses = 0, lastLoopAt = 0, maxGap = 0, reportedAt = 0;
  static Report latest{Phase::CONFIG_MISSING, 0, 0, 0, 0, WL_DISCONNECTED};
  const uint32_t now = millis();
  const bool raw = digitalRead(BUTTON_PIN);
  if (!initialized) { initialized = true; candidate = stable = raw; changedAt = lastLoopAt = now; }
  const uint32_t gap = now - lastLoopAt;
  lastLoopAt = now;
  if (gap > maxGap) maxGap = gap;
  bool trigger = false;
  if (raw != candidate) { candidate = raw; changedAt = now; }
  if (candidate != stable && uint32_t(now - changedAt) >= 30) {
    stable = candidate;
    trigger = stable == LOW;
  }
  if (Serial.available() && Serial.read() == 't') trigger = true;
  if (trigger) {
    localToggle = !localToggle;
    Serial.printf("LOCAL event=%lu at=%lu toggle=%u state=%s\n",
      (unsigned long)++presses, (unsigned long)now, localToggle, phaseName(latest.phase));
  }
  Report update;
  if (wifiTaskReady && xQueueReceive(reportQueue, &update, 0) == pdTRUE) {
    latest = update;
    Serial.printf("STATE=%s attempt=%lu window_at=%lu backoff_ms=%lu status=%d IP=%s\n",
      phaseName(latest.phase), (unsigned long)latest.attempt, (unsigned long)latest.windowAt,
      (unsigned long)latest.backoffMs, latest.status, IPAddress(latest.ip).toString().c_str());
  }
  Notice notice;
  for (uint8_t i = 0; i < 4 && wifiTaskReady && xQueueReceive(noticeQueue, &notice, 0) == pdTRUE; ++i) {
    if (notice.event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED)
      Serial.printf("DISCONNECTED at=%lu reason=%u\n", (unsigned long)notice.at, notice.reason);
    else Serial.printf("GOT_IP event_at=%lu; application confirms status and IP separately\n", (unsigned long)notice.at);
  }
  const uint32_t dropped = droppedNotices.exchange(0);
  if (dropped) Serial.printf("NOTICE_OVERFLOW dropped=%lu\n", (unsigned long)dropped);
  if (uint32_t(now - reportedAt) >= 5000) {
    reportedAt = now;
    Serial.printf("INPUT max_gap_ms=%lu task=%u state=%s\n", (unsigned long)maxGap, wifiTaskReady, phaseName(latest.phase));
    maxGap = 0;
  }
  delay(1);
}

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[2] Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开[3] Arduino3.3.12 WiFiSTA.cpp:兼容begin/disconnect包装[4] Arduino3.3.12 NetworkEvents:独立事件任务与类型[7] IDF5.5.5 WiFi:原始断开reason枚举

06 / 联网实践

用相同环境重建,再上传到桥接串口

先完成F05的core3.3.12安装。在仓库根目录执行,PORT替换为插拔确认的实际串口;CDC关闭,因此Serial日志走USB-to-UART而非另一个原生USB插座。私有头文件修改后必须重新编译上传,单独改电脑文件不会改变设备。

代码 / 配置
arduino-cli compile --fqbn 'esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc' examples/esp32/esp32-wifi-connect-retry
arduino-cli upload --fqbn 'esp32:esp32:esp32s3:FlashSize=8M,PSRAM=opi,FlashMode=qio,PartitionScheme=default_8MB,CDCOnBoot=default,USBMode=hwcdc' -p PORT --upload-property upload.speed=115200 examples/esp32/esp32-wifi-connect-retry
arduino-cli monitor -p PORT --config baudrate=115200
做到这里,你应该看到
构建日志可追溯CLI/core/FQBN;未实板时保留上传与无线验证为未完成。

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[6] 固定DevKitC-1 v1.1:GPIO与桥接串口

07 / 联网实践

四组场景,分别证明配置、失败和恢复

以下都是待执行步骤,本站没有实际IP、无线reason、恢复时间或按钮间隔数据。用下载CSV记录场景、配置标签、attempt、STATE、reason、时间和LOCAL,不记录口令。只有日志和AP管理界面的真实观察才能证明联网。

  1. 占位基线:上传默认包,看到CONFIG_MISSING;发30次间隔t或按30次按钮,LOCAL编号应递增,长按不反复触发。
  2. 正确口令:创建私有配置,重编译上传。确认ONLINE、非零IP与测试AP中的设备;继续输入并保存INPUT最大间隔。
  3. 错误口令:只在私有文件改错后重新上传,保存实际reason及窗口结束后的BACKOFF/新attempt,确认本地输入仍有事件。AP和失败阶段不同,reason可能不同。
  4. AP关闭/恢复:还原正确配置并ONLINE,再关闭测试AP,保存掉线、至少两个后续尝试窗口与LOCAL日志;恢复AP,等待后续窗口重新ONLINE,记录实际恢复耗时。
  5. 重启板重复基线,确认没有把终端旧日志当成新结果。对任何NOTICE_OVERFLOW保留记录,不能把缺失日志解释成没有掉线。
做到这里,你应该看到
正确、错误与AP恢复均有真实事件链;ONLINE不作为HTTP、公网或云服务验收。

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[2] Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开[7] IDF5.5.5 WiFi:原始断开reason枚举

08 / 联网实践

先从配置与频段排起,再解释原始原因

CONFIG_MISSING先核对私有文件名、目录与实际编译入口。START_FAILED先恢复固定core/FQBN、不加载其他网络库,保存启动日志再RESET。一直BACKOFF先核对2.4GHz、SSID大小写、AP安全模式和口令,然后结合实际reason看失败阶段。不要用无限缩短退避掩盖配置错误。

没有LOCAL先发送t区分按钮接线和程序,再查GPIO5、共地与四脚组;看INPUT间隔与创建结果。ONLINE后网页打不开属于更高层任务,本例没有HTTP请求;先保留这条联网基线再进入局域网页篇。

恢复公开安全版时删除私有头文件,再编译上传占位包;这不是擦除设备历史凭据的安全工具。上传超时回F06处理端口、BOOT/RESET与波特率,不先擦Flash或eFuse。

参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[2] Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开[6] 固定DevKitC-1 v1.1:GPIO与桥接串口[8] Arduino3.3.12 WiFiGeneric:persistent与WIFI_STORAGE_RAM

BACK TO THE SOURCE

资料来源与版本说明

Arduino3.3.12具体STA与事件源码核对;应用attempt不是空中关联次数;无本站无线结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. Arduino3.3.12 WiFi:STA API与回调线程注意
  2. Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开
  3. Arduino3.3.12 WiFiSTA.cpp:兼容begin/disconnect包装
  4. Arduino3.3.12 NetworkEvents:独立事件任务与类型
  5. IDF5.5.5 / S3:任务、队列和调度边界
  6. 固定DevKitC-1 v1.1:GPIO与桥接串口
  7. IDF5.5.5 WiFi:原始断开reason枚举
  8. Arduino3.3.12 WiFiGeneric:persistent与WIFI_STORAGE_RAM