diff --git a/arch/esp32/CPUUsageTracker.cpp b/arch/esp32/CPUUsageTracker.cpp index 3cc0d6a7..84441ea4 100644 --- a/arch/esp32/CPUUsageTracker.cpp +++ b/arch/esp32/CPUUsageTracker.cpp @@ -6,7 +6,7 @@ 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) +void IRAM_ATTR CPUUsageTracker::s_tick_hook() { if (xTaskGetCurrentTaskHandle() == s_idle_handle) { s_idle_ticks++; } else { @@ -15,23 +15,26 @@ void IRAM_ATTR CPUUsageTracker::s_tick_hook() { // ← was CPUsageTracker (typ } void CPUUsageTracker::s_sample_cb(void* arg) { - static_cast(arg)->_onSample(); // ← was CpuUsageTracker (old name) + static_cast(arg)->_onSample(); } 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; + const uint32_t busy = s_busy_ticks; + const uint32_t idle = s_idle_ticks; + const uint32_t db = busy - _last_busy; + const 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; + const uint32_t total = db + di; + const float sample = (total > 0) ? (float)db / (float)total : 0.0f; - _avg1 = DECAY1 * _avg1 + (1.0f - DECAY1) * sample; - _avg5 = DECAY5 * _avg5 + (1.0f - DECAY5) * sample; - _avg15 = DECAY15 * _avg15 + (1.0f - DECAY15) * sample; + const uint8_t s8 = (uint8_t)(sample * 255.0f + 0.5f); + _sma_sum -= _sma_buf[_sma_idx]; + _sma_buf[_sma_idx] = s8; + _sma_sum += s8; + _sma_idx = (_sma_idx + 1) & (SMA_WINDOW - 1); + _sma_avg = (float)_sma_sum * (1.0f / (SMA_WINDOW * 255.0f)); } void CPUUsageTracker::begin() { @@ -42,10 +45,10 @@ void CPUUsageTracker::begin() { .callback = s_sample_cb, .arg = this, .dispatch_method = ESP_TIMER_TASK, - .name = "cpu_avg" + .name = "cpu_sample" }; esp_timer_create(&args, &_timer); - esp_timer_start_periodic(_timer, 5000000ULL); // 5 seconds + esp_timer_start_periodic(_timer, 60000000ULL / SMA_WINDOW); // SMA_WINDOW samples per minute } #endif diff --git a/arch/esp32/CPUUsageTracker.h b/arch/esp32/CPUUsageTracker.h index 686bd74e..eea394d0 100644 --- a/arch/esp32/CPUUsageTracker.h +++ b/arch/esp32/CPUUsageTracker.h @@ -11,14 +11,10 @@ class CPUUsageTracker { public: void begin(); - float getLoadAvg1() const { return _avg1; } - float getLoadAvg5() const { return _avg5; } - float getLoadAvg15() const { return _avg15; } + float getCore0Util() const { return _sma_avg; } 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 constexpr uint8_t SMA_WINDOW = 64; // power of 2 — enables & mask static volatile uint32_t s_idle_ticks; static volatile uint32_t s_busy_ticks; @@ -27,11 +23,15 @@ private: 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; + uint32_t _last_idle = 0; + uint32_t _last_busy = 0; + + uint8_t _sma_buf[SMA_WINDOW] = {}; + uint16_t _sma_sum = 0; + uint8_t _sma_idx = 0; + + volatile float _sma_avg = 0.0f; + esp_timer_handle_t _timer = nullptr; void _onSample(); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a5a234f7..f8446648 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1532,10 +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,\"load_avg\":[%.2f,%.2f,%.2f],\"errors\":%u,\"queue_len\":%u}", + "{\"battery_mv\":%u,\"uptime_secs\":%u,\"core0_util\":%.1f,\"errors\":%u,\"queue_len\":%u}", getBatteryMilliVolts(true), _ms->getMillis() / 1000, - _cpu_tracker.getLoadAvg1(), _cpu_tracker.getLoadAvg5(), _cpu_tracker.getLoadAvg15(), + _cpu_tracker.getCore0Util() * 100.0f, _err_flags, _mgr->getOutboundTotal()); } @@ -1941,7 +1941,7 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { sample.heap_min = ESP.getMinFreeHeap(); sample.psram_free = ESP.getFreePsram(); sample.psram_min = ESP.getMinFreePsram(); - sample.load_avg1_pct = (uint8_t)(_cpu_tracker.getLoadAvg1() * 100.0f + 0.5f); + sample.core0_util_pct = (uint8_t)(_cpu_tracker.getCore0Util() * 100.0f + 0.5f); #endif if (board.isExternalPowered()) sample.flags |= HISTORY_FLAG_EXTERNAL_POWER; if (board.isCharging()) sample.flags |= HISTORY_FLAG_CHARGING; @@ -2532,7 +2532,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,\"load_avg\":[%.2f,%.2f,%.2f],\"errors\":%u,\"queue_len\":%u," + "\"uptime_secs\":%lu,\"core0_util\":%.1f,\"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," @@ -2565,7 +2565,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(), + _cpu_tracker.getCore0Util() * 100.0f, _err_flags, static_cast(_mgr->getOutboundTotal()), board.isExternalPowered() ? "true" : "false", diff --git a/src/helpers/StatsHistory.cpp b/src/helpers/StatsHistory.cpp index 7416c805..9bde7c5b 100644 --- a/src/helpers/StatsHistory.cpp +++ b/src/helpers/StatsHistory.cpp @@ -211,8 +211,8 @@ bool buildPointValue(const HistorySample& sample, const HistorySample* previous, value = static_cast(sample.battery_mv); return true; } - if (strcmp(series, "cpu_load") == 0) { - value = static_cast(sample.load_avg1_pct); + if (strcmp(series, "core0_util") == 0) { + value = static_cast(sample.core0_util_pct); return true; } if (strcmp(series, "memory") == 0) { @@ -313,8 +313,8 @@ const char* seriesTitle(const char* series) { if (strcmp(series, "battery") == 0) { return "Battery"; } - if (strcmp(series, "cpu_load") == 0) { - return "CPU Load"; + if (strcmp(series, "core0_util") == 0) { + return "Core0 Util"; } if (strcmp(series, "memory") == 0) { return "Heap Free"; @@ -362,7 +362,7 @@ const char* seriesUnit(const char* series) { if (strcmp(series, "battery") == 0) { return "mV"; } - if (strcmp(series, "cpu_load") == 0) { + if (strcmp(series, "core0_util") == 0) { return "pct"; } if (strcmp(series, "memory") == 0) { @@ -649,7 +649,7 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con long gps_lon_e6 = 0; unsigned sensor_flags = 0; unsigned gps_satellites = 0; - unsigned load_avg1_pct = 0; + unsigned core0_util_pct = 0; int parsed = sscanf(line, "%lu,%lu,%u,%u,%d,%d,%d,%lu,%lu,%lu,%lu,%u,%u,%u,%u,%u,%u,%u,%d,%d,%u,%u,%d,%d,%ld,%ld,%u,%u,%u", @@ -681,7 +681,7 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con &gps_lon_e6, &sensor_flags, &gps_satellites, - &load_avg1_pct); + &core0_util_pct); if (parsed == 29 || parsed == 28) { memset(&sample, 0, sizeof(sample)); sample.epoch_secs = static_cast(epoch_secs); @@ -715,7 +715,7 @@ bool StatsHistory::parseSummaryLine(const char* line, HistorySample& sample) con sample.gps_satellites = static_cast(gps_satellites); sample.flags = static_cast(flags); sample.battery_pct = -1; - sample.load_avg1_pct = (parsed == 29) ? static_cast(load_avg1_pct) : 0; + sample.core0_util_pct = (parsed == 29) ? static_cast(core0_util_pct) : 0; return true; } @@ -1129,7 +1129,7 @@ void StatsHistory::flushSummaryLog() { static_cast(latest.gps_lon_e6), static_cast(latest.sensor_flags), static_cast(latest.gps_satellites), - static_cast(latest.load_avg1_pct)); + static_cast(latest.core0_util_pct)); File latest_file = openArchiveWriteWithRecovery(_archive, kSummaryLatestPath); if (!latest_file) { diff --git a/src/helpers/StatsHistory.h b/src/helpers/StatsHistory.h index 40119a3a..e5fb8c4a 100644 --- a/src/helpers/StatsHistory.h +++ b/src/helpers/StatsHistory.h @@ -37,7 +37,7 @@ struct HistorySample { uint8_t gps_satellites; uint8_t flags; int8_t battery_pct; - uint8_t load_avg1_pct; + uint8_t core0_util_pct; }; struct HistoryEvent { diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 6bf9f614..65b359a7 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -531,8 +531,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( :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; } @@ -1490,14 +1488,7 @@ 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("Core0 Util", (core.core0_util || 0) + " %")} ${renderMetric("Queue", String(core.queue_len ?? 0))} ${renderMetric("Errors", String(core.errors ?? 0))}
@@ -1779,7 +1770,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( const kTrendFmt = { battery: [v => Math.round(v), "mV"], voltage: [v => (v/100).toFixed(2), "V"], - cpu_load: [v => Math.round(v), "%"], + core0_util: [v => v, "%"], memory: [v => formatBytes(v), ""], packets: [v => Math.round(v), "pkts"], error_rate: [v => (v/10).toFixed(1), "%"], @@ -1827,7 +1818,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( if (recent >= 750) return "#d7a531"; // high return "#2f8f4e"; // normal } - if (key === "cpu_load") return "#e07b39"; + if (key === "core0_util") return "#e07b39"; if (key === "error_rate") return "#ef4444"; if (key === "gps_satellites") return "#2f8f4e"; if (key === "signal") return "#3b82f6"; @@ -1847,7 +1838,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( return baseColor || "#2f8f4e"; } function sparkValueRange(key, values) { - if (key === "cpu_load") { + if (key === "core0_util") { return { min: 0, max: Math.max(10, ...values) }; } if (key === "packets" || key === "gps_satellites") { @@ -2110,7 +2101,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( const sensors = summaryPayload && summaryPayload.sensors ? summaryPayload.sensors : null; const gpsEnabled = !!(sensors && sensors.gps_enabled === true); const mcuTempPresent = !!(sensors && Number.isFinite(sensors.mcu_temp_c)); - const order = ["battery", "cpu_load", "memory", "packets", "error_rate", "signal", "noise_floor"]; + const order = ["battery", "core0_util", "memory", "packets", "error_rate", "signal", "noise_floor"]; if (mcuTempPresent) { order.splice(1, 0, "mcu_temp"); }