feat(runtime): add diagnostic and network recovery controls

This commit is contained in:
chenjintao
2026-08-12 16:32:40 +08:00
parent a94df9545d
commit 1c32d9f0e7
11 changed files with 868 additions and 0 deletions
@@ -138,6 +138,11 @@ esp_err_t platform_wifi_portal_connect_sta(int timeout_ms)
return wifi_portal_connect_sta(timeout_ms);
}
esp_err_t platform_wifi_portal_reconnect_sta(int timeout_ms)
{
return wifi_portal_reconnect_sta(timeout_ms);
}
esp_err_t platform_wifi_portal_save_credentials(const char *ssid, const char *password)
{
return wifi_portal_save_credentials(ssid, password);
@@ -18,12 +18,355 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifndef CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
#define CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE 0
#endif
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include "esp_heap_caps.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/semphr.h"
#include "ping/ping_sock.h"
#endif
#define PLATFORM_HTTPS_DEFAULT_TIMEOUT_MS 10000
#define PLATFORM_HTTPS_DNS_RETRY_COUNT 2
#define PLATFORM_HTTPS_DNS_RETRY_DELAY_MS 500
#define PLATFORM_HTTPS_HOST_MAX_LEN 256
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
#define PLATFORM_HTTPS_PROBE_PING_TIMEOUT_MS 400
#define PLATFORM_HTTPS_PROBE_TCP_TIMEOUT_MS 800
#endif
static const char *TAG = "platform_https";
static const char *dns_error_name(int err);
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
static volatile uint32_t s_dns_lookup_count;
static volatile uint32_t s_dns_lookup_failure_count;
static volatile uint32_t s_dns_lookup_active;
static struct sockaddr_storage s_last_resolved_addr;
static socklen_t s_last_resolved_addr_len;
static char s_last_resolved_host[PLATFORM_HTTPS_HOST_MAX_LEN];
static portMUX_TYPE s_probe_cache_lock = portMUX_INITIALIZER_UNLOCKED;
typedef struct {
SemaphoreHandle_t done;
volatile uint32_t replies;
} https_ping_probe_context_t;
static void https_ping_success(esp_ping_handle_t handle, void *args)
{
(void)handle;
https_ping_probe_context_t *context = (https_ping_probe_context_t *)args;
if (context != NULL) {
context->replies++;
}
}
static void https_ping_end(esp_ping_handle_t handle, void *args)
{
(void)handle;
https_ping_probe_context_t *context = (https_ping_probe_context_t *)args;
if (context != NULL && context->done != NULL) {
(void)xSemaphoreGive(context->done);
}
}
static void run_ipv4_ping_probe(const char *label, const esp_ip4_addr_t *target)
{
if (target == NULL || target->addr == 0U) {
ESP_LOGW(TAG, "Network probe ping label=%s skipped: no IPv4 target", label);
return;
}
https_ping_probe_context_t context = {
.done = xSemaphoreCreateBinary(),
};
if (context.done == NULL) {
ESP_LOGW(TAG, "Network probe ping label=%s failed: no semaphore memory", label);
return;
}
esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG();
config.count = 1;
config.interval_ms = 0;
config.timeout_ms = PLATFORM_HTTPS_PROBE_PING_TIMEOUT_MS;
ip_addr_set_ip4_u32(&config.target_addr, target->addr);
esp_ping_callbacks_t callbacks = {
.cb_args = &context,
.on_ping_success = https_ping_success,
.on_ping_timeout = NULL,
.on_ping_end = https_ping_end,
};
esp_ping_handle_t session = NULL;
int64_t started_us = esp_timer_get_time();
esp_err_t err = esp_ping_new_session(&config, &callbacks, &session);
if (err == ESP_OK) {
err = esp_ping_start(session);
}
if (err == ESP_OK) {
TickType_t wait_ticks = pdMS_TO_TICKS(PLATFORM_HTTPS_PROBE_PING_TIMEOUT_MS + 300);
if (wait_ticks == 0) {
wait_ticks = 1;
}
if (xSemaphoreTake(context.done, wait_ticks) != pdTRUE) {
err = ESP_ERR_TIMEOUT;
}
}
if (session != NULL) {
(void)esp_ping_stop(session);
(void)esp_ping_delete_session(session);
}
ESP_LOGW(TAG,
"Network probe ping label=%s target=" IPSTR " result=%s replies=%u elapsed_ms=%lld",
label,
IP2STR(target),
err == ESP_OK && context.replies > 0U ? "reachable" :
(err == ESP_OK ? "no_reply" : esp_err_to_name(err)),
(unsigned)context.replies,
(long long)((esp_timer_get_time() - started_us) / 1000));
vSemaphoreDelete(context.done);
}
static void cache_resolved_endpoint(const char *host, const struct addrinfo *addr)
{
const struct addrinfo *selected = addr;
while (selected != NULL &&
selected->ai_family != AF_INET &&
selected->ai_family != AF_INET6) {
selected = selected->ai_next;
}
if (host == NULL || selected == NULL || selected->ai_addr == NULL ||
selected->ai_addrlen > sizeof(s_last_resolved_addr)) {
return;
}
struct sockaddr_storage endpoint = {0};
memcpy(&endpoint, selected->ai_addr, selected->ai_addrlen);
if (selected->ai_family == AF_INET) {
((struct sockaddr_in *)&endpoint)->sin_port = htons(443);
} else {
((struct sockaddr_in6 *)&endpoint)->sin6_port = htons(443);
}
portENTER_CRITICAL(&s_probe_cache_lock);
s_last_resolved_addr = endpoint;
s_last_resolved_addr_len = selected->ai_addrlen;
strlcpy(s_last_resolved_host, host, sizeof(s_last_resolved_host));
portEXIT_CRITICAL(&s_probe_cache_lock);
}
static void run_cached_endpoint_probe(void)
{
struct sockaddr_storage endpoint = {0};
socklen_t endpoint_len = 0;
char host[PLATFORM_HTTPS_HOST_MAX_LEN] = {0};
portENTER_CRITICAL(&s_probe_cache_lock);
endpoint = s_last_resolved_addr;
endpoint_len = s_last_resolved_addr_len;
strlcpy(host, s_last_resolved_host, sizeof(host));
portEXIT_CRITICAL(&s_probe_cache_lock);
if (endpoint_len == 0 || host[0] == '\0') {
ESP_LOGW(TAG, "Network probe cached TCP skipped: no successful DNS result cached");
return;
}
char address[64] = {0};
void *address_ptr = endpoint.ss_family == AF_INET ?
(void *)&((struct sockaddr_in *)&endpoint)->sin_addr :
(void *)&((struct sockaddr_in6 *)&endpoint)->sin6_addr;
const char *formatted = inet_ntop(endpoint.ss_family,
address_ptr,
address,
sizeof(address));
int64_t started_us = esp_timer_get_time();
int sock = socket(endpoint.ss_family, SOCK_STREAM, IPPROTO_TCP);
int socket_error = 0;
bool connected = false;
if (sock >= 0) {
int flags = fcntl(sock, F_GETFL, 0);
if (flags >= 0 && fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0) {
int connect_result = connect(sock,
(const struct sockaddr *)&endpoint,
endpoint_len);
if (connect_result == 0) {
connected = true;
} else if (errno == EINPROGRESS) {
fd_set write_set;
FD_ZERO(&write_set);
FD_SET(sock, &write_set);
struct timeval timeout = {
.tv_sec = 0,
.tv_usec = PLATFORM_HTTPS_PROBE_TCP_TIMEOUT_MS * 1000,
};
int selected = select(sock + 1, NULL, &write_set, NULL, &timeout);
socklen_t error_len = sizeof(socket_error);
if (selected > 0 &&
getsockopt(sock,
SOL_SOCKET,
SO_ERROR,
&socket_error,
&error_len) == 0 &&
socket_error == 0) {
connected = true;
} else if (selected == 0) {
socket_error = ETIMEDOUT;
} else if (socket_error == 0) {
socket_error = errno;
}
} else {
socket_error = errno;
}
} else {
socket_error = errno;
}
close(sock);
} else {
socket_error = errno;
}
ESP_LOGW(TAG,
"Network probe cached TCP host=%s address=%s:443 result=%s socket_error=%d elapsed_ms=%lld",
host,
formatted != NULL ? address : "unknown",
connected ? "reachable" : "failed",
socket_error,
(long long)((esp_timer_get_time() - started_us) / 1000));
}
static void run_dns_failure_network_probes(const esp_netif_ip_info_t *ip,
const esp_netif_dns_info_t *fallback_dns)
{
ESP_LOGW(TAG, "Network probe bundle started after DNS failure");
run_ipv4_ping_probe("gateway", ip != NULL ? &ip->gw : NULL);
if (fallback_dns != NULL && fallback_dns->ip.type == ESP_IPADDR_TYPE_V4) {
run_ipv4_ping_probe("fallback_dns", &fallback_dns->ip.u_addr.ip4);
} else {
ESP_LOGW(TAG, "Network probe fallback DNS ping skipped: no IPv4 address");
}
run_cached_endpoint_probe();
ESP_LOGW(TAG, "Network probe bundle completed");
}
static const char *dhcp_status_name(esp_netif_dhcp_status_t status)
{
switch (status) {
case ESP_NETIF_DHCP_INIT:
return "init";
case ESP_NETIF_DHCP_STARTED:
return "started";
case ESP_NETIF_DHCP_STOPPED:
return "stopped";
default:
return "unknown";
}
}
static const char *format_dns_address(const esp_netif_dns_info_t *dns,
char *buffer,
size_t buffer_size)
{
if (dns == NULL || buffer == NULL || buffer_size == 0U) {
return "invalid";
}
if (dns->ip.type == ESP_IPADDR_TYPE_V6) {
int written = snprintf(buffer,
buffer_size,
IPV6STR,
IPV62STR(dns->ip.u_addr.ip6));
return written >= 0 && (size_t)written < buffer_size ? buffer : "unknown";
}
int written = snprintf(buffer,
buffer_size,
IPSTR,
IP2STR(&dns->ip.u_addr.ip4));
return written >= 0 && (size_t)written < buffer_size ? buffer : "unknown";
}
static void log_dns_failure_diagnostics(const char *host,
int attempt,
int dns_err,
uint32_t lookup_id,
uint32_t elapsed_ms)
{
esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
esp_netif_ip_info_t ip = {0};
esp_netif_dns_info_t dns_main = {0};
esp_netif_dns_info_t dns_backup = {0};
esp_netif_dns_info_t dns_fallback = {0};
esp_netif_dhcp_status_t dhcp_status = ESP_NETIF_DHCP_INIT;
esp_err_t ip_err = sta != NULL ? esp_netif_get_ip_info(sta, &ip) : ESP_ERR_NOT_FOUND;
esp_err_t main_err = esp_netif_get_dns_info(sta, ESP_NETIF_DNS_MAIN, &dns_main);
esp_err_t backup_err = esp_netif_get_dns_info(sta, ESP_NETIF_DNS_BACKUP, &dns_backup);
esp_err_t fallback_err = esp_netif_get_dns_info(sta,
ESP_NETIF_DNS_FALLBACK,
&dns_fallback);
esp_err_t dhcp_err = sta != NULL ? esp_netif_dhcpc_get_status(sta, &dhcp_status) :
ESP_ERR_NOT_FOUND;
wifi_ap_record_t ap = {0};
esp_err_t ap_err = esp_wifi_sta_get_ap_info(&ap);
char main_buffer[48] = {0};
char backup_buffer[48] = {0};
char fallback_buffer[48] = {0};
const char *main_address = main_err == ESP_OK ?
format_dns_address(&dns_main, main_buffer, sizeof(main_buffer)) : "unavailable";
const char *backup_address = backup_err == ESP_OK ?
format_dns_address(&dns_backup, backup_buffer, sizeof(backup_buffer)) : "unavailable";
const char *fallback_address = fallback_err == ESP_OK ?
format_dns_address(&dns_fallback, fallback_buffer, sizeof(fallback_buffer)) :
"unavailable";
ESP_LOGW(TAG,
"DNS diagnostics lookup=%u attempt=%d elapsed_ms=%u host=%s err=%s(%d) active=%u failures=%u sta=%d ip=" IPSTR " netmask=" IPSTR " gw=" IPSTR " dhcp=%d/%s dns(main=%s backup=%s fallback=%s status=%s/%s/%s) rssi=%d ap=%s heap(internal=%u largest=%u dma_largest=%u)",
(unsigned)lookup_id,
attempt,
(unsigned)elapsed_ms,
host != NULL ? host : "-",
dns_error_name(dns_err),
dns_err,
(unsigned)__atomic_load_n(&s_dns_lookup_active, __ATOMIC_RELAXED),
(unsigned)__atomic_load_n(&s_dns_lookup_failure_count, __ATOMIC_RELAXED),
sta != NULL ? 1 : 0,
IP2STR(&ip.ip),
IP2STR(&ip.netmask),
IP2STR(&ip.gw),
dhcp_err == ESP_OK ? (int)dhcp_status : -1,
dhcp_err == ESP_OK ? dhcp_status_name(dhcp_status) : esp_err_to_name(dhcp_err),
main_address,
backup_address,
fallback_address,
esp_err_to_name(main_err),
esp_err_to_name(backup_err),
esp_err_to_name(fallback_err),
ap_err == ESP_OK ? (int)ap.rssi : 0,
esp_err_to_name(ap_err),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_DMA | MALLOC_CAP_8BIT));
if (ip_err != ESP_OK) {
ESP_LOGW(TAG,
"DNS diagnostics STA IP query failed: %s",
esp_err_to_name(ip_err));
}
if (attempt == 1) {
run_dns_failure_network_probes(ip_err == ESP_OK ? &ip : NULL,
fallback_err == ESP_OK ? &dns_fallback : NULL);
}
}
#endif
static int effective_positive_or_zero(int value)
{
@@ -106,23 +449,54 @@ static esp_err_t resolve_host_before_open(const char *url)
return err;
}
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
uint32_t lookup_id = __atomic_add_fetch(&s_dns_lookup_count, 1U, __ATOMIC_RELAXED);
(void)__atomic_add_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED);
#endif
for (int attempt = 1; attempt <= PLATFORM_HTTPS_DNS_RETRY_COUNT; ++attempt) {
struct addrinfo hints = {
.ai_family = AF_UNSPEC,
.ai_socktype = SOCK_STREAM,
};
struct addrinfo *addr = NULL;
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
int64_t attempt_started_us = esp_timer_get_time();
#endif
int dns_err = getaddrinfo(host, NULL, &hints, &addr);
if (dns_err == 0 && addr != NULL) {
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
uint32_t elapsed_ms = (uint32_t)((esp_timer_get_time() - attempt_started_us) / 1000);
cache_resolved_endpoint(host, addr);
if (__atomic_load_n(&s_dns_lookup_failure_count, __ATOMIC_RELAXED) > 0U ||
elapsed_ms >= 1000U) {
ESP_LOGI(TAG,
"DNS resolved lookup=%u attempt=%d elapsed_ms=%u host=%s",
(unsigned)lookup_id,
attempt,
(unsigned)elapsed_ms,
host);
}
#endif
freeaddrinfo(addr);
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
(void)__atomic_sub_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED);
#endif
return ESP_OK;
}
if (addr != NULL) {
freeaddrinfo(addr);
}
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
(void)__atomic_add_fetch(&s_dns_lookup_failure_count, 1U, __ATOMIC_RELAXED);
uint32_t elapsed_ms = (uint32_t)((esp_timer_get_time() - attempt_started_us) / 1000);
log_dns_failure_diagnostics(host, attempt, dns_err, lookup_id, elapsed_ms);
#endif
if (dns_err == EAI_MEMORY) {
ESP_LOGW(TAG, "DNS resolve failed host=%s err=%s(%d)", host, dns_error_name(dns_err), dns_err);
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
(void)__atomic_sub_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED);
#endif
return ESP_ERR_NO_MEM;
}
@@ -143,6 +517,9 @@ static esp_err_t resolve_host_before_open(const char *url)
}
}
#if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE
(void)__atomic_sub_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED);
#endif
return ESP_ERR_HTTP_CONNECT;
}
@@ -291,3 +668,8 @@ esp_err_t platform_https_post_json(const platform_https_post_json_request_t *req
response->body_len = total_read;
return ESP_OK;
}
bool platform_https_is_connect_error(esp_err_t err)
{
return err == ESP_ERR_HTTP_CONNECT;
}
@@ -343,6 +343,65 @@ esp_err_t wifi_portal_connect_sta(int timeout_ms)
return ESP_ERR_TIMEOUT;
}
esp_err_t wifi_portal_reconnect_sta(int timeout_ms)
{
int effective_timeout_ms = normalize_connect_timeout_ms(timeout_ms);
int64_t deadline_us = esp_timer_get_time() + (int64_t)effective_timeout_ms * 1000;
{
ESP_RETURN_ON_ERROR(wifi_portal_lock(), TAG, "Failed to lock Wi-Fi portal");
PortalUnlockGuard guard;
if (!wifi_portal_has_saved_credentials()) {
return ESP_ERR_NOT_FOUND;
}
ESP_RETURN_ON_ERROR(ensure_wifi_manager_initialized(),
TAG,
"Failed to initialize Wi-Fi manager");
ESP_LOGW(TAG,
"Controlled STA reassociation started timeout_ms=%d connected=%d",
effective_timeout_ms,
WifiManager::GetInstance().IsConnected() ? 1 : 0);
esp_err_t disconnect_err = esp_wifi_disconnect();
if (disconnect_err != ESP_OK && disconnect_err != ESP_ERR_WIFI_NOT_CONNECT) {
ESP_LOGW(TAG,
"Controlled STA disconnect returned %s",
esp_err_to_name(disconnect_err));
}
}
int64_t disconnect_deadline_us = esp_timer_get_time() + 2000LL * 1000LL;
if (disconnect_deadline_us > deadline_us) {
disconnect_deadline_us = deadline_us;
}
while (esp_timer_get_time() < disconnect_deadline_us &&
WifiManager::GetInstance().IsConnected()) {
vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS));
}
esp_err_t connect_err = esp_wifi_connect();
if (connect_err != ESP_OK) {
ESP_LOGI(TAG,
"Controlled STA reconnect command returned %s; wait for manager auto-reconnect",
esp_err_to_name(connect_err));
}
while (esp_timer_get_time() < deadline_us) {
if (WifiManager::GetInstance().IsConnected()) {
(void)apply_sta_stability_policy();
ESP_LOGI(TAG, "Controlled STA reassociation completed");
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(WIFI_PORTAL_CONNECT_POLL_MS));
}
ESP_LOGW(TAG,
"Controlled STA reassociation timed out after %d ms",
effective_timeout_ms);
return ESP_ERR_TIMEOUT;
}
esp_err_t wifi_portal_save_credentials(const char *ssid, const char *password)
{
install_wifi_storage_hooks();