从配网已有固件,转到自己管理连接状态
现成固件的配网页、账号绑定与本篇的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:状态、首次内部重试、异步连接与断开
凭据只在本机填写,本地输入可先独立测试
公开示例里只有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
把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枚举
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:任务、队列和调度边界
完整程序:超时、掉线与输入各有自己的路径
程序只用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, ¬ice, 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, ¬ice, 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枚举
用相同环境重建,再上传到桥接串口
先完成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与桥接串口
四组场景,分别证明配置、失败和恢复
以下都是待执行步骤,本站没有实际IP、无线reason、恢复时间或按钮间隔数据。用下载CSV记录场景、配置标签、attempt、STATE、reason、时间和LOCAL,不记录口令。只有日志和AP管理界面的真实观察才能证明联网。
- 占位基线:上传默认包,看到CONFIG_MISSING;发30次间隔t或按30次按钮,LOCAL编号应递增,长按不反复触发。
- 正确口令:创建私有配置,重编译上传。确认ONLINE、非零IP与测试AP中的设备;继续输入并保存INPUT最大间隔。
- 错误口令:只在私有文件改错后重新上传,保存实际reason及窗口结束后的BACKOFF/新attempt,确认本地输入仍有事件。AP和失败阶段不同,reason可能不同。
- AP关闭/恢复:还原正确配置并ONLINE,再关闭测试AP,保存掉线、至少两个后续尝试窗口与LOCAL日志;恢复AP,等待后续窗口重新ONLINE,记录实际恢复耗时。
- 重启板重复基线,确认没有把终端旧日志当成新结果。对任何NOTICE_OVERFLOW保留记录,不能把缺失日志解释成没有掉线。
- 做到这里,你应该看到
- 正确、错误与AP恢复均有真实事件链;ONLINE不作为HTTP、公网或云服务验收。
参考:[1] Arduino3.3.12 WiFi:STA API与回调线程注意[2] Arduino3.3.12 STA.cpp:状态、首次内部重试、异步连接与断开[7] IDF5.5.5 WiFi:原始断开reason枚举
先从配置与频段排起,再解释原始原因
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
资料来源与版本说明
Arduino3.3.12具体STA与事件源码核对;应用attempt不是空中关联次数;无本站无线结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。