소스 검색

ESP32: add CPUUsageTracker – core-0 load averages (1/5/15 min)

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.
Valentin V. Bartenev 3 달 전
부모
커밋
a6f01d544e
6개의 변경된 파일114개의 추가작업 그리고 3개의 파일을 삭제
  1. 51 0
      arch/esp32/CPUUsageTracker.cpp
  2. 40 0
      arch/esp32/CPUUsageTracker.h
  3. 7 2
      examples/simple_repeater/MyMesh.cpp
  4. 4 0
      examples/simple_repeater/MyMesh.h
  5. 1 0
      platformio.ini
  6. 11 1
      src/helpers/web/WebPanelServer.cpp

+ 51 - 0
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<CPUUsageTracker*>(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

+ 40 - 0
arch/esp32/CPUUsageTracker.h

@@ -0,0 +1,40 @@
+#pragma once
+#ifdef ESP32
+
+#include <stdint.h>
+#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

+ 7 - 2
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<unsigned long>(uptime_millis / 1000),
+                     _cpu_tracker.getLoadAvg1(), _cpu_tracker.getLoadAvg5(), _cpu_tracker.getLoadAvg15(),
                      _err_flags,
                      static_cast<unsigned>(_mgr->getOutboundTotal()),
                      board.isExternalPowered() ? "true" : "false",

+ 4 - 0
examples/simple_repeater/MyMesh.h

@@ -11,6 +11,7 @@
   #include <LittleFS.h>
 #elif defined(ESP32)
   #include <SPIFFS.h>
+  #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 {

+ 1 - 0
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 =

+ 11 - 1
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(
         <div class="metric-grid core-metrics">
           ${renderMetric("Uptime", formatDuration(core.uptime_secs))}
           ${renderMetric("Battery", (core.battery_mv || 0) + " mV")}
+          ${Array.isArray(core.load_avg) ? `<div class="metric">
+          <div class="metric-label">Load Avg</div>
+          <div class="metric-spread">
+            <span>${core.load_avg[0].toFixed(2)}</span>
+            <span>${core.load_avg[1].toFixed(2)}</span>
+            <span>${core.load_avg[2].toFixed(2)}</span>
+          </div>
+          </div>` : ""}
           ${renderMetric("Queue", String(core.queue_len ?? 0))}
           ${renderMetric("Errors", String(core.errors ?? 0))}
         </div>