Files
ESP32_COMM/main/monitor/health_monitor.cpp
T
小龙虾 f9cbcc396b feat(spi): 增强 SPI master:性能剖析与执行耗时跟踪
- 新增 `lastWorkerExecUs()`:获取最近一次 worker 任务执行耗时(µs)。
- 引入 `SpiProfile` 结构体:事务级详细剖析,记录 SPI 事务各阶段的时间戳。
- 实现 `lastProfile()`:诊断用,获取最近一次事务剖析快照。
- SPI 时钟配置提升至 13.3 MHz,改善性能。
- 修改前端 JavaScript 轮询逻辑:新增 uptime 检查,更好地处理 ESP32 重启状态。
- SPI 测速 HTML 的计时单位由毫秒改为微秒显示。
- 新建 `spi_ring_cache.h`:共享 SPI ring push 缓存工具。
- 新增 `http_util.h`:共享 HTTP 处理器工具,含 JSON 拼接与查询参数解析。
- 引入 `json_parse.h`:轻量级 JSON 解析器,针对扁平 JSON 对象设计。
- 新增 Windows 环境下的项目构建批处理脚本。
- 重构 CAN 反馈处理:从推送式改为请求驱动架构,降低内存占用、提升效率。
2026-08-02 17:37:19 +08:00

