多种配网方式:串口配网,网络配网,蓝牙配网,热点配网等
ws2812三色指示灯,状态指示灯
后续会陆续更新最新代码
/*
* t_wifi.cpp — WiFi + NTP 校时 + 三级配网兜底(自包含,可单独烧录)
* 烧录:platformio run -e wifi -t upload
*
* 配网三级兜底(凭据统一存 NVS-无电存储,不硬编码):
* ① 串口:9IEDITWIFI:SSID,PASSWORD / 9IPROV(进配网) / 9IRESETCONFIG(清空)
* ② 网页:无凭据或连不上 → 开热点 ESP32-SETUP,手机连上后浏览器配网
* ③ BLE/BluFi:后续接入(小程序),逻辑同样写入同一 NVS 列表
* ④(下一步)MQTT:订阅指令主题,远程 9IEDITWIFI: 预配置到内部
*
* 主/备逻辑:NVS 中凭据按优先级排序,下标 0 = 主(最后一次连成功者);
* 连接时用 WiFiMulti 按序尝试,连上自动 promote 到队首。
*
* 状态灯(WS2812@IO48):连接中/掉线=红闪(异常色);联网成功=蓝常亮(唯一正常指示灯);打印时间=绿脉冲(可关)
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include "pins.h"
#include "wifi_creds.h"
#include "prov_ap.h"
static WifiCreds creds;
// 开发期便利:NVS 为空时用这组预设“播种”;量产请把 DEV_SEED_CREDS 设为 0
#define DEV_SEED_CREDS 1
static const char *PRESET_SSID[] = {"aaa", "bbb", "huawei", "xiaomi", "iphone"};
static const char *PRESET_PASS[] = {"123456", "123456", "123456", "123456", "123456"};
static WiFiUDP ntpUDP;
static NTPClient timeClient(ntpUDP, "pool.ntp.org", 8 * 3600, 60000); // UTC+8
// ---------------- 状态指示灯(WS2812@IO48) ----------------
// 设计原则:
// · 联网成功并正常运行 = 单一常亮指示灯(蓝),不闪烁 —— 这是唯一“正常状态灯”
// · 仅网络异常(开机连接中 / 在线后掉线)才出现其他颜色(红 闪烁)
// · 打印时间时叠加一次“绿色脉冲”(短暂事件提示,覆盖在常亮蓝之上;不需要可注释掉调用)
// ledTick() 是“状态机驱动”:被 loop / 连接循环反复调用,按当前 ledState 决定灯色。
enum LedState
{
LED_CONNECTING,
LED_ONLINE
};
static LedState ledState = LED_CONNECTING;
static unsigned long ledGreenUntil = 0; // 绿色脉冲结束时刻(覆盖在状态灯之上)
static uint8_t ledBR = 255, ledBG = 0, ledBB = 0; // 异常时的闪烁基色(红)
static bool ledBlinkOn = false;
static unsigned long ledLastToggle = 0;
static void ledSet(uint8_t r, uint8_t g, uint8_t b) { neopixelWrite(PIN_WS2812, r, g, b); }
static void ledConnecting()
{
ledState = LED_CONNECTING;
ledBR = 255;
ledBG = 0;
ledBB = 0;
ledLastToggle = 0;
ledGreenUntil = 0;
}
static void ledOnline()
{
ledState = LED_ONLINE;
ledGreenUntil = 0;
ledSet(0, 0, 255);
} // 常亮蓝
static void ledPulseGreen(unsigned long ms = 600) { ledGreenUntil = millis() + ms; } // 时间脉冲,可调
static void ledTick()
{
unsigned long now = millis();
// 绿色脉冲优先(事件提示):窗口内强制绿,覆盖任何状态色
if (ledGreenUntil && (long)(now - ledGreenUntil) < 0)
{
ledSet(0, 255, 0);
return;
}
if (ledGreenUntil && (long)(now - ledGreenUntil) >= 0)
ledGreenUntil = 0; // 刚过期,本拍清掉
if (ledState == LED_ONLINE)
{
ledSet(0, 0, 255); // 常亮蓝 —— 稳定单一指示灯,不闪烁
return;
}
// 连接中(网络异常):闪烁,300ms 一拍
if (now - ledLastToggle >= 300)
{
ledLastToggle = now;
ledBlinkOn = !ledBlinkOn;
ledSet(ledBlinkOn ? ledBR : 0, ledBlinkOn ? ledBG : 0, ledBlinkOn ? ledBB : 0);
}
}
// ---------------- 串口配网指令 ----------------
static bool needRestart = false;
static void parseSerialLine(const String &line)
{
if (line.startsWith("9IEDITWIFI:"))
{
String body = line.substring(11);
int comma = body.indexOf(',');
if (comma < 0)
{
Serial.println("[PROV] 格式错:9IEDITWIFI:SSID,PASSWORD");
return;
}
String ssid = body.substring(0, comma);
String pass = body.substring(comma + 1);
// 注意:不再以 $ 结尾,回车/换行即结束,密码可包含 $ 等任意字符
ssid.trim();
pass.trim();
if (ssid.length() == 0)
{
Serial.println("[PROV] SSID 空");
return;
}
creds.set(ssid.c_str(), pass.c_str());
Serial.printf("[PROV] 已保存 WiFi: %s,即将重连\n", ssid.c_str());
needRestart = true;
}
else if (line == "9IPROV")
{
Serial.println("[PROV] 收到 9IPROV,重启后进入网页配网");
needRestart = true;
}
else if (line == "9IRESETCONFIG")
{
creds.clearAll();
Serial.println("[PROV] 已清空全部 WiFi 凭据,重启后进入网页配网");
needRestart = true;
}
}
static void checkSerial()
{
static String buf;
while (Serial.available())
{
char c = Serial.read();
if (c == '\n' || c == '\r')
{
if (buf.length())
{
parseSerialLine(buf);
buf = "";
}
}
else
buf += c;
}
}
// ---------------- 连接(主/备依次尝试) ----------------
static bool attemptConnect(unsigned long timeoutMs)
{
WiFiMulti wm;
for (int i = 0; i < creds.count(); i++)
wm.addAP(creds.get(i)->ssid, creds.get(i)->pass);
Serial.print("[WiFi] 连接中");
ledConnecting();
unsigned long t = millis();
int dots = 0;
while (wm.run() != WL_CONNECTED && millis() - t < timeoutMs)
{
ledTick();
checkSerial();
if (needRestart)
return false;
if ((int)((millis() - t) / 200) > dots)
{
Serial.print(".");
dots++;
}
delay(20);
}
Serial.println();
if (WiFi.status() == WL_CONNECTED)
{
String cur = WiFi.SSID();
for (int i = 0; i < creds.count(); i++)
if (cur == creds.get(i)->ssid)
{
creds.promote(i);
creds.save();
break;
}
ledOnline();
Serial.printf("[WiFi] 联网成功 SSID=%s IP=%s RSSI=%ddBm (主)\n",
WiFi.SSID().c_str(), WiFi.localIP().toString().c_str(), WiFi.RSSI());
return true;
}
return false;
}
// 初始化
void setup()
{
Serial.begin(115200);
Serial.setTxTimeoutMs(0);
delay(1000);
Serial.println("=== WiFi + NTP + 三级配网 测试 ===");
creds.begin();
Serial.printf("[WiFi] NVS 已有 %d 条凭据\n", creds.count());
if (creds.empty())
{
#if DEV_SEED_CREDS
Serial.println("[WiFi] NVS 为空,先用开发预设播种(量产请关 DEV_SEED_CREDS)");
for (int i = 0; i < 5; i++)
creds.set(PRESET_SSID[i], PRESET_PASS[i]);
#else
Serial.println("[PROV] NVS 为空,进入网页配网");
startApProvisioning(creds, 180000);
ESP.restart();
#endif
}
WiFi.mode(WIFI_STA);
if (!attemptConnect(40000))
{
Serial.println("[WiFi] 主/备均失败,进入网页配网(180s 超时)");
startApProvisioning(creds, 180000);
ESP.restart();
}
timeClient.begin();
for (int k = 0; k < 5 && !timeClient.update(); k++)
{
ledTick();
delay(300);
}
if (timeClient.isTimeSet())
Serial.printf("[NTP] 校时成功: %s\n", timeClient.getFormattedTime().c_str());
else
Serial.println("[NTP] 校时失败(无网络时间)");
}
// 循环体
void loop()
{
ledTick(); // 状态指示灯
checkSerial(); // 串口监测
if (needRestart)
{
delay(200);
ESP.restart();
}
static unsigned long last = 0;
if (WiFi.status() == WL_CONNECTED)
{
if (millis() - last > 5000)
{
last = millis();
// 更新时钟
timeClient.update();
if (timeClient.isTimeSet())
{
time_t t = timeClient.getEpochTime();
struct tm *ti = localtime(&t);
Serial.printf("[时间] %04d-%02d-%02d %02d:%02d\n",
ti->tm_year + 1900, ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min);
// ledPulseGreen(); // 打印时间时绿脉冲一下;不需要此提示可注释掉本行
}
}
}
else
{
Serial.println("[WiFi] 在线后掉线,重新连接");
WiFi.mode(WIFI_STA);
attemptConnect(40000); // 内部会切到 ledConnecting()(红闪=网络异常色)
}
delay(50);
}