2026-06-28 04:32:18 +08:00
|
|
|
|
/*
|
2026-05-31 03:20:26 +08:00
|
|
|
|
* wifi_ota.cpp - WiFi OTA 服务器实现
|
|
|
|
|
|
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
|
2026-04-27 00:42:26 +08:00
|
|
|
|
* T4.1 - T4.5
|
|
|
|
|
|
*/
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
|
|
|
|
#include "freertos/task.h"
|
|
|
|
|
|
#include "freertos/semphr.h"
|
2026-06-17 11:52:26 +08:00
|
|
|
|
#include "esp_log.h"
|
2026-04-27 00:42:26 +08:00
|
|
|
|
#include "esp_wifi.h"
|
|
|
|
|
|
#include "esp_netif.h"
|
|
|
|
|
|
#include "esp_http_server.h"
|
|
|
|
|
|
#include "esp_system.h"
|
2026-05-05 03:28:44 +08:00
|
|
|
|
#include "esp_chip_info.h"
|
|
|
|
|
|
#include "esp_ota_ops.h"
|
2026-04-27 00:42:26 +08:00
|
|
|
|
#include "nvs_flash.h"
|
|
|
|
|
|
#include "lwip/netdb.h"
|
|
|
|
|
|
#include "esp_netif.h"
|
|
|
|
|
|
#include "esp_netif_types.h"
|
2026-05-05 03:28:44 +08:00
|
|
|
|
#include "esp_event.h"
|
|
|
|
|
|
#include "esp_mac.h"
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
#include "app_config.h"
|
|
|
|
|
|
#include "wifi_ota.h"
|
2026-06-14 23:03:28 +08:00
|
|
|
|
#include "esp32_ota_core.h"
|
2026-06-17 02:18:42 +08:00
|
|
|
|
#include "ota_coordinator.h"
|
2026-04-27 00:42:26 +08:00
|
|
|
|
#include "ota_relay.h"
|
|
|
|
|
|
#include "health_monitor.h"
|
2026-04-27 02:34:47 +08:00
|
|
|
|
#include "monitor/debug_log.h"
|
2026-04-27 03:22:49 +08:00
|
|
|
|
#include "spi_master.h"
|
2026-06-20 00:46:04 +08:00
|
|
|
|
#include "ble_ota.h"
|
2026-06-21 23:48:31 +08:00
|
|
|
|
#include "http_handlers.h"
|
2026-06-03 03:14:01 +08:00
|
|
|
|
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-07-15 02:23:25 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// 文件作用域 static helper
|
|
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 清除指定 NVS 命名空间下的全部键(erase_all + commit + close)。
|
|
|
|
|
|
* 纯函数,内部不打日志,由调用方负责日志输出。
|
|
|
|
|
|
* @param ns NVS 命名空间字符串(例如 "nvs.net80211"、"phy")。
|
|
|
|
|
|
* @return ESP_OK 成功;否则返回 nvs_open 的错误码。
|
|
|
|
|
|
*/
|
|
|
|
|
|
static esp_err_t clearNvsNamespace(const char *ns)
|
|
|
|
|
|
{
|
|
|
|
|
|
nvs_handle_t h;
|
|
|
|
|
|
esp_err_t err = nvs_open(ns, NVS_READWRITE, &h);
|
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
|
return err;
|
|
|
|
|
|
}
|
|
|
|
|
|
nvs_erase_all(h);
|
|
|
|
|
|
nvs_commit(h);
|
|
|
|
|
|
nvs_close(h);
|
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
// ============================================================
|
2026-06-15 01:14:29 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 获取 WifiOta 单例实例(静态局部变量实现)。
|
|
|
|
|
|
* @return WifiOta 单例引用。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta& WifiOta::instance()
|
|
|
|
|
|
{
|
|
|
|
|
|
static WifiOta inst;
|
|
|
|
|
|
return inst;
|
|
|
|
|
|
}
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-06-14 17:10:24 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// STA 后台重连任务(避免在系统事件回调中调用 esp_wifi_connect 死锁)
|
|
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 独立 FreeRTOS 任务,处理 STA 连接/重连的退避延迟和 esp_wifi_connect() 调用。
|
|
|
|
|
|
* 事件回调仅通过 xTaskNotifyGive 唤醒本任务,不直接在系统事件任务中阻塞或调用 WiFi API。
|
2026-06-21 23:48:31 +08:00
|
|
|
|
* @param pvParameters FreeRTOS 任务参数(未使用)。
|
2026-06-14 17:10:24 +08:00
|
|
|
|
*/
|
|
|
|
|
|
void WifiOta::reconnectTask(void* pvParameters)
|
|
|
|
|
|
{
|
|
|
|
|
|
(void)pvParameters;
|
|
|
|
|
|
while (1) {
|
|
|
|
|
|
// 等待事件回调通知(阻塞,不消耗 CPU)
|
|
|
|
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
|
|
|
|
|
|
|
|
|
|
WifiOta& self = WifiOta::instance();
|
|
|
|
|
|
|
|
|
|
|
|
// 已连接或已放弃,无需处理
|
|
|
|
|
|
if (self.getStaConnected() || self.getStaRetryExhausted()) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.stateLock();
|
|
|
|
|
|
|
|
|
|
|
|
// 达到最大重试次数
|
|
|
|
|
|
if (self.getStaRetryCount() >= WIFI_STA_MAX_RETRY) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", self.getStaRetryCount());
|
|
|
|
|
|
self.setStaRetryExhausted(true);
|
|
|
|
|
|
self.stateUnlock();
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.setStaRetryCount(self.getStaRetryCount() + 1);
|
|
|
|
|
|
int retryCount = self.getStaRetryCount();
|
|
|
|
|
|
|
|
|
|
|
|
// 计算退避延迟:冷启动前 3 次递增退避,其余固定 1s
|
|
|
|
|
|
int delay_ms = 0;
|
|
|
|
|
|
if (self.m_coldBoot && retryCount <= 3) {
|
|
|
|
|
|
delay_ms = 2000 * retryCount; // 2s, 4s, 6s
|
|
|
|
|
|
DEBUG_INFO("WIFI", "冷启动退避重连第 %d 次 (等 %dms)",
|
|
|
|
|
|
retryCount, delay_ms);
|
|
|
|
|
|
} else if (retryCount > 1) {
|
|
|
|
|
|
delay_ms = 1000; // 普通重连 1s 延迟
|
|
|
|
|
|
DEBUG_INFO("WIFI", "重连第 %d 次...", retryCount);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "首次连接...");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.stateUnlock();
|
|
|
|
|
|
|
|
|
|
|
|
if (delay_ms > 0) {
|
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟后再次检查状态,防止期间已连接或已放弃
|
|
|
|
|
|
if (!self.getStaConnected() && !self.getStaRetryExhausted()) {
|
|
|
|
|
|
esp_err_t err = esp_wifi_connect();
|
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "esp_wifi_connect() 失败: %s", esp_err_to_name(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// WiFi/IP 事件回调(事件驱动连接)
|
|
|
|
|
|
// ----------------------------------------
|
2026-05-26 02:44:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief WiFi/IP 事件回调,处理 STA 启停、重连和获取 IP 等状态变化。
|
|
|
|
|
|
* @param arg 回调用户参数。
|
|
|
|
|
|
* @param event_base 事件基类(WIFI_EVENT 或 IP_EVENT)。
|
|
|
|
|
|
* @param event_id 具体事件 ID。
|
|
|
|
|
|
* @param event_data 事件附带数据。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
|
2026-05-05 03:28:44 +08:00
|
|
|
|
int32_t event_id, void* event_data)
|
|
|
|
|
|
{
|
2026-06-14 17:10:24 +08:00
|
|
|
|
WifiOta& inst = WifiOta::instance();
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
if (event_base == WIFI_EVENT) {
|
|
|
|
|
|
switch (event_id) {
|
|
|
|
|
|
case WIFI_EVENT_STA_START:
|
|
|
|
|
|
DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
|
2026-06-14 17:10:24 +08:00
|
|
|
|
inst.stateLock();
|
|
|
|
|
|
inst.setStaRetryCount(0);
|
|
|
|
|
|
inst.setStaRetryExhausted(false);
|
|
|
|
|
|
inst.stateUnlock();
|
|
|
|
|
|
if (inst.m_reconnectTask) {
|
|
|
|
|
|
xTaskNotifyGive(inst.m_reconnectTask);
|
|
|
|
|
|
}
|
2026-05-05 03:28:44 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WIFI_EVENT_STA_CONNECTED:
|
|
|
|
|
|
DEBUG_INFO("WIFI", "STA 已关联到 AP");
|
2026-06-14 17:10:24 +08:00
|
|
|
|
inst.stateLock();
|
|
|
|
|
|
inst.setStaConnected(true);
|
|
|
|
|
|
inst.setStaRetryCount(0);
|
|
|
|
|
|
inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志
|
|
|
|
|
|
inst.stateUnlock();
|
2026-07-16 01:24:12 +08:00
|
|
|
|
// 推送 WiFi 连接状态给 HealthMonitor
|
2026-06-22 01:53:16 +08:00
|
|
|
|
HealthMonitor::instance().setWifiConnected(true);
|
2026-05-05 03:28:44 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case WIFI_EVENT_STA_DISCONNECTED: {
|
|
|
|
|
|
wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
|
2026-06-14 17:10:24 +08:00
|
|
|
|
inst.stateLock();
|
|
|
|
|
|
inst.setStaConnected(false);
|
|
|
|
|
|
bool exhausted = inst.getStaRetryExhausted();
|
|
|
|
|
|
int retryCount = inst.getStaRetryCount();
|
|
|
|
|
|
inst.stateUnlock();
|
2026-07-16 01:24:12 +08:00
|
|
|
|
// 推送 WiFi 断开状态给 HealthMonitor
|
2026-06-22 01:53:16 +08:00
|
|
|
|
HealthMonitor::instance().setWifiConnected(false);
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
2026-06-14 17:10:24 +08:00
|
|
|
|
if (!exhausted) {
|
|
|
|
|
|
DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d",
|
|
|
|
|
|
disconn->reason, retryCount);
|
|
|
|
|
|
if (inst.m_reconnectTask) {
|
|
|
|
|
|
xTaskNotifyGive(inst.m_reconnectTask);
|
2026-05-30 20:04:04 +08:00
|
|
|
|
}
|
2026-05-05 03:28:44 +08:00
|
|
|
|
} else {
|
2026-06-14 17:10:24 +08:00
|
|
|
|
DEBUG_WARN("WIFI", "STA 断开, 重试次数已耗尽");
|
2026-05-05 03:28:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case WIFI_EVENT_AP_STACONNECTED: {
|
|
|
|
|
|
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
|
|
|
|
|
|
DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 连入, AID=%d", MAC2STR(event->mac), event->aid);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case WIFI_EVENT_AP_STADISCONNECTED: {
|
|
|
|
|
|
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
|
|
|
|
|
|
DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 离开, AID=%d, reason=%d",
|
|
|
|
|
|
MAC2STR(event->mac), event->aid, event->reason);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (event_base == IP_EVENT) {
|
|
|
|
|
|
switch (event_id) {
|
|
|
|
|
|
case IP_EVENT_STA_GOT_IP: {
|
|
|
|
|
|
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
|
|
|
|
|
|
DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
2026-05-31 03:20:26 +08:00
|
|
|
|
if (WifiOta::instance().m_staGotIpSem) {
|
|
|
|
|
|
xSemaphoreGive(WifiOta::instance().m_staGotIpSem);
|
2026-05-05 03:28:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 00:42:26 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// WiFi AP 模式初始化
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
void WifiOta::initAp()
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
2026-04-28 01:06:17 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_AP;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
|
|
|
|
|
if (ap_netif == NULL) {
|
|
|
|
|
|
ap_netif = esp_netif_create_default_wifi_ap();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ap_netif != NULL) {
|
|
|
|
|
|
esp_netif_dhcps_stop(ap_netif);
|
|
|
|
|
|
|
|
|
|
|
|
esp_netif_ip_info_t ip_info;
|
|
|
|
|
|
memset(&ip_info, 0, sizeof(ip_info));
|
|
|
|
|
|
// 方案 A:SoftAP 使用 192.168.31.0/24 网段
|
|
|
|
|
|
inet_pton(AF_INET, "192.168.31.1", &ip_info.ip);
|
|
|
|
|
|
inet_pton(AF_INET, "192.168.31.1", &ip_info.gw);
|
|
|
|
|
|
inet_pton(AF_INET, "255.255.255.0", &ip_info.netmask);
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
|
|
|
|
|
|
|
|
|
|
|
|
esp_netif_dhcps_start(ap_netif);
|
|
|
|
|
|
DEBUG_INFO("WIFI", "AP IP 已设置: 192.168.31.1");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 00:42:26 +08:00
|
|
|
|
wifi_config_t wifi_config = {};
|
2026-04-27 02:34:47 +08:00
|
|
|
|
memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID) + 1);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
|
2026-04-27 02:34:47 +08:00
|
|
|
|
memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD) + 1);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
|
|
|
|
|
|
wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
|
|
|
|
|
wifi_config.ap.channel = WIFI_AP_CHANNEL;
|
|
|
|
|
|
|
2026-04-28 01:06:17 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "AP SSID: %s, Channel: %d", WIFI_AP_SSID, WIFI_AP_CHANNEL);
|
|
|
|
|
|
|
2026-04-27 00:42:26 +08:00
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_start());
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
|
2026-05-05 03:28:44 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1");
|
|
|
|
|
|
DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1");
|
2026-04-28 01:06:17 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// WiFi STA 模式初始化
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 初始化 STA 模式并连接预配置的路由器。
|
|
|
|
|
|
* @return ESP_OK 成功,其他失败。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_err_t WifiOta::initSta()
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
2026-04-28 01:06:17 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_STA;
|
2026-06-14 17:10:24 +08:00
|
|
|
|
WifiOta::instance().setStaRetryCount(0);
|
|
|
|
|
|
WifiOta::instance().setStaRetryExhausted(false);
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
|
|
|
|
|
// ★ 设置国家代码:中国,允许信道 1-13
|
|
|
|
|
|
// ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
|
|
|
|
|
|
wifi_country_t country = {};
|
|
|
|
|
|
memcpy(country.cc, "CN", 3);
|
|
|
|
|
|
country.schan = 1;
|
|
|
|
|
|
country.nchan = 13;
|
|
|
|
|
|
country.policy = WIFI_COUNTRY_POLICY_MANUAL;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
wifi_config_t wifi_config = {};
|
2026-05-05 03:28:44 +08:00
|
|
|
|
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
|
|
|
|
|
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
|
|
|
|
|
|
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
wifi_config.sta.pmf_cfg.capable = true;
|
|
|
|
|
|
wifi_config.sta.pmf_cfg.required = false;
|
2026-05-05 03:28:44 +08:00
|
|
|
|
wifi_config.sta.listen_interval = 3;
|
2026-04-27 02:34:47 +08:00
|
|
|
|
memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, strlen(WIFI_STA_SSID) + 1);
|
|
|
|
|
|
memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, strlen(WIFI_STA_PASSWORD) + 1);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "STA SSID: %s (全信道扫描, CN 国家代码)", WIFI_STA_SSID);
|
2026-04-28 01:06:17 +08:00
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_country(&country));
|
2026-04-27 00:42:26 +08:00
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
|
|
|
|
|
|
2026-07-17 02:02:36 +08:00
|
|
|
|
/* 冷启动时在 wifi_start 前短延时 */
|
2026-05-31 03:20:26 +08:00
|
|
|
|
if (WifiOta::instance().m_coldBoot) {
|
2026-07-17 02:02:36 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "冷启动: wifi_start 前等待 300ms");
|
2026-07-17 00:24:09 +08:00
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(300));
|
2026-05-30 20:04:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_start());
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
|
|
|
|
|
|
|
|
|
|
|
DEBUG_INFO("WIFI", "WiFi STA 启动, SSID: %s, 事件驱动连接...", WIFI_STA_SSID);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
|
|
|
|
|
|
* @param ip_str 输出缓冲区,存放获取到的 IP 字符串。
|
2026-06-21 23:48:31 +08:00
|
|
|
|
* @param ip_len 缓冲区长度。
|
2026-05-05 04:48:31 +08:00
|
|
|
|
* @return ESP_OK 连接成功,其他失败。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_err_t WifiOta::waitStaConnected(char *ip_str, size_t ip_len)
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
2026-07-15 02:23:25 +08:00
|
|
|
|
SemaphoreHandle_t wait_sem = WifiOta::instance().m_staGotIpSem;
|
|
|
|
|
|
const char *wait_name = "STA_GOT_IP";
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
|
|
|
|
|
if (wait_sem == NULL) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
|
|
|
|
|
|
return ESP_ERR_INVALID_STATE;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
// ★ 使用轮询等待,检查 WifiOta::instance().m_staRetryExhausted 标志加速退出
|
2026-05-30 20:04:04 +08:00
|
|
|
|
// 不再一次性等完整超时,而是每秒检查一次重试耗尽状态
|
2026-05-05 03:28:44 +08:00
|
|
|
|
TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
|
2026-05-30 20:04:04 +08:00
|
|
|
|
TickType_t start_tick = xTaskGetTickCount();
|
|
|
|
|
|
TickType_t deadline = start_tick + timeout_ticks;
|
|
|
|
|
|
|
|
|
|
|
|
while (xTaskGetTickCount() < deadline) {
|
|
|
|
|
|
// 每次最多等 1 秒
|
|
|
|
|
|
TickType_t wait_until = xTaskGetTickCount() + pdMS_TO_TICKS(1000);
|
|
|
|
|
|
if (wait_until > deadline) wait_until = deadline;
|
|
|
|
|
|
|
|
|
|
|
|
if (xSemaphoreTake(wait_sem, wait_until - xTaskGetTickCount()) == pdTRUE) {
|
|
|
|
|
|
esp_netif_ip_info_t ip_info;
|
|
|
|
|
|
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
|
|
|
|
|
if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
|
|
|
|
|
|
if (ip_str) {
|
2026-05-31 03:20:26 +08:00
|
|
|
|
snprintf(ip_str, ip_len, IPSTR, IP2STR(&ip_info.ip));
|
2026-05-30 20:04:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
|
2026-05-05 03:28:44 +08:00
|
|
|
|
}
|
2026-06-14 17:10:24 +08:00
|
|
|
|
WifiOta::instance().setStaConnected(true);
|
2026-05-30 20:04:04 +08:00
|
|
|
|
return ESP_OK;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ★ 重试已耗尽,不再等待
|
2026-06-14 17:10:24 +08:00
|
|
|
|
if (WifiOta::instance().getStaRetryExhausted()) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "STA 重试已耗尽 (%d 次),立即切换 AP", WifiOta::instance().getStaRetryCount());
|
2026-05-30 20:04:04 +08:00
|
|
|
|
return ESP_ERR_TIMEOUT;
|
2026-05-05 03:28:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 17:10:24 +08:00
|
|
|
|
DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, WifiOta::instance().getStaRetryCount());
|
2026-04-27 00:42:26 +08:00
|
|
|
|
return ESP_ERR_TIMEOUT;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// ESP32 OTA 内部函数
|
2026-06-14 23:03:28 +08:00
|
|
|
|
// 注:begin/write/finalize/abort 已抽离到 Esp32OtaCore(main/ota/esp32_ota_core.{h,cpp})
|
|
|
|
|
|
// WiFi 与 BLE 通道共用同一核心,本文件仅负责 HTTP 传输层。
|
2026-06-14 17:10:24 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
2026-04-27 00:42:26 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// 启动 HTTP OTA 服务器
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 启动 HTTP OTA 服务器,注册各接口路由。
|
|
|
|
|
|
* @return ESP_OK 成功,其他失败。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_err_t WifiOta::httpServerStart()
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
|
|
|
|
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
|
|
|
|
|
config.server_port = HTTP_SERVER_PORT;
|
|
|
|
|
|
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
2026-05-05 03:28:44 +08:00
|
|
|
|
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
2026-07-20 03:10:42 +08:00
|
|
|
|
/* Socket 预算(无法“合并” httpd 内部 3 个 fd:listen + ctrl 对):
|
|
|
|
|
|
*
|
|
|
|
|
|
* LWIP 总池 CONFIG_LWIP_MAX_SOCKETS
|
|
|
|
|
|
* ├─ httpd 内部固定 3
|
|
|
|
|
|
* ├─ max_open_sockets = 业务并发(下面 want)
|
|
|
|
|
|
* └─ 余量:UDP 调试日志 1 + 偶发其它
|
|
|
|
|
|
*
|
|
|
|
|
|
* 约束:max_open_sockets <= LWIP_MAX_SOCKETS - 3
|
|
|
|
|
|
*
|
|
|
|
|
|
* 业务并发压到 4(tools 单浏览器已足够):
|
|
|
|
|
|
* 1× /system/status 轮询 + 1× /logs 轮询 + 1× 操作(OTA/CAN) + 1× 余量
|
|
|
|
|
|
* OTA 大上传期间前端 pausePolling,只占 1 条长连接,不与轮询抢槽。
|
|
|
|
|
|
* lru_purge:满槽时踢最久空闲连接,避免 EMFILE 卡死。
|
|
|
|
|
|
* 硬编码 7 在 LWIP=8 时会 ESP_ERR_INVALID_ARG → HTTP 起不来。 */
|
|
|
|
|
|
{
|
|
|
|
|
|
const int lwip_cap = CONFIG_LWIP_MAX_SOCKETS - 3; // httpd 硬上限
|
|
|
|
|
|
const int want = 4; // 合并并发需求后的业务槽位(原 7 偏松)
|
|
|
|
|
|
config.max_open_sockets = (lwip_cap < want) ? ((lwip_cap > 1) ? lwip_cap : 1) : want;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 满连接时淘汰最久未用的空闲 socket(相当于“复用/腾挪”槽位) */
|
2026-07-16 01:24:12 +08:00
|
|
|
|
config.lru_purge_enable = true;
|
2026-07-17 02:02:36 +08:00
|
|
|
|
/* idle 收发超时(秒);进行中的 handler 连接不受此 idle 检测踢掉 */
|
2026-07-16 01:24:12 +08:00
|
|
|
|
config.recv_wait_timeout = 10;
|
|
|
|
|
|
config.send_wait_timeout = 10;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-04-28 01:06:17 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
|
|
|
|
|
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_err_t err = httpd_start(&WifiOta::instance().m_httpServer, &config);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
|
|
|
|
|
|
return err;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 03:12:21 +08:00
|
|
|
|
// GD32 固件就绪真相源为 OtaCoordinator / NVS meta(/system/status),
|
|
|
|
|
|
// 不再在 WifiOta 上维护影子 storage_done 标志。
|
2026-05-26 02:44:57 +08:00
|
|
|
|
{
|
|
|
|
|
|
ota_meta_t boot_meta;
|
2026-06-21 23:48:31 +08:00
|
|
|
|
if (OtaCoordinator::instance().loadGd32Meta(&boot_meta) == ESP_OK &&
|
2026-05-26 02:44:57 +08:00
|
|
|
|
boot_meta.total_size > 0 && boot_meta.stored_size >= boot_meta.total_size) {
|
2026-07-13 03:12:21 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "从 NVS 恢复: GD32 固件已就绪 (%u bytes)", boot_meta.total_size);
|
2026-05-26 02:44:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 04:32:18 +08:00
|
|
|
|
struct UriEntry {
|
|
|
|
|
|
const char *uri;
|
|
|
|
|
|
httpd_method_t method;
|
|
|
|
|
|
esp_err_t (*handler)(httpd_req_t *);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
};
|
2026-06-28 04:32:18 +08:00
|
|
|
|
static const UriEntry kHttpRoutes[] = {
|
|
|
|
|
|
{"/ota/upload", HTTP_POST, ota_upload_handler},
|
|
|
|
|
|
{"/ota/relay", HTTP_POST, ota_relay_trigger_handler},
|
|
|
|
|
|
{"/ota/update", HTTP_POST, esp32_ota_update_handler},
|
|
|
|
|
|
{"/restart", HTTP_POST, restart_handler},
|
|
|
|
|
|
{"/logs", HTTP_GET, logs_handler},
|
|
|
|
|
|
{"/system/status", HTTP_GET, system_status_handler},
|
|
|
|
|
|
{"/system/probe", HTTP_GET, system_probe_handler},
|
2026-07-20 03:10:42 +08:00
|
|
|
|
{"/system/heap_low_log", HTTP_GET, system_heap_low_log_handler},
|
2026-07-16 23:00:15 +08:00
|
|
|
|
{"/can/send_batch", HTTP_POST, can_send_batch_handler},
|
2026-06-29 01:51:44 +08:00
|
|
|
|
{"/can/feedback", HTTP_GET, can_feedback_handler},
|
|
|
|
|
|
{"/can/mode", HTTP_GET, can_mode_handler},
|
|
|
|
|
|
{"/can/mode", HTTP_POST, can_mode_handler},
|
2026-06-28 04:32:18 +08:00
|
|
|
|
{"/spi/send", HTTP_POST, spi_send_handler},
|
2026-05-05 03:28:44 +08:00
|
|
|
|
};
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-07-20 03:10:42 +08:00
|
|
|
|
/* 同一 path 可有多 method(如 GET+POST);OPTIONS 每个 path 只注册一次,
|
|
|
|
|
|
* 否则 httpd 报 “handler ... method 6 already registered”。 */
|
|
|
|
|
|
for (size_t i = 0; i < sizeof(kHttpRoutes) / sizeof(kHttpRoutes[0]); ++i) {
|
|
|
|
|
|
const UriEntry &r = kHttpRoutes[i];
|
2026-06-28 04:32:18 +08:00
|
|
|
|
httpd_uri_t main_uri = {};
|
|
|
|
|
|
main_uri.uri = r.uri;
|
|
|
|
|
|
main_uri.method = r.method;
|
|
|
|
|
|
main_uri.handler = r.handler;
|
|
|
|
|
|
main_uri.user_ctx = NULL;
|
|
|
|
|
|
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &main_uri);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-07-20 03:10:42 +08:00
|
|
|
|
bool need_opts = true;
|
|
|
|
|
|
for (size_t j = 0; j < i; ++j) {
|
|
|
|
|
|
if (strcmp(kHttpRoutes[j].uri, r.uri) == 0) {
|
|
|
|
|
|
need_opts = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (need_opts) {
|
|
|
|
|
|
httpd_uri_t opts_uri = {};
|
|
|
|
|
|
opts_uri.uri = r.uri;
|
|
|
|
|
|
opts_uri.method = HTTP_OPTIONS;
|
|
|
|
|
|
opts_uri.handler = http_options_handler;
|
|
|
|
|
|
opts_uri.user_ctx = NULL;
|
|
|
|
|
|
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
|
|
|
|
|
|
}
|
2026-06-28 04:32:18 +08:00
|
|
|
|
}
|
2026-05-26 02:44:57 +08:00
|
|
|
|
|
2026-07-20 03:10:42 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "HTTP端点已注册 (ota/can/spi/logs/status)");
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// 公开 API
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 获取当前 WiFi 工作模式(AP 或 STA)。
|
|
|
|
|
|
* @return 当前 WiFi 模式枚举值。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
ota_wifi_mode_t WifiOta::getMode()
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
2026-05-31 03:20:26 +08:00
|
|
|
|
return WifiOta::instance().m_wifiMode;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 获取当前 WiFi 信号强度。
|
|
|
|
|
|
* @return 信号强度(dBm),未连接时返回无效值。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
int8_t WifiOta::getRssi()
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
2026-06-14 17:10:24 +08:00
|
|
|
|
if (WifiOta::instance().m_wifiMode != OTA_WIFI_MODE_STA || !WifiOta::instance().getStaConnected()) {
|
2026-04-27 00:42:26 +08:00
|
|
|
|
return -127;
|
|
|
|
|
|
}
|
|
|
|
|
|
wifi_ap_record_t ap_info;
|
|
|
|
|
|
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
|
|
|
|
|
return ap_info.rssi;
|
|
|
|
|
|
}
|
|
|
|
|
|
return -127;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 判断 STA 是否已连接到上级 WiFi。
|
|
|
|
|
|
* @return true 已连接,false 未连接。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
bool WifiOta::isConnected()
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
2026-06-14 17:10:24 +08:00
|
|
|
|
return WifiOta::instance().getStaConnected();
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 01:53:16 +08:00
|
|
|
|
// ============================================================
|
2026-07-16 01:24:12 +08:00
|
|
|
|
// IBleCoexListener 接口实现
|
2026-06-22 01:53:16 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
void WifiOta::onBleOtaStart()
|
|
|
|
|
|
{
|
|
|
|
|
|
setBleOtaActive(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void WifiOta::onBleOtaEnd()
|
|
|
|
|
|
{
|
|
|
|
|
|
setBleOtaActive(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// ★ WiFi 早期初始化(在 app_main 调度器启动前调用)
|
|
|
|
|
|
// 包括:NVS → netif → event handler → WiFi driver → 配置 → connect
|
|
|
|
|
|
// 返回:true = STA 模式已启动(正在连接),false = 需要切 AP
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
|
|
|
|
|
|
* @return true STA 初始化中,false 需回退 AP。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
bool WifiOta::earlyInit()
|
2026-05-05 03:28:44 +08:00
|
|
|
|
{
|
2026-07-17 00:24:09 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "=== WiFi 早期初始化 ===");
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
2026-07-17 00:24:09 +08:00
|
|
|
|
// ★ 冷启动检测:判断是否为上电/欠压复位
|
2026-05-30 20:04:04 +08:00
|
|
|
|
esp_reset_reason_t reset_reason = esp_reset_reason();
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta::instance().m_coldBoot = (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_BROWNOUT);
|
2026-07-17 00:24:09 +08:00
|
|
|
|
|
2026-05-30 20:04:04 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "复位原因: %d (%s), 冷启动: %s",
|
|
|
|
|
|
reset_reason,
|
|
|
|
|
|
(reset_reason == ESP_RST_POWERON) ? "POWER_ON" :
|
|
|
|
|
|
(reset_reason == ESP_RST_BROWNOUT) ? "BROWNOUT" :
|
|
|
|
|
|
(reset_reason == ESP_RST_SW) ? "SOFTWARE" :
|
|
|
|
|
|
(reset_reason == ESP_RST_PANIC) ? "PANIC" :
|
|
|
|
|
|
(reset_reason == ESP_RST_INT_WDT) ? "INT_WDT" :
|
|
|
|
|
|
(reset_reason == ESP_RST_TASK_WDT) ? "TASK_WDT" :
|
|
|
|
|
|
(reset_reason == ESP_RST_DEEPSLEEP) ? "DEEP_SLEEP" :
|
|
|
|
|
|
"OTHER",
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta::instance().m_coldBoot ? "是" : "否");
|
2026-05-30 20:04:04 +08:00
|
|
|
|
|
2026-07-17 02:02:36 +08:00
|
|
|
|
/* 冷启动:电源与外设上电完成后再初始化 NVS/WiFi */
|
2026-07-17 00:24:09 +08:00
|
|
|
|
if (WifiOta::instance().m_coldBoot) {
|
2026-07-17 02:02:36 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "冷启动: 等待 400ms 后初始化 NVS/WiFi");
|
2026-07-17 00:24:09 +08:00
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(400));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
esp_err_t err = nvs_flash_init();
|
2026-05-30 20:04:04 +08:00
|
|
|
|
bool nvs_erased = false;
|
2026-05-05 03:28:44 +08:00
|
|
|
|
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
|
|
|
|
// NVS 分区损坏或版本不匹配,擦除后重试
|
|
|
|
|
|
DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
|
|
|
|
|
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
2026-05-30 20:04:04 +08:00
|
|
|
|
nvs_erased = true;
|
2026-05-05 03:28:44 +08:00
|
|
|
|
err = nvs_flash_init();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
|
|
|
|
|
|
} else {
|
2026-05-30 20:04:04 +08:00
|
|
|
|
if (nvs_erased) {
|
2026-07-20 03:10:42 +08:00
|
|
|
|
DEBUG_WARN("WIFI", "NVS重建,PHY校准已清,将重校");
|
2026-05-30 20:04:04 +08:00
|
|
|
|
}
|
2026-05-05 03:28:44 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "NVS 初始化成功");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 02:02:36 +08:00
|
|
|
|
/* 冷启动清理 WiFi 连接缓存;欠压复位后再清 PHY 强制重校准 */
|
2026-05-31 03:20:26 +08:00
|
|
|
|
if (WifiOta::instance().m_coldBoot) {
|
2026-07-15 02:23:25 +08:00
|
|
|
|
err = clearNvsNamespace("nvs.net80211");
|
2026-05-30 20:04:04 +08:00
|
|
|
|
if (err == ESP_OK) {
|
2026-06-17 11:52:26 +08:00
|
|
|
|
DEBUG_DBG("WIFI", "冷启动: 已清除 WiFi NVS 缓存 (nvs.net80211)");
|
2026-05-30 20:04:04 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "冷启动: 无 WiFi NVS 缓存分区,跳过清除");
|
|
|
|
|
|
}
|
2026-07-17 00:24:09 +08:00
|
|
|
|
if (reset_reason == ESP_RST_BROWNOUT) {
|
|
|
|
|
|
err = clearNvsNamespace("phy");
|
|
|
|
|
|
if (err == ESP_OK) {
|
2026-07-17 02:02:36 +08:00
|
|
|
|
DEBUG_WARN("WIFI", "欠压复位后: 已清除 PHY 校准数据");
|
2026-07-17 00:24:09 +08:00
|
|
|
|
}
|
2026-05-30 20:04:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 17:10:24 +08:00
|
|
|
|
// ★ 创建状态互斥锁(保护 WiFi 连接状态、OTA 中继标志等跨任务共享变量)
|
|
|
|
|
|
if (WifiOta::instance().getStateMutex() == NULL) {
|
|
|
|
|
|
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
|
|
|
|
|
|
if (mtx) {
|
|
|
|
|
|
// 通过临时获取再赋值(私有成员在同类内可访问)
|
|
|
|
|
|
WifiOta::instance().m_stateMutex = mtx;
|
2026-06-17 11:52:26 +08:00
|
|
|
|
DEBUG_DBG("WIFI", "状态互斥锁已创建");
|
2026-06-14 17:10:24 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "创建状态互斥锁失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta::instance().m_staGotIpSem = xSemaphoreCreateBinary();
|
2026-07-15 02:23:25 +08:00
|
|
|
|
if (!WifiOta::instance().m_staGotIpSem) {
|
2026-05-05 03:28:44 +08:00
|
|
|
|
DEBUG_ERR("WIFI", "创建信号量失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 17:10:24 +08:00
|
|
|
|
// 创建后台重连任务:避免在系统事件任务中调用 esp_wifi_connect()/vTaskDelay 导致死锁
|
|
|
|
|
|
if (WifiOta::instance().m_reconnectTask == nullptr) {
|
|
|
|
|
|
BaseType_t ret = xTaskCreate(&WifiOta::reconnectTask, "wifi_reconnect",
|
2026-07-16 01:24:12 +08:00
|
|
|
|
TASK_STACK_WIFI_RECONNECT, NULL, 5,
|
2026-06-14 17:10:24 +08:00
|
|
|
|
&WifiOta::instance().m_reconnectTask);
|
|
|
|
|
|
if (ret != pdPASS) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "创建重连任务失败");
|
|
|
|
|
|
} else {
|
2026-06-17 11:52:26 +08:00
|
|
|
|
DEBUG_DBG("WIFI", "WiFi 重连任务已创建");
|
2026-06-14 17:10:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
|
2026-05-31 03:20:26 +08:00
|
|
|
|
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL));
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL));
|
2026-06-17 11:52:26 +08:00
|
|
|
|
DEBUG_DBG("WIFI", "WiFi/IP 事件回调已注册");
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
|
|
|
|
|
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
|
|
|
|
|
if (sta_netif == NULL) {
|
|
|
|
|
|
sta_netif = esp_netif_create_default_wifi_sta();
|
|
|
|
|
|
if (sta_netif == NULL) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "创建 STA netif 失败");
|
|
|
|
|
|
} else {
|
2026-06-17 11:52:26 +08:00
|
|
|
|
DEBUG_DBG("WIFI", "STA netif 已创建");
|
2026-05-05 03:28:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 11:52:26 +08:00
|
|
|
|
// ★ 压制 WiFi 框架内部 INFO 日志(pp/net80211/wifi/wifi_init 等 tag 大量启动打印)
|
|
|
|
|
|
esp_log_level_set("wifi", ESP_LOG_WARN);
|
|
|
|
|
|
esp_log_level_set("wifi_init", ESP_LOG_WARN);
|
|
|
|
|
|
esp_log_level_set("pp", ESP_LOG_WARN);
|
|
|
|
|
|
esp_log_level_set("net80211", ESP_LOG_WARN);
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
|
|
|
|
|
ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
|
|
|
|
|
|
DEBUG_INFO("WIFI", "WiFi driver 初始化成功");
|
|
|
|
|
|
|
|
|
|
|
|
// STA 优先,失败时返回 false 让 task 切 AP
|
|
|
|
|
|
if (CONFIG_WIFI_MODE == 1) {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "工作模式: STA (优先)");
|
2026-05-31 03:20:26 +08:00
|
|
|
|
err = initSta();
|
2026-05-05 03:28:44 +08:00
|
|
|
|
if (err == ESP_OK) {
|
|
|
|
|
|
// 连接已触发(STA_START 事件驱动),返回 true
|
2026-05-31 03:20:26 +08:00
|
|
|
|
// 实际连接状态由 WifiOta::taskEntry 等待确认
|
2026-05-05 03:28:44 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
DEBUG_WARN("WIFI", "STA 初始化失败,将切换 AP");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "工作模式: AP");
|
2026-05-31 03:20:26 +08:00
|
|
|
|
initAp();
|
2026-05-05 03:28:44 +08:00
|
|
|
|
return false; // AP 模式,不需要等待连接
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 00:42:26 +08:00
|
|
|
|
// ----------------------------------------
|
|
|
|
|
|
// WiFi OTA FreeRTOS 任务
|
2026-05-31 03:20:26 +08:00
|
|
|
|
// WiFi 初始化已在 app_main 中通过 WifiOta::instance().earlyInit() 完成
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控
|
2026-05-05 04:48:31 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。
|
|
|
|
|
|
* @param pvParameters FreeRTOS 任务参数,未使用。
|
|
|
|
|
|
*/
|
2026-05-31 03:20:26 +08:00
|
|
|
|
void WifiOta::taskEntry(void *pvParameters)
|
2026-04-27 00:42:26 +08:00
|
|
|
|
{
|
|
|
|
|
|
(void)pvParameters;
|
2026-06-20 00:46:04 +08:00
|
|
|
|
WifiOta::instance().m_taskHandle = xTaskGetCurrentTaskHandle();
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-04-28 01:06:17 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// 等待 STA 连接结果(wifi_early_init 已触发连接)
|
2026-05-31 03:20:26 +08:00
|
|
|
|
if (WifiOta::getMode() == OTA_WIFI_MODE_STA) {
|
2026-04-27 00:42:26 +08:00
|
|
|
|
char ip_str[16] = {0};
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_err_t err = instance().waitStaConnected(ip_str, sizeof(ip_str));
|
2026-04-27 00:42:26 +08:00
|
|
|
|
if (err == ESP_OK) {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
|
2026-05-30 20:04:04 +08:00
|
|
|
|
// ★ 完整的 STA→AP 切换流程:
|
|
|
|
|
|
// 1. 停止 WiFi
|
|
|
|
|
|
// 2. 反初始化 WiFi 驱动(释放内部资源,包括 NVS 缓存)
|
|
|
|
|
|
// 3. 重新初始化 WiFi 驱动
|
|
|
|
|
|
// 4. 重新注册事件回调
|
|
|
|
|
|
// 5. 以 AP 模式启动
|
|
|
|
|
|
esp_wifi_stop();
|
|
|
|
|
|
esp_err_t deinit_err = esp_wifi_deinit();
|
|
|
|
|
|
if (deinit_err != ESP_OK) {
|
|
|
|
|
|
DEBUG_WARN("WIFI", "WiFi deinit 失败: %s,仅 stop 后切 AP", esp_err_to_name(deinit_err));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "WiFi 驱动已反初始化,重新初始化...");
|
|
|
|
|
|
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
|
|
|
|
|
deinit_err = esp_wifi_init(&wifi_cfg);
|
|
|
|
|
|
if (deinit_err != ESP_OK) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "WiFi 驱动重新初始化失败: %s", esp_err_to_name(deinit_err));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// ★ deinit 后事件回调会丢失,必须重新注册
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL);
|
|
|
|
|
|
esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL);
|
2026-05-30 20:04:04 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "WiFi/IP 事件回调已重新注册");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-31 03:20:26 +08:00
|
|
|
|
WifiOta::instance().m_wifiMode = OTA_WIFI_MODE_AP;
|
|
|
|
|
|
instance().initAp();
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// AP 模式:wifi_early_init 已完成,无需额外操作
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-05-31 03:20:26 +08:00
|
|
|
|
esp_err_t err = instance().httpServerStart();
|
2026-04-27 00:42:26 +08:00
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
|
DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 19:14:13 +08:00
|
|
|
|
// ★ 开机清理:如果 NVS 中有不完整的 OTA 元数据(上次 OTA 中断),
|
|
|
|
|
|
// GD32 可能还卡在 Bootloader 中,导致 SPI 全面超时 → HTTP socket 饥荒。
|
|
|
|
|
|
// 主动清除残留元数据并尝试恢复 GD32 APP。
|
|
|
|
|
|
{
|
|
|
|
|
|
ota_meta_t boot_meta;
|
|
|
|
|
|
if (OtaCoordinator::instance().loadGd32Meta(&boot_meta) == ESP_OK &&
|
|
|
|
|
|
boot_meta.total_size > 0 && boot_meta.stored_size < boot_meta.total_size) {
|
2026-07-20 03:10:42 +08:00
|
|
|
|
DEBUG_WARN("WIFI", "不完整OTA缓存 stored=%u total=%u,清残留",
|
2026-07-06 19:14:13 +08:00
|
|
|
|
(unsigned)boot_meta.stored_size, (unsigned)boot_meta.total_size);
|
|
|
|
|
|
OtaCoordinator::instance().clearGd32Meta();
|
|
|
|
|
|
OtaCoordinator::instance().abortCurrent();
|
|
|
|
|
|
// 给 GD32 时间从 Bootloader 超时退出并重启到 APP
|
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(3000));
|
|
|
|
|
|
// 尝试探测 GD32 APP 是否恢复
|
|
|
|
|
|
gd32_version_t ver = {};
|
|
|
|
|
|
if (OtaCoordinator::instance().probeGd32AppOnce(&ver, nullptr) == ESP_OK) {
|
|
|
|
|
|
DEBUG_INFO("WIFI", "GD32 APP 已恢复: v%d.%d.%d.%d",
|
|
|
|
|
|
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DEBUG_WARN("WIFI", "GD32 APP 未恢复,SPI 链路可能不可用");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// ★ WiFi 已连接,UDP 从这里开始能通
|
|
|
|
|
|
// 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
|
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(500));
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-07-20 03:10:42 +08:00
|
|
|
|
// 启动摘要(单次;前面已 delay 等网络/ARP 稳定)
|
2026-05-05 03:28:44 +08:00
|
|
|
|
esp_chip_info_t chip;
|
|
|
|
|
|
esp_chip_info(&chip);
|
2026-07-20 03:10:42 +08:00
|
|
|
|
DEBUG_INFO("WIFI", "========================================");
|
|
|
|
|
|
DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
|
|
|
|
|
|
DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
|
|
|
|
|
|
DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
|
|
|
|
|
|
DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", WifiOta::getMode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
|
|
|
|
|
|
DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
|
|
|
|
|
|
DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
|
|
|
|
|
|
DEBUG_INFO("WIFI", "========================================");
|
|
|
|
|
|
DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
|
|
|
|
|
|
|
|
|
|
|
|
/* 启动日志 burst 已过:环 64→16 槽,释放大约 7.5KB 给业务堆 */
|
|
|
|
|
|
DebugLog::instance().shrinkRingAfterBoot();
|
2026-05-05 03:28:44 +08:00
|
|
|
|
|
2026-05-05 04:48:31 +08:00
|
|
|
|
// WiFi 状态监控
|
|
|
|
|
|
// 每秒一次循环:每 10 秒检查 RSSI
|
2026-05-05 03:28:44 +08:00
|
|
|
|
uint32_t tick = 0;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
while (1) {
|
2026-05-05 03:28:44 +08:00
|
|
|
|
tick++;
|
2026-04-27 00:42:26 +08:00
|
|
|
|
|
2026-06-16 23:17:06 +08:00
|
|
|
|
// ★ BLE OTA 传输期间,暂停 WiFi RSSI 轮询,避免 esp_wifi_sta_get_ap_info()
|
|
|
|
|
|
// 触发射频活动干扰 BLE 连接事件
|
|
|
|
|
|
if (WifiOta::instance().m_bleOtaActive.load()) {
|
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
// 每 10 秒检查一次 RSSI
|
|
|
|
|
|
if (tick % 10 == 0) {
|
2026-05-31 03:20:26 +08:00
|
|
|
|
int8_t rssi = WifiOta::getRssi();
|
2026-05-05 03:28:44 +08:00
|
|
|
|
if (rssi != -127) {
|
2026-05-31 03:20:26 +08:00
|
|
|
|
HealthMonitor::instance().updateFromWifiRssi(rssi);
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 03:28:44 +08:00
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
2026-04-27 00:42:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|