From a6f01d544e0dd7e55582f5bf4630bf237dc2ae96 Mon Sep 17 00:00:00 2001 From: "Valentin V. Bartenev" Date: Sat, 9 May 2026 17:26:27 +0300 Subject: [PATCH] =?UTF-8?q?ESP32:=20add=20CPUUsageTracker=20=E2=80=93=20co?= =?UTF-8?q?re-0=20load=20averages=20(1/5/15=20min)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track CPU utilisation on core 0 using a FreeRTOS tick hook that increments per-tick idle/busy counters, sampled every 5s by an esp_timer callback. Exponential moving averages with Linux-style time constants (1 / 5 / 15 min) are maintained in software: DECAY1 = exp(-5/60) ≈ 0.9200 (1-minute window) DECAY5 = exp(-5/300) ≈ 0.9835 (5-minute window) DECAY15 = exp(-5/900) ≈ 0.9945 (15-minute window) Core 1 is excluded intentionally: it runs the Arduino loopTask at 100% load for LoRa packet processing, so its figure is always 1.0 and carries no diagnostic value. All other tasks are pinned to core 0 by task_pinning.c, so core-0 load reflects the true system utilisation. New files: arch/esp32/CPUUsageTracker.h – class declaration arch/esp32/CPUUsageTracker.cpp – tick hook + esp_timer sampling Integration: MyMesh::begin() calls _cpu_tracker.begin() on ESP32 builds. formatStatsReply() emits "load_avg":[<1m>,<5m>,<15m>] in the compact stats JSON payload. formatWebStatsSummaryJson() adds the same field to the web-panel stats endpoint under core.load_avg. The web-panel HUD gains a "Load Avg" metric tile showing all three values side-by-side; the core-metrics grid is widened from 4 to 5 columns. The tile is rendered conditionally so older firmware (or non-ESP32 builds) that omit the field degrade gracefully. CPUUsageTracker.cpp is added to the esp32_base build_src_filter in platformio.ini. --- arch/esp32/CPUUsageTracker.cpp | 51 +++++++++++++++++++++++++++++ arch/esp32/CPUUsageTracker.h | 40 ++++++++++++++++++++++ examples/simple_repeater/MyMesh.cpp | 9 +++-- examples/simple_repeater/MyMesh.h | 4 +++ platformio.ini | 1 + src/helpers/web/WebPanelServer.cpp | 12 ++++++- 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 arch/esp32/CPUUsageTracker.cpp create mode 100644 arch/esp32/CPUUsageTracker.h diff --git a/arch/esp32/CPUUsageTracker.cpp b/arch/esp32/CPUUsageTracker.cpp new file mode 100644 index 00000000..3cc0d6a7 --- /dev/null +++ b/arch/esp32/CPUUsageTracker.cpp @@ -0,0 +1,51 @@ +#ifdef ESP32 +#include "CPUUsageTracker.h" +#include "esp_freertos_hooks.h" + +volatile uint32_t CPUUsageTracker::s_idle_ticks = 0; +volatile uint32_t CPUUsageTracker::s_busy_ticks = 0; +TaskHandle_t CPUUsageTracker::s_idle_handle = nullptr; + +void IRAM_ATTR CPUUsageTracker::s_tick_hook() { // ← was CPUsageTracker (typo) + if (xTaskGetCurrentTaskHandle() == s_idle_handle) { + s_idle_ticks++; + } else { + s_busy_ticks++; + } +} + +void CPUUsageTracker::s_sample_cb(void* arg) { + static_cast(arg)->_onSample(); // ← was CpuUsageTracker (old name) +} + +void CPUUsageTracker::_onSample() { + uint32_t busy = s_busy_ticks; + uint32_t idle = s_idle_ticks; + uint32_t db = busy - _last_busy; + uint32_t di = idle - _last_idle; + _last_busy = busy; + _last_idle = idle; + + uint32_t total = db + di; + float sample = (total > 0) ? (float)db / total : 0.0f; + + _avg1 = DECAY1 * _avg1 + (1.0f - DECAY1) * sample; + _avg5 = DECAY5 * _avg5 + (1.0f - DECAY5) * sample; + _avg15 = DECAY15 * _avg15 + (1.0f - DECAY15) * sample; +} + +void CPUUsageTracker::begin() { + s_idle_handle = xTaskGetIdleTaskHandleForCPU(0); + esp_register_freertos_tick_hook_for_cpu(s_tick_hook, 0); + + esp_timer_create_args_t args = { + .callback = s_sample_cb, + .arg = this, + .dispatch_method = ESP_TIMER_TASK, + .name = "cpu_avg" + }; + esp_timer_create(&args, &_timer); + esp_timer_start_periodic(_timer, 5000000ULL); // 5 seconds +} + +#endif diff --git a/arch/esp32/CPUUsageTracker.h b/arch/esp32/CPUUsageTracker.h new file mode 100644 index 00000000..686bd74e --- /dev/null +++ b/arch/esp32/CPUUsageTracker.h @@ -0,0 +1,40 @@ +#pragma once +#ifdef ESP32 + +#include +#include "esp_attr.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +class CPUUsageTracker { +public: + void begin(); + + float getLoadAvg1() const { return _avg1; } + float getLoadAvg5() const { return _avg5; } + float getLoadAvg15() const { return _avg15; } + +private: + static constexpr float DECAY1 = 0.920044415f; // exp(-5/60) + static constexpr float DECAY5 = 0.983471454f; // exp(-5/300) + static constexpr float DECAY15 = 0.994459848f; // exp(-5/900) + + static volatile uint32_t s_idle_ticks; + static volatile uint32_t s_busy_ticks; + static TaskHandle_t s_idle_handle; + + static void IRAM_ATTR s_tick_hook(); + static void s_sample_cb(void* arg); + + uint32_t _last_idle = 0; + uint32_t _last_busy = 0; + float _avg1 = 0.0f; + float _avg5 = 0.0f; + float _avg15 = 0.0f; + esp_timer_handle_t _timer = nullptr; + + void _onSample(); +}; + +#endif diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 64043d83..71c334fb 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1269,6 +1269,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) { mesh::Mesh::begin(); +#if defined(ESP32) + _cpu_tracker.begin(); +#endif _fs = fs; _archive = archive; last_millis = millis(); @@ -1529,9 +1532,10 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) { void MyMesh::formatStatsReply(char *reply, size_t reply_size) { snprintf(reply, reply_size, - "{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}", + "{\"battery_mv\":%u,\"uptime_secs\":%u,\"load_avg\":[%.2f,%.2f,%.2f],\"errors\":%u,\"queue_len\":%u}", getBatteryMilliVolts(true), _ms->getMillis() / 1000, + _cpu_tracker.getLoadAvg1(), _cpu_tracker.getLoadAvg5(), _cpu_tracker.getLoadAvg15(), _err_flags, _mgr->getOutboundTotal()); } @@ -2527,7 +2531,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { "\"archive\":{\"logical\":\"%s\",\"available\":%s,\"path\":\"%s\",\"type\":\"%s\"," "\"total_bytes\":%llu,\"used_bytes\":%llu}," "\"core\":{\"battery_mv\":%u,\"battery_pct\":%d,\"battery_display_pct\":%d,\"battery_min_mv\":%u,\"battery_max_mv\":%u," - "\"uptime_secs\":%lu,\"errors\":%u,\"queue_len\":%u," + "\"uptime_secs\":%lu,\"load_avg\":[%.2f,%.2f,%.2f],\"errors\":%u,\"queue_len\":%u," "\"external_power\":%s,\"charging\":%s,\"vbus\":%s}," "\"radio\":{\"noise_floor\":%d,\"last_rssi\":%.2f,\"last_snr\":%.2f,\"tx_air_secs\":%lu,\"rx_air_secs\":%lu}," "\"packets\":{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u," @@ -2560,6 +2564,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { battery_min_mv, battery_max_mv, static_cast(uptime_millis / 1000), + _cpu_tracker.getLoadAvg1(), _cpu_tracker.getLoadAvg5(), _cpu_tracker.getLoadAvg15(), _err_flags, static_cast(_mgr->getOutboundTotal()), board.isExternalPowered() ? "true" : "false", diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index c4636ff0..0b2b54fd 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -11,6 +11,7 @@ #include #elif defined(ESP32) #include + #include "../../arch/esp32/CPUUsageTracker.h" #endif #ifdef WITH_RS232_BRIDGE @@ -176,6 +177,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm #endif #if defined(ESP_PLATFORM) && WITH_WEB_PANEL WebService web; +#endif +#if defined(ESP32) + CPUUsageTracker _cpu_tracker; #endif StatsHistory _stats_history; struct { diff --git a/platformio.ini b/platformio.ini index 2d4e468d..49ec4dad 100644 --- a/platformio.ini +++ b/platformio.ini @@ -69,6 +69,7 @@ build_flags = ${arduino_base.build_flags} build_src_filter = ${arduino_base.build_src_filter} +<../arch/esp32/task_pinning.c> +<../arch/esp32/tls_cipher_restrict.c> + +<../arch/esp32/CPUUsageTracker.cpp> [esp32_ota] lib_deps = diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 186057d8..d94bb2c5 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -494,11 +494,13 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( .metric-grid-4 { grid-template-columns:repeat(4,minmax(0,1fr)); } .core-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; } .core-grid .hud-row { align-content:start; } - .core-metrics { grid-template-columns:repeat(4,minmax(0,1fr)); } + .core-metrics { grid-template-columns:repeat(5,minmax(0,1fr)); } .metric { background:rgba(255,255,255,.45); border:1px solid var(--border); border-radius:12px; padding:10px; } :root[data-theme="dark"] .metric { background:rgba(0,0,0,.16); } .metric-label { font-size:11px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em; } .metric-value { margin-top:4px; font-size:16px; font-weight:700; color:var(--text); } + .metric-spread { display:flex; justify-content:space-between; margin-top:4px; } + .metric-spread span { font-size:16px; font-weight:700; color:var(--text); } .events-table-wrap { overflow-x:auto; border:1px solid var(--border); border-radius:12px; } .events-table { width:100%; border-collapse:collapse; } .events-table th, .events-table td { padding:10px 12px; text-align:left; font-size:13px; } @@ -1457,6 +1459,14 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
${renderMetric("Uptime", formatDuration(core.uptime_secs))} ${renderMetric("Battery", (core.battery_mv || 0) + " mV")} + ${Array.isArray(core.load_avg) ? `
+
Load Avg
+
+ ${core.load_avg[0].toFixed(2)} + ${core.load_avg[1].toFixed(2)} + ${core.load_avg[2].toFixed(2)} +
+
` : ""} ${renderMetric("Queue", String(core.queue_len ?? 0))} ${renderMetric("Errors", String(core.errors ?? 0))}