885 lines
35 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* health_monitor.cpp - 系统健康监控
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
*
* 所有功能封装为 HealthMonitor 类方法。
* 所有对 m_healthState / m_lastTemperature / m_lastWifiRssi 的访问
* 均通过 m_mutex 保护,消除跨任务竞态。
*/
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "driver/temperature_sensor.h"
#include "health_monitor.h"
#include "debug_log.h"
#include "task_stack.h"
#include "ota/ota_coordinator.h"
#include "ble_ota.h"
#include "wifi_ota.h"
#include "spi_master.h"
#include "esp_event.h"
#include "esp_netif_types.h"
#include "server/http_util.h"
// ============================================================
// heap hook:捕获"破新低时的最后一次 alloc"(最后一击,未必是主因)
// ============================================================
// CONFIG_HEAP_USE_HOOKS=y 时,每次 malloc 成功后进入本弱符号。
//
// ★ 为何不能记任务名(用户看到"每次都是 WiFi"的根因):
// 1) WiFi IRAM 路径禁止在 hook 里调 FreeRTOS/pcTaskGetName(会搞挂 STA)。
// 2) 即便能取任务名:HTTP/UDP **发送前** lwip 仍会为 pbuf/TX 再 malloc,
// 芯片 minimum free 往往在这条"最后一击"上被压低,任务名就会是 wifi/tiT/httpd,
// 看起来像"WiFi 吃堆",实际只是发送路径的末包分配。
// 因此 hook **永不写任务名**;只记 size + caller 地址(addr2line)。
// 前端诊断应标明「最后一击 / 可能网络发送」,勿把连接列的 WiFi 当成触发任务。
//
// ★ 记录策略:health_mon 周期把真实 free 同步进 g_est_free;
// hook 内仅做 g_est_free -= size(C3 单核,无锁可接受漂移)。
// 当 est 创新低时更新 hook 快照。free hook 不知 size,靠周期 resync 纠偏。
//
// ★ 采样协议(消除多字段撕裂读):
// hook 写「最后一击」快照的多个字段(min_est/alloc_size/caller/free_after/fired
// + recent_allocs[])虽各自是字宽 volatile,但 reader 跨字段组合可撕裂
// (fired=1 配上上一次的 alloc_size)。reader(maybeRecordHeapLowWater)在
// portENTER_CRITICAL(&s_hookMux) 内一次性拷出全部字段——单核禁中断即与 hook
// (在 heap 临界区内写)互斥。m_mutex 不参与此互斥(hook 不获取它)。
// reader 临界区仅做 volatile 拷贝,不分配、不调 FreeRTOS/flash,保持极短。
namespace {
/** 最近若干次「值得关注」的 alloc(size≥阈值),供破新低时对照。 */
struct RecentAllocSnap {
uint32_t size;
uint32_t caller;
};
static constexpr uint32_t kHookInterestingMin = 48; ///< 忽略极小碎片分配
static constexpr size_t kRecentAllocCap = 4;
volatile uint32_t g_hook_min_est = 0xFFFFFFFFu; ///< hook 见过的最低 est free
volatile uint32_t g_hook_alloc_size = 0; ///< 触发 est 新低的那次 alloc 大小
volatile uint32_t g_hook_caller_addr = 0; ///< 调用者返回地址
volatile uint32_t g_hook_free_after = 0; ///< 那次后的 est free(estimated,非精确实测)
volatile uint32_t g_hook_fired = 0;
volatile uint32_t g_hook_armed = 0;
volatile uint32_t g_est_free = 0; ///< estimated 近似空闲(只减不增),health_mon 周期用 esp_get_free_heap_size() 实测 resync 纠偏;误差上界 ≈ resync 周期(5s)内总 alloc 字节
volatile uint32_t g_ip_event_count = 0;
RecentAllocSnap g_recent_allocs[kRecentAllocCap] = {};
volatile uint32_t g_recent_alloc_w = 0;
volatile uint32_t g_recent_alloc_n = 0;
/* s_hookMux:reader(maybeRecordHeapLowWater,任务上下文)取多字段一致快照用的
* portMUX(单核 = 禁中断)。hook 端不加锁——它在 heap 临界区内写,与 reader 的
* 禁中断互斥。仅用于消除「最后一击」快照多字段撕裂读,不保护 m_heapLowWater
* (后者由 m_mutex 保护)。 */
static portMUX_TYPE s_hookMux = portMUX_INITIALIZER_UNLOCKED;
void hookResyncEstFree(uint32_t real_free)
{
g_est_free = real_free;
}
/** @brief reader 一次性拷出的 hook 共享状态一致快照(任务上下文,禁中断极短临界区)。 */
struct HookSnap {
uint32_t alloc_size;
uint32_t caller_addr;
uint32_t free_after; ///< estimated(非实测)
uint32_t fired;
uint32_t est_free;
uint32_t min_est;
uint32_t recent_n;
RecentAllocSnap recent[kRecentAllocCap];
};
/** @brief 任务上下文用:禁中断拷出 hook 共享状态一致快照。仅 volatile 拷贝,不分配/不调 FreeRTOS。 */
HookSnap captureHookSnapshot()
{
HookSnap s = {};
portENTER_CRITICAL(&s_hookMux);
s.alloc_size = g_hook_alloc_size;
s.caller_addr = g_hook_caller_addr;
s.free_after = g_hook_free_after;
s.fired = g_hook_fired;
s.est_free = g_est_free;
s.min_est = g_hook_min_est;
s.recent_n = g_recent_alloc_n;
for (uint32_t i = 0; i < s.recent_n && i < (uint32_t)kRecentAllocCap; ++i) {
s.recent[i] = g_recent_allocs[i];
}
portEXIT_CRITICAL(&s_hookMux);
return s;
}
/** @brief 在 s_hookMux 临界区内调用:消费已记录的 hook 命中,重置 min_est/fired。
* 必须由 reader 在写完 HeapLowWaterRecord 后调用,避免同一窗口重复记。 */
void consumeHookFiredInLock()
{
/* min_est 抬到当前 est:本次已记下低谷,下次需更低才会再次触发。
* g_est_free 在此读以与 hook 写一致(s_hookMux 内)。 */
g_hook_min_est = g_est_free;
g_hook_fired = 0;
}
} // namespace
// ESP-IDF heap alloc hook:仅 DRAM 标量 + 内联 return address,禁止 FreeRTOS/flash 长函数。
//
// ★ hook 可调用约束(IRAM 安全契约):
// - 禁止:FreeRTOS API、flash/rodata 访问、mutex/semaphore、长函数。
// - 允许:DRAM volatile 标量读写、__builtin_return_address、纯算术。
// - 写共享状态天然原子:hook 已在 heap 临界区内(禁中断),reader 用 s_hookMux
// 禁中断取一致快照,二者互斥;hook 端不加锁、保持 IRAM 安全。
// - 嵌套分配:hook 内禁止任何可能触发 malloc 的调用(否则递归进 hook,armed
// 标志不防递归——当前 hook 体内无分配调用,保持之)。
extern "C"
void esp_heap_trace_alloc_hook(void *ptr, size_t size, uint32_t caps)
{
(void)caps;
if (!ptr || size == 0 || g_hook_armed == 0) {
return;
}
const uint32_t sz = (uint32_t)size;
/* 近似空闲:只减不增(free 路径不在此修正),health_mon 周期 resync。 */
uint32_t est = g_est_free;
if (est >= sz) {
est -= sz;
} else {
est = 0;
}
g_est_free = est;
/* 环形记下近期「有意义」分配,破新低时可读到发送路径小包之前的大块。 */
if (sz >= kHookInterestingMin) {
const uint32_t w = g_recent_alloc_w % (uint32_t)kRecentAllocCap;
g_recent_allocs[w].size = sz;
g_recent_allocs[w].caller = (uint32_t)(uintptr_t)__builtin_return_address(0);
g_recent_alloc_w = w + 1U;
if (g_recent_alloc_n < (uint32_t)kRecentAllocCap) {
g_recent_alloc_n = g_recent_alloc_n + 1U;
}
}
/* est 创新低 → 记「最后一击」alloc(常为网络 pbuf,任务若可读会是 wifi) */
if (est < g_hook_min_est) {
g_hook_min_est = est;
g_hook_alloc_size = sz;
g_hook_caller_addr = (uint32_t)(uintptr_t)__builtin_return_address(0);
g_hook_free_after = est;
g_hook_fired = 1;
/* 不采任务名(WiFi IRAM 路径禁止 FreeRTOS 调用) */
}
}
// IP 事件 handler:STA 获得 IP(含 DHCP 续约)时计数 +1
void ipEventHandler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
(void)arg; (void)event_base; (void)event_id; (void)event_data;
if (event_base == IP_EVENT && (event_id == IP_EVENT_STA_GOT_IP || event_id == IP_EVENT_ETH_GOT_IP)) {
uint32_t cur = g_ip_event_count;
g_ip_event_count = cur + 1;
}
}
// ============================================================
// 栈水位主动巡检
// ============================================================
// 告警阈值:剩余高水位 < 栈大小 20% 即告警(复用 /system/status 的任务表)。
// 去重:每个任务只在"水位由安全跌入危险"时告警一次,恢复后再跌入才再次告警,
// 避免 5 秒巡检刷屏。health_mon 任务从不被 OTA 暂停,DEBUG_ERR 经 UDP/UART
// 直发,不受 OTA 门控抑制。
static constexpr float STACK_WARN_RATIO = 0.20f; ///< 剩余水位告警阈值(栈大小比例)
static constexpr size_t STACK_TASK_MAX = 12; ///< 受监控任务上限(含 spi_worker 等)
static bool s_stackWarned[STACK_TASK_MAX] = {}; ///< 各任务是否已告警(去重)
// ============================================================
// 单例
// ============================================================
/**
* @brief 获取 HealthMonitor 单例实例。
* @return HealthMonitor 引用。
*/
HealthMonitor& HealthMonitor::instance()
{
static HealthMonitor inst;
return inst;
}
// ============================================================
// 初始化
// ============================================================
/**
* @brief 初始化健康监控模块,创建内部互斥锁与基础状态。
*/
void HealthMonitor::init()
{
m_mutex = xSemaphoreCreateMutex();
if (m_mutex == NULL) {
DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
}
// 注册 IP 事件 handler,统计 STA 获得 IP(含 DHCP 续约)次数。
// 配合 heap_low_log 快照的 ip_event_count 字段,可验证"低谷是不是 DHCP 续约引起"。
// 失败仅告警,不致命(计数器保持 0,快照里 ip_event_count 永远 0)。
esp_err_t ev = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ipEventHandler, nullptr);
if (ev != ESP_OK) {
DEBUG_WARN("HEALTH", "IP 事件 handler 注册失败: %s", esp_err_to_name(ev));
}
// 武装 heap alloc hook:init() 在 app_main 之后调用。armed=0 时 hook 直接 return。
// hook 本身禁止任何 FreeRTOS/flash 调用(见 esp_heap_trace_alloc_hook 注释)。
g_hook_armed = 1;
}
// ============================================================
// 线程安全的健康状态读写
// ============================================================
/**
* @brief 加锁读取当前健康状态。
* @return 当前健康状态。
*/
health_state_t HealthMonitor::getState()
{
LockGuard lock(m_mutex);
return m_healthState;
}
/**
* @brief 加锁设置健康状态。
* @param newState 新的健康状态。
*/
void HealthMonitor::setState(health_state_t newState)
{
LockGuard lock(m_mutex);
m_healthState = newState;
}
// ============================================================
// 获取当前健康状态(public 接口)
// ============================================================
/**
* @brief 获取当前系统健康状态(线程安全)。
* @return 当前健康状态。
*/
health_state_t HealthMonitor::getCurrent()
{
return getState();
}
/**
* @brief 获取最近一次芯片温度。
* @return 温度值(摄氏度)。
*/
float HealthMonitor::getLastTemperature()
{
LockGuard lock(m_mutex);
return m_lastTemperature;
}
/**
* @brief 获取最近一次 WiFi RSSI 及其年龄。
* @param out_rssi 输出参数,返回 RSSI 值。
* @param out_age_ms 输出参数,返回样本年龄(毫秒)。
* @return 是否有有效数据。
*/
bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
{
int64_t lastMs;
int8_t rssi;
{
LockGuard lock(m_mutex);
lastMs = m_lastWifiRssiMs;
rssi = m_lastWifiRssi;
}
if (lastMs <= 0) {
return false;
}
int64_t now_ms = esp_timer_get_time() / 1000;
uint32_t age_ms = (now_ms > lastMs) ? (uint32_t)(now_ms - lastMs) : 0;
if (out_rssi) *out_rssi = rssi;
if (out_age_ms) *out_age_ms = age_ms;
return true;
}
// ============================================================
// 温度监控(定时器回调)
// ============================================================
/**
* @brief 温度监控定时器回调函数。
* @param arg 用户参数(未使用)。
*/
void HealthMonitor::tempMonitorCallback(void *arg)
{
auto& self = instance();
float temp = 0.0f;
esp_err_t err = temperature_sensor_get_celsius(self.m_tempHandle, &temp);
if (err != ESP_OK) {
DEBUG_WARN("TEMP", "读取温度失败: %s", esp_err_to_name(err));
return;
}
{
LockGuard lock(self.m_mutex);
self.m_lastTemperature = temp;
}
if (temp >= TEMP_CRITICAL) {
self.setState(SYS_SAFE_SHUTDOWN);
DEBUG_ERR("TEMP", "过热 %.1fC>=%dC 安全关机", temp, TEMP_CRITICAL);
DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN");
} else if (temp >= TEMP_WARNING) {
health_state_t cur = self.getState();
if (cur < SYS_WARNING) {
self.setState(SYS_WARNING);
DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
}
}
}
// ============================================================
// FreeRTOS 任务入口
// ============================================================
/**
* @brief 健康监控 FreeRTOS 任务入口。
* @param pvParameters 任务参数。
*/
void HealthMonitor::taskEntry(void *pvParameters)
{
(void)pvParameters;
auto& self = instance();
self.m_taskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO("MON", "=== 健康监控任务启动 ===");
temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
esp_err_t err = temperature_sensor_install(&temp_config, &self.m_tempHandle);
if (err != ESP_OK) {
DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
} else {
ESP_ERROR_CHECK(temperature_sensor_enable(self.m_tempHandle));
}
// 定时器,每1秒读取一次温度
esp_timer_create_args_t timer_args = {};
timer_args.callback = &tempMonitorCallback;
timer_args.arg = nullptr;
timer_args.name = "temp_monitor";
timer_args.dispatch_method = ESP_TIMER_TASK;
if (esp_timer_create(&timer_args, &self.m_tempTimer) == ESP_OK) {
esp_timer_start_periodic(self.m_tempTimer, 1000000); // 1秒
} else {
self.m_tempTimer = nullptr;
DEBUG_ERR("MON", "创建温度监控定时器失败");
}
// 周期性巡检
while (1) {
// 栈溢出 canary 触发检测:vApplicationStackOverflowHook 置位后,
// 在 health_mon 完好栈上用 DEBUG_ERR 经 UDP/UART 发详细告警。
// 标志只在置位后告警一次,不自动清除(canary 触发即不可逆,需重启恢复)。
if (g_stackOverflowDetected) {
DEBUG_ERR("MON", "栈溢出(canary),检查任务栈");
// 告警一次后清标志,避免 5 秒刷屏;后续若再次触发会重新置位
g_stackOverflowDetected = false;
}
uint32_t free_heap = esp_get_free_heap_size();
/* 同步 hook 近似空闲:纠正 free 路径未记账造成的向下漂移。
* 必须在 maybeRecord 之前,否则 est 长期偏低会误触发 hook 快照。 */
hookResyncEstFree(free_heap);
// 任务栈指标:本周期只采集一次,堆低水位快照和栈水位巡检共用。
TaskStackMetric metrics[STACK_TASK_MAX];
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
// 堆低水位快照:仅在"最低空闲"创新低时记录一条上下文(5s 检测足够)
// 传入已采集的 metrics,避免 maybeRecordHeapLowWater 内部二次遍历任务表。
self.maybeRecordHeapLowWater(metrics, count);
if (free_heap < HEAP_WARNING_MIN) {
/* 堆告警节流:OTA 期间阈值更低、间隔更长 */
static TickType_t s_last_heap_warn = 0;
TickType_t now = xTaskGetTickCount();
const bool ota_busy = !OtaCoordinator::instance().isIdle();
const uint32_t interval_ms = ota_busy ? 15000U
: (free_heap < 8192U ? 10000U : 3000U);
const uint32_t warn_floor = ota_busy ? 4096U : (uint32_t)HEAP_WARNING_MIN;
if (free_heap < warn_floor &&
(now - s_last_heap_warn) >= pdMS_TO_TICKS(interval_ms)) {
s_last_heap_warn = now;
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
DEBUG_WARN("MON", "堆紧%s free=%lu min=%lu big=%lu",
ota_busy ? "(OTA中)" : "",
(unsigned long)free_heap,
(unsigned long)esp_get_minimum_free_heap_size(),
(unsigned long)largest_block);
}
}
health_state_t cur = self.getState();
if (cur >= SYS_CRITICAL) {
DEBUG_ERR("MON", "健康状态危险: %d", cur);
}
// 栈水位主动巡检:复用已采集的任务表,低水位时 DEBUG_ERR 主动告警。
// health_mon 任务不被 OTA 暂停,告警经 UDP/UART 直发,穿透 OTA 门控。
for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) {
if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) {
continue;
}
UBaseType_t hw = uxTaskGetStackHighWaterMark(metrics[i].handle);
size_t warn_bytes = (size_t)(metrics[i].stack_size_bytes * STACK_WARN_RATIO);
if (hw < (UBaseType_t)warn_bytes) {
if (!s_stackWarned[i]) {
DEBUG_ERR("MON", "栈低 task=%s left=%lu thr=%lu sz=%lu",
metrics[i].name,
(unsigned long)hw,
(unsigned long)warn_bytes,
(unsigned long)metrics[i].stack_size_bytes);
s_stackWarned[i] = true;
}
} else if (s_stackWarned[i]) {
// 水位恢复,清除告警标志,允许下次再次跌入时告警
s_stackWarned[i] = false;
DEBUG_INFO("MON", "栈水位恢复: 任务=%s 剩余=%luB",
metrics[i].name, (unsigned long)hw);
}
}
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
// ============================================================
// OTA 健康检查
// ============================================================
/**
* @brief OTA 传输前执行健康检查。
* @return 健康检查状态。
*/
health_status_t HealthMonitor::checkBeforeOta()
{
health_status_t status = HEALTH_MON_OK;
health_state_t cur = getState();
if (cur >= SYS_SAFE_SHUTDOWN) {
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输");
return HEALTH_MON_SHUTDOWN;
} else if (cur >= SYS_CRITICAL) {
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输");
return HEALTH_MON_CRITICAL;
} else if (cur >= SYS_WARNING) {
status = HEALTH_MON_WARNING;
DEBUG_DBG("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
}
bool wifi_connected;
{
LockGuard lock(m_mutex);
wifi_connected = m_wifiConnected;
}
if (!wifi_connected) {
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接");
return HEALTH_MON_CRITICAL;
}
int8_t last_rssi = -127;
uint32_t age_ms = 0;
if (getLastWifiRssi(&last_rssi, &age_ms)) {
if (age_ms <= OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS) {
if (last_rssi < OTA_WIFI_RSSI_CRITICAL) {
DEBUG_WARN("HEALTH", "OTA前 RSSI %d<%d age=%lums",
last_rssi, OTA_WIFI_RSSI_CRITICAL, (unsigned long)age_ms);
return HEALTH_MON_CRITICAL;
} else if (last_rssi < WIFI_RSSI_NORMAL) {
if (status < HEALTH_MON_WARNING) {
status = HEALTH_MON_WARNING;
}
DEBUG_WARN("HEALTH", "OTA前 RSSI %d<%d age=%lums",
last_rssi, WIFI_RSSI_NORMAL, (unsigned long)age_ms);
}
}
}
if (status == HEALTH_MON_OK) {
DEBUG_DBG("HEALTH", "OTA 前检查: 正常");
}
return status;
}
/**
* @brief OTA 中继转发阶段执行健康检查(跳过 WiFi 检查)。
* @return 健康检查状态。
*/
health_status_t HealthMonitor::checkBeforeOtaRelay()
{
health_status_t status = HEALTH_MON_OK;
health_state_t cur = getState();
if (cur >= SYS_SAFE_SHUTDOWN) {
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_SAFE_SHUTDOWN,禁止传输");
return HEALTH_MON_SHUTDOWN;
} else if (cur >= SYS_CRITICAL) {
/* SYS_CRITICAL 可能是由 WiFi RSSI 过低导致的,
* 但 SPI 中继阶段不依赖 WiFi,只检查温度/硬件等本地状态。
* 温度达到 TEMP_CRITICAL(100°C) 时 m_healthState 会被设为
* SYS_SAFE_SHUTDOWN,在上面的分支已经拦截。
* 所以 SYS_CRITICAL 在中继阶段只可能是 WiFi 导致的,允许继续。 */
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_CRITICAL (WiFi 相关),SPI 中继允许继续");
} else if (cur >= SYS_WARNING) {
status = HEALTH_MON_WARNING;
DEBUG_DBG("HEALTH", "OTA 中继检查: SYS_WARNING,继续传输");
}
/* 跳过 WiFi 连接状态和 RSSI 检查:
* SPI 中继阶段固件已存储在 ESP32 Flash 中,
* 不依赖 WiFi 连接,无需检查 WiFi 状态。 */
if (status == HEALTH_MON_OK) {
DEBUG_DBG("HEALTH", "OTA 中继检查: 正常");
}
return status;
}
// ============================================================
// WiFi 连接状态推送(由 WiFi 事件回调调用)
// ============================================================
/**
* @brief 由 WiFi 事件回调推送连接状态。
* @param connected true=已连接,false=断开。
*/
void HealthMonitor::setWifiConnected(bool connected)
{
LockGuard lock(m_mutex);
m_wifiConnected = connected;
}
// ============================================================
// WiFi RSSI 更新
// ============================================================
/**
* @brief 根据 WiFi RSSI 更新健康状态。
* @param rssi WiFi RSSI 值。
* @return 健康检查状态。
*/
health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
{
health_state_t cur = getState();
health_state_t next = SYS_NORMAL;
if (rssi < WIFI_RSSI_CRITICAL) {
next = SYS_CRITICAL;
} else if (rssi < WIFI_RSSI_NORMAL) {
next = SYS_WARNING;
}
// 仅在状态发生转换时打印一次,避免同一阈值下反复刷屏。
if (next != cur) {
if (next == SYS_CRITICAL) {
setState(SYS_CRITICAL);
DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
} else if (next == SYS_WARNING) {
setState(SYS_WARNING);
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
} else { // next == SYS_NORMAL,从 WARNING/CRITICAL 恢复
setState(SYS_NORMAL);
DEBUG_INFO("HEALTH", "RSSI恢复 %d>=%d (lv%d)", rssi, WIFI_RSSI_NORMAL, (int)cur);
}
}
if (next == SYS_CRITICAL) return HEALTH_MON_CRITICAL;
if (next == SYS_WARNING) return HEALTH_MON_WARNING;
return HEALTH_MON_OK;
}
// ============================================================
// 堆低水位快照("最低空闲"创新低时记录上下文)
// ============================================================
/**
* @brief 检测"内部堆最低空闲"是否创新低,是则覆盖写入唯一快照槽。
*
* 调用方:health_mon 5s 巡检任务。仅在"新低"时写一条,避免每 5s 刷一条无意义记录。
* 上下文字段:OTA 系统状态、通道、RSSI、温度、栈剩余最少的任务名+剩余字节。
* 所有外部读取(OtaCoordinator::getSnapshot / collectTaskStackMetrics / rssi / temp)
* 都先在锁外取,避免持锁期间嵌套调用其它模块的锁;持锁后只做字段拷贝。
*/
void HealthMonitor::maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics, size_t pre_count)
{
// 与 http_handlers.cpp 同口径:内部 DRAM 数据堆
static constexpr uint32_t CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
const uint32_t cur_min = (uint32_t)heap_caps_get_minimum_free_size(CAPS);
// 不创新低则直接返回(不持锁)
if (cur_min >= m_heapLowWaterLastMin) {
return;
}
// ── 锁外收集上下文 ──
const uint32_t free_heap = (uint32_t)esp_get_free_heap_size();
const uint32_t largest_block = (uint32_t)heap_caps_get_largest_free_block(CAPS);
/* hook 共享状态一致快照:禁中断一次性拷出,避免多字段撕裂读。
* 必须在 m_mutex 外取——s_hookMux 与 m_mutex 不可嵌套持锁。 */
const HookSnap hs = captureHookSnapshot();
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
const uint8_t sys_state = (uint8_t)snap.sys_state;
const uint8_t ota_channel = (uint8_t)snap.channel;
const uint32_t ota_stored = (uint32_t)snap.meta.stored_size;
const uint32_t ota_total = (uint32_t)snap.meta.total_size;
int8_t rssi = -127;
{
int8_t r;
uint32_t age_ms;
if (getLastWifiRssi(&r, &age_ms)) {
rssi = r;
}
}
const float temperature = getLastTemperature();
/* current_task = 采样者,不是触发者。固定写 health_mon,避免误解为“谁 alloc”。 */
char current_task[16] = "health_mon";
// 连接状态
const uint8_t ble_connected = BleOta::instance().isConnected() ? 1 : 0;
const uint8_t wifi_connected = WifiOta::instance().getStaConnected() ? 1 : 0;
// 嫌疑资源水位:定位"哪个子系统吃堆"
const uint16_t ble_queue_depth = (uint16_t)ble_ota_get_data_queue_depth();
const uint16_t ble_reorder = (uint16_t)ble_ota_get_reorder_cached_count();
const uint16_t spi_pending = (uint16_t)SpiMaster::instance().getPendingJobCount();
// 内存全景:default 含 PSRAM,与 internal 对比可判断"是内部碎片 vs 系统级紧"
const uint32_t default_free = (uint32_t)heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
uint32_t alloc_count = 0;
{
multi_heap_info_t info = {};
heap_caps_get_info(&info, CAPS);
alloc_count = (uint32_t)info.allocated_blocks;
}
// 收集所有受监控任务栈水位,按"剩余升序"取前 8 个
HeapLowTaskStack stacks[8];
uint8_t stack_count = 0;
{
// 优先复用巡检循环已采集的指标,避免二次遍历
TaskStackMetric local_metrics[STACK_TASK_MAX];
const TaskStackMetric *metrics;
size_t count;
if (pre_metrics && pre_count > 0) {
metrics = pre_metrics;
count = pre_count;
} else {
count = collectTaskStackMetrics(local_metrics, STACK_TASK_MAX);
metrics = local_metrics;
}
// 先全部采集
struct Item { char name[16]; uint32_t rem; };
Item items[STACK_TASK_MAX];
size_t item_n = 0;
for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) {
if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) {
continue;
}
UBaseType_t hw = uxTaskGetStackHighWaterMark(metrics[i].handle);
Item &it = items[item_n++];
it.rem = (uint32_t)hw * sizeof(StackType_t);
strncpy(it.name, metrics[i].name ? metrics[i].name : "?", sizeof(it.name) - 1);
it.name[sizeof(it.name) - 1] = '\0';
}
// 冒泡排序按 rem 升序(item_n ≤ 9,开销小)
for (size_t i = 0; i + 1 < item_n; ++i) {
for (size_t j = i + 1; j < item_n; ++j) {
if (items[j].rem < items[i].rem) {
Item t = items[i]; items[i] = items[j]; items[j] = t;
}
}
}
// 取前 8
for (size_t i = 0; i < item_n && stack_count < 8; ++i) {
strncpy(stacks[stack_count].name, items[i].name, sizeof(stacks[stack_count].name) - 1);
stacks[stack_count].name[sizeof(stacks[stack_count].name) - 1] = '\0';
stacks[stack_count].remaining = items[i].rem;
++stack_count;
}
}
// ── 持锁覆盖写唯一槽 ──
LockGuard lock(m_mutex);
HeapLowWaterRecord &rec = m_heapLowWater;
rec = {};
rec.ts_ms = (uint64_t)(esp_timer_get_time() / 1000);
rec.free_heap = free_heap;
rec.min_free_heap = cur_min;
rec.largest_free_block = largest_block;
rec.sys_state = sys_state;
rec.ota_channel = ota_channel;
rec.rssi = rssi;
rec.temperature = temperature;
rec.task_stack_count = stack_count;
for (uint8_t i = 0; i < stack_count && i < 8; ++i) {
rec.task_stacks[i] = stacks[i];
}
strncpy(rec.current_task, current_task, sizeof(rec.current_task) - 1);
rec.current_task[sizeof(rec.current_task) - 1] = '\0';
rec.ble_connected = ble_connected;
rec.wifi_connected = wifi_connected;
rec.ota_stored_size = ota_stored;
rec.ota_total_size = ota_total;
rec.ble_data_queue_depth = ble_queue_depth;
rec.ble_reorder_cached = ble_reorder;
rec.spi_pending_jobs = spi_pending;
rec.reserved = 0;
rec.default_free_heap = default_free;
rec.alloc_count = alloc_count;
// heap hook:「最后一击」alloc(无任务名;见文件头注释)
// 字段取自锁外已取的一致快照 hs(captureHookSnapshot),不再裸读 volatile,
// 消除 fired/size/caller/free_after 多字段撕裂读。
{
const uint32_t hook_size = hs.alloc_size;
const uint32_t hook_caller = hs.caller_addr;
const uint32_t hook_after = hs.free_after;
const uint32_t hook_fired = hs.fired;
/* 窗口内更大块(若存在):发送前小 pbuf 压低水位时,主因更可能是此前 bulk */
uint32_t big_size = 0;
uint32_t big_caller = 0;
const uint32_t n = hs.recent_n;
for (uint32_t i = 0; i < n && i < (uint32_t)kRecentAllocCap; ++i) {
if (hs.recent[i].size > big_size) {
big_size = hs.recent[i].size;
big_caller = hs.recent[i].caller;
}
}
rec.hook_alloc_size = hook_size;
rec.hook_caller_addr = hook_caller;
rec.hook_free_after = hook_after;
rec.hook_fired = hook_fired;
/* hook_task 存类别:net? / net?+big / bulk / tiny(非 FreeRTOS 任务名) */
rec.hook_task[0] = '\0';
if (hook_fired) {
const char *cls = "tiny";
if (hook_size >= 2048U) {
cls = "bulk";
} else if (hook_size >= kHookInterestingMin) {
if (big_size > hook_size && big_size >= 2048U) {
cls = "net?+big";
rec.hook_caller_addr = big_caller;
} else {
cls = "net?";
}
}
strncpy(rec.hook_task, cls, sizeof(rec.hook_task) - 1);
rec.hook_task[sizeof(rec.hook_task) - 1] = '\0';
}
/* 消费已记录的 hook 命中:在 s_hookMux 内重置 min_est/fired,避免同一窗口
* 重复记。与 hook 写互斥(禁中断),不持 m_mutex 同时持 s_hookMux。 */
portENTER_CRITICAL(&s_hookMux);
consumeHookFiredInLock();
portEXIT_CRITICAL(&s_hookMux);
}
rec.ip_event_count = g_ip_event_count;
m_heapLowWaterValid = true;
m_heapLowWaterLastMin = cur_min;
}
/**
* @brief 输出设备唯一快照槽:{"count":0|1,"records":[...]}。
* cap 不够装下整条时退回 count=0,避免半截 JSON。
*/
int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
{
LockGuard lock(m_mutex);
if (!m_heapLowWaterValid) {
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
const HeapLowWaterRecord &r = m_heapLowWater;
/* hook_free_after 是 estimated(hook 只减不增近似值),JSON 字段名显式标注 est,
* hook_confidence 告知前端置信度:hook 命中=est_low_watermark,未命中=none。
* 前端应据此区分 estimated 与 measured(free_heap/min_free_heap 为实测)。 */
const char *hook_confidence = r.hook_fired ? "est_low_watermark" : "none";
int pos = http_json_append(buf, cap, offset,
"{\"count\":1,\"records\":[{"
"\"ts_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu,"
"\"largest_free_block\":%lu,\"sys_state\":%u,\"ota_channel\":%u,"
"\"rssi\":%d,\"temperature\":%.1f,"
"\"current_task\":\"%s\",\"ble_connected\":%u,\"wifi_connected\":%u,"
"\"ota_stored_size\":%lu,\"ota_total_size\":%lu,"
"\"ble_data_queue_depth\":%u,\"ble_reorder_cached\":%u,"
"\"spi_pending_jobs\":%u,\"default_free_heap\":%lu,\"alloc_count\":%lu,"
"\"hook_task\":\"%s\",\"hook_alloc_size\":%lu,\"hook_caller_addr\":%lu,"
"\"hook_est_free_after\":%lu,\"hook_confidence\":\"%s\",\"hook_fired\":%lu,"
"\"ip_event_count\":%lu,"
"\"task_stacks\":[",
(unsigned long long)r.ts_ms,
(unsigned long)r.free_heap,
(unsigned long)r.min_free_heap,
(unsigned long)r.largest_free_block,
(unsigned)r.sys_state,
(unsigned)r.ota_channel,
(int)r.rssi,
(double)r.temperature,
r.current_task,
(unsigned)r.ble_connected,
(unsigned)r.wifi_connected,
(unsigned long)r.ota_stored_size,
(unsigned long)r.ota_total_size,
(unsigned)r.ble_data_queue_depth,
(unsigned)r.ble_reorder_cached,
(unsigned)r.spi_pending_jobs,
(unsigned long)r.default_free_heap,
(unsigned long)r.alloc_count,
r.hook_task,
(unsigned long)r.hook_alloc_size,
(unsigned long)r.hook_caller_addr,
(unsigned long)r.hook_free_after,
hook_confidence,
(unsigned long)r.hook_fired,
(unsigned long)r.ip_event_count);
if (pos == offset) {
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
for (uint8_t k = 0; k < r.task_stack_count && k < 8; ++k) {
int after = http_json_append(buf, cap, pos,
"%s{\"name\":\"%s\",\"remaining\":%lu}",
k == 0 ? "" : ",",
r.task_stacks[k].name,
(unsigned long)r.task_stacks[k].remaining);
if (after == pos) {
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
pos = after;
}
int closed = http_json_append(buf, cap, pos, "]}]}");
if (closed == pos) {
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
}
return closed;
}