From a30917016223e2af2b92a00ebd9363e70801cd0a Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 10 Mar 2026 13:11:36 +0100 Subject: [PATCH] chore: align formatter baseline with esptoolkit-template --- .clang-format | 11 + .editorconfig | 11 + .vscode/bin/clang-format | 19 + .vscode/extensions.json | 9 + .vscode/settings.json | 30 + .vscode/tasks.json | 12 + README.md | 7 + examples/basic_monitor/basic_monitor.ino | 98 ++- examples/json_export/json_export.ino | 71 +- examples/manual_sampling/manual_sampling.ino | 97 +- scripts/format_cpp.sh | 24 + src/ESPCpuMonitor.h | 1 - src/esp_cpu_monitor/cpu_monitor.cpp | 878 ++++++++++--------- src/esp_cpu_monitor/cpu_monitor.h | 175 ++-- src/esp_cpu_monitor/cpu_monitor_allocator.h | 114 ++- test/test_cpu_monitor/test_cpu_monitor.cpp | 98 ++- 16 files changed, 912 insertions(+), 743 deletions(-) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100755 .vscode/bin/clang-format create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/format_cpp.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 5e89d8c..c6e917e 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,13 @@ When your feature shuts down (task exit, OTA handoff, mode switch), call `cpuMon ## Tests - Build and run `examples/basic_monitor`, `examples/manual_sampling`, and `examples/json_export` via PlatformIO CI or Arduino CLI for a quick smoke test on ESP32 dev boards. +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see [LICENSE.md](LICENSE.md). diff --git a/examples/basic_monitor/basic_monitor.ino b/examples/basic_monitor/basic_monitor.ino index 51cee9b..60203d6 100644 --- a/examples/basic_monitor/basic_monitor.ino +++ b/examples/basic_monitor/basic_monitor.ino @@ -1,58 +1,72 @@ #include #include -#include #include +#include ESPCpuMonitor cpuMonitor; void setup() { - Serial.begin(115200); + Serial.begin(115200); - CpuMonitorConfig cfg; - cfg.sampleIntervalMs = 1000; // 1s sampling cadence - cfg.calibrationSamples = 5; // treat first 5 periods as 100% idle baseline - cfg.historySize = 20; - cfg.enableTemperature = true; // toggle temperature readings if your board supports it - cfg.smoothingMode = CpuSmoothingMode::Ewma; - cfg.smoothingAlpha = 0.25f; // smaller alpha => smoother trend - cpuMonitor.init(cfg); + CpuMonitorConfig cfg; + cfg.sampleIntervalMs = 1000; // 1s sampling cadence + cfg.calibrationSamples = 5; // treat first 5 periods as 100% idle baseline + cfg.historySize = 20; + cfg.enableTemperature = true; // toggle temperature readings if your board supports it + cfg.smoothingMode = CpuSmoothingMode::Ewma; + cfg.smoothingAlpha = 0.25f; // smaller alpha => smoother trend + cpuMonitor.init(cfg); - cpuMonitor.onSample([](const CpuUsageSample &sample) { + cpuMonitor.onSample([](const CpuUsageSample &sample) { #if portNUM_PROCESSORS > 1 - ESP_LOGI("CPU", "core0=%.1f%% core1=%.1f%% avg=%.1f%% smoothed=%.1f%% temp=%.1fC (avg %.1fC)", - sample.perCore[0], sample.perCore[1], sample.average, - std::isnan(sample.smoothedAverage) ? -1.0f : sample.smoothedAverage, - std::isnan(sample.temperatureC) ? -1.0f : sample.temperatureC, - std::isnan(sample.temperatureAvgC) ? -1.0f : sample.temperatureAvgC); + ESP_LOGI( + "CPU", + "core0=%.1f%% core1=%.1f%% avg=%.1f%% smoothed=%.1f%% temp=%.1fC (avg %.1fC)", + sample.perCore[0], + sample.perCore[1], + sample.average, + std::isnan(sample.smoothedAverage) ? -1.0f : sample.smoothedAverage, + std::isnan(sample.temperatureC) ? -1.0f : sample.temperatureC, + std::isnan(sample.temperatureAvgC) ? -1.0f : sample.temperatureAvgC + ); #else - ESP_LOGI("CPU", "core0=%.1f%% avg=%.1f%% smoothed=%.1f%% temp=%.1fC (avg %.1fC)", - sample.perCore[0], sample.average, - std::isnan(sample.smoothedAverage) ? -1.0f : sample.smoothedAverage, - std::isnan(sample.temperatureC) ? -1.0f : sample.temperatureC, - std::isnan(sample.temperatureAvgC) ? -1.0f : sample.temperatureAvgC); + ESP_LOGI( + "CPU", + "core0=%.1f%% avg=%.1f%% smoothed=%.1f%% temp=%.1fC (avg %.1fC)", + sample.perCore[0], + sample.average, + std::isnan(sample.smoothedAverage) ? -1.0f : sample.smoothedAverage, + std::isnan(sample.temperatureC) ? -1.0f : sample.temperatureC, + std::isnan(sample.temperatureAvgC) ? -1.0f : sample.temperatureAvgC + ); #endif - }); + }); } void loop() { - // Optional: pull the latest average every few seconds for a quick check - static uint32_t lastPrint = 0; - if (millis() - lastPrint > 3000) { - lastPrint = millis(); - float avg = cpuMonitor.getLastAverage(); - float smoothed = cpuMonitor.getLastSmoothedAverage(); - if (avg >= 0.0f) { - float temp = 0.0f; - float tempAvg = 0.0f; - if (cpuMonitor.getLastTemperature(temp, tempAvg)) { - Serial.printf("[loop] CPU avg: %.1f%% smoothed: %.1f%% temp: %.1fC (avg %.1fC)\n", - avg, smoothed, temp, tempAvg); - } else { - Serial.printf("[loop] CPU avg: %.1f%% smoothed: %.1f%% temp: n/a\n", avg, smoothed); - } - } else { - Serial.println("[loop] calibrating..."); - } - } - delay(250); + // Optional: pull the latest average every few seconds for a quick check + static uint32_t lastPrint = 0; + if (millis() - lastPrint > 3000) { + lastPrint = millis(); + float avg = cpuMonitor.getLastAverage(); + float smoothed = cpuMonitor.getLastSmoothedAverage(); + if (avg >= 0.0f) { + float temp = 0.0f; + float tempAvg = 0.0f; + if (cpuMonitor.getLastTemperature(temp, tempAvg)) { + Serial.printf( + "[loop] CPU avg: %.1f%% smoothed: %.1f%% temp: %.1fC (avg %.1fC)\n", + avg, + smoothed, + temp, + tempAvg + ); + } else { + Serial.printf("[loop] CPU avg: %.1f%% smoothed: %.1f%% temp: n/a\n", avg, smoothed); + } + } else { + Serial.println("[loop] calibrating..."); + } + } + delay(250); } diff --git a/examples/json_export/json_export.ino b/examples/json_export/json_export.ino index 3c4f002..b2c3017 100644 --- a/examples/json_export/json_export.ino +++ b/examples/json_export/json_export.ino @@ -13,41 +13,44 @@ ESPCpuMonitor cpuMonitor; static const uint32_t HISTORY_LOG_PERIOD_MS = 5000; void setup() { - Serial.begin(115200); - - CpuMonitorConfig cfg; - cfg.sampleIntervalMs = 1000; // 1s cadence for telemetry - cfg.historySize = 12; // keep a handful of points for charts/debug - cfg.enableTemperature = true; - cfg.smoothingMode = CpuSmoothingMode::Ewma; - cfg.smoothingAlpha = 0.2f; - cpuMonitor.init(cfg); - - cpuMonitor.onSample([](const CpuUsageSample &sample) { - StaticJsonDocument<256> doc; - cpuMonitor.toJson(sample, doc); - doc["avgRounded"] = static_cast(roundf(sample.average)); // handy for dashboards - - serializeJson(doc, Serial); - Serial.println(); // newline-delimited JSON (NDJSON) stream - }); + Serial.begin(115200); + + CpuMonitorConfig cfg; + cfg.sampleIntervalMs = 1000; // 1s cadence for telemetry + cfg.historySize = 12; // keep a handful of points for charts/debug + cfg.enableTemperature = true; + cfg.smoothingMode = CpuSmoothingMode::Ewma; + cfg.smoothingAlpha = 0.2f; + cpuMonitor.init(cfg); + + cpuMonitor.onSample([](const CpuUsageSample &sample) { + StaticJsonDocument<256> doc; + cpuMonitor.toJson(sample, doc); + doc["avgRounded"] = static_cast(roundf(sample.average)); // handy for dashboards + + serializeJson(doc, Serial); + Serial.println(); // newline-delimited JSON (NDJSON) stream + }); } void loop() { - static uint32_t lastHistoryMs = 0; - - if (cpuMonitor.isReady() && millis() - lastHistoryMs >= HISTORY_LOG_PERIOD_MS) { - lastHistoryMs = millis(); - const auto hist = cpuMonitor.history(); - if (!hist.empty()) { - const auto &latest = hist.back(); - Serial.printf("[json_export] history_depth=%u last_avg=%.1f%% last_smoothed=%.1f%% last_temp=%.1fC\n", - static_cast(hist.size()), - latest.average, - std::isnan(latest.smoothedAverage) ? -1.0f : latest.smoothedAverage, - std::isnan(latest.temperatureC) ? -1.0f : latest.temperatureC); - } - } - - delay(50); + static uint32_t lastHistoryMs = 0; + + if (cpuMonitor.isReady() && millis() - lastHistoryMs >= HISTORY_LOG_PERIOD_MS) { + lastHistoryMs = millis(); + const auto hist = cpuMonitor.history(); + if (!hist.empty()) { + const auto &latest = hist.back(); + Serial.printf( + "[json_export] history_depth=%u last_avg=%.1f%% last_smoothed=%.1f%% " + "last_temp=%.1fC\n", + static_cast(hist.size()), + latest.average, + std::isnan(latest.smoothedAverage) ? -1.0f : latest.smoothedAverage, + std::isnan(latest.temperatureC) ? -1.0f : latest.temperatureC + ); + } + } + + delay(50); } diff --git a/examples/manual_sampling/manual_sampling.ino b/examples/manual_sampling/manual_sampling.ino index a7aa28a..f9141e2 100644 --- a/examples/manual_sampling/manual_sampling.ino +++ b/examples/manual_sampling/manual_sampling.ino @@ -7,56 +7,61 @@ ESPCpuMonitor cpuMonitor; // Simple spin to create a small CPU bump for the manual sampler. static void spinWork(uint32_t durationMicros) { - const uint32_t start = micros(); - while ((uint32_t)(micros() - start) < durationMicros) { - asm volatile("nop"); - } + const uint32_t start = micros(); + while ((uint32_t)(micros() - start) < durationMicros) { + asm volatile("nop"); + } } void setup() { - Serial.begin(115200); - - CpuMonitorConfig cfg; - cfg.sampleIntervalMs = 0; // manual sampling only - cfg.calibrationSamples = 8; // give the idle baseline a few readings - cfg.historySize = 10; - cfg.enablePerCore = false; // focus on averaged load - cfg.enableTemperature = false; // keep it portable on targets without temp sensor - cfg.smoothingMode = CpuSmoothingMode::RollingMean; - cfg.smoothingWindow = 6; - cpuMonitor.init(cfg); - - cpuMonitor.onSample([](const CpuUsageSample &sample) { - ESP_LOGI("CPU", "avg=%.1f%% smoothed=%.1f%% ts=%llu", - sample.average, - sample.smoothedAverage, - static_cast(sample.timestampUs)); - }); + Serial.begin(115200); + + CpuMonitorConfig cfg; + cfg.sampleIntervalMs = 0; // manual sampling only + cfg.calibrationSamples = 8; // give the idle baseline a few readings + cfg.historySize = 10; + cfg.enablePerCore = false; // focus on averaged load + cfg.enableTemperature = false; // keep it portable on targets without temp sensor + cfg.smoothingMode = CpuSmoothingMode::RollingMean; + cfg.smoothingWindow = 6; + cpuMonitor.init(cfg); + + cpuMonitor.onSample([](const CpuUsageSample &sample) { + ESP_LOGI( + "CPU", + "avg=%.1f%% smoothed=%.1f%% ts=%llu", + sample.average, + sample.smoothedAverage, + static_cast(sample.timestampUs) + ); + }); } void loop() { - static uint32_t lastSampleMs = 0; - - // Simulate bursty work after calibration: spin for ~5ms while in a "busy" window. - const bool busyWindow = cpuMonitor.isReady() && (millis() / 3000) % 2 == 0; - if (busyWindow) { - spinWork(5000); - } - - if (millis() - lastSampleMs >= SAMPLE_PERIOD_MS) { - lastSampleMs = millis(); - - CpuUsageSample sample{}; - bool ready = cpuMonitor.sampleNow(sample); // drive sampling yourself - if (!ready) { - Serial.println("[manual_sampling] calibrating baseline..."); - } else { - Serial.printf("[manual_sampling] avg=%.1f%% smoothed=%.1f%% history_depth=%u\n", - sample.average, - sample.smoothedAverage, - static_cast(cpuMonitor.history().size())); - } - } - - delay(10); + static uint32_t lastSampleMs = 0; + + // Simulate bursty work after calibration: spin for ~5ms while in a "busy" window. + const bool busyWindow = cpuMonitor.isReady() && (millis() / 3000) % 2 == 0; + if (busyWindow) { + spinWork(5000); + } + + if (millis() - lastSampleMs >= SAMPLE_PERIOD_MS) { + lastSampleMs = millis(); + + CpuUsageSample sample{}; + bool ready = cpuMonitor.sampleNow(sample); // drive sampling yourself + if (!ready) { + Serial.println("[manual_sampling] calibrating baseline..."); + } else { + Serial.printf( + "[manual_sampling] avg=%.1f%% smoothed=%.1f%% history_depth=%u\n", + sample.average, + sample.smoothedAverage, + static_cast(cpuMonitor.history().size()) + ); + } + } + + delay(10); } diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/ESPCpuMonitor.h b/src/ESPCpuMonitor.h index 9f0ea43..8a043e8 100644 --- a/src/ESPCpuMonitor.h +++ b/src/ESPCpuMonitor.h @@ -1,4 +1,3 @@ #pragma once #include "esp_cpu_monitor/cpu_monitor.h" - diff --git a/src/esp_cpu_monitor/cpu_monitor.cpp b/src/esp_cpu_monitor/cpu_monitor.cpp index 4ac6bf5..17e1c2c 100644 --- a/src/esp_cpu_monitor/cpu_monitor.cpp +++ b/src/esp_cpu_monitor/cpu_monitor.cpp @@ -1,7 +1,7 @@ #include "cpu_monitor.h" -#include #include +#include #include static const char *TAG = "ESPCpuMonitor"; @@ -10,555 +10,577 @@ volatile uint64_t ESPCpuMonitor::s_idleCount[portNUM_PROCESSORS] = {}; ESPCpuMonitor *ESPCpuMonitor::s_instance = nullptr; ESPCpuMonitor::ESPCpuMonitor() { - resetState(CpuMonitorConfig{}); + resetState(CpuMonitorConfig{}); } ESPCpuMonitor::~ESPCpuMonitor() { - deinit(); + deinit(); } -template -static bool idleHookRegisteredSuccessfully(const TResult &res) { +template static bool idleHookRegisteredSuccessfully(const TResult &res) { #if defined(ESP_OK) - if constexpr (std::is_same::value) { - return res == ESP_OK; - } + if constexpr (std::is_same::value) { + return res == ESP_OK; + } #endif - return static_cast(res); + return static_cast(res); } -template -static int idleHookErrorCode(const TResult &res) { +template static int idleHookErrorCode(const TResult &res) { #if defined(ESP_OK) - if constexpr (std::is_same::value) { - return static_cast(res); - } + if constexpr (std::is_same::value) { + return static_cast(res); + } #endif - return static_cast(res) ? 0 : -1; + return static_cast(res) ? 0 : -1; } static bool isValidSmoothingMode(CpuSmoothingMode mode) { - return mode == CpuSmoothingMode::None - || mode == CpuSmoothingMode::RollingMean - || mode == CpuSmoothingMode::Ewma; + return mode == CpuSmoothingMode::None || mode == CpuSmoothingMode::RollingMean || + mode == CpuSmoothingMode::Ewma; } void ESPCpuMonitor::resetState(const CpuMonitorConfig &cfg) { - config_ = cfg; - if (!isValidSmoothingMode(config_.smoothingMode)) { - config_.smoothingMode = CpuSmoothingMode::None; - } - if (config_.smoothingWindow == 0) { - config_.smoothingWindow = 1; - } - if (config_.smoothingWindow > ESPCM_MAX_SMOOTHING_WINDOW) { - config_.smoothingWindow = ESPCM_MAX_SMOOTHING_WINDOW; - } - if (!(config_.smoothingAlpha > 0.0f && config_.smoothingAlpha <= 1.0f)) { - config_.smoothingAlpha = 0.2f; - } - calibrationSamplesNeeded_ = config_.calibrationSamples == 0 ? 1 : config_.calibrationSamples; - calibrationSamplesDone_ = 0; - hasSample_ = false; - calibrated_ = false; - history_ = CpuMonitorDeque(CpuMonitorAllocator(config_.usePSRAMBuffers)); - callbacks_ = CpuMonitorVector(CpuMonitorAllocator(config_.usePSRAMBuffers)); - resetSmoothingState(); - resetTemperatureState(); - temperatureEnabled_ = false; + config_ = cfg; + if (!isValidSmoothingMode(config_.smoothingMode)) { + config_.smoothingMode = CpuSmoothingMode::None; + } + if (config_.smoothingWindow == 0) { + config_.smoothingWindow = 1; + } + if (config_.smoothingWindow > ESPCM_MAX_SMOOTHING_WINDOW) { + config_.smoothingWindow = ESPCM_MAX_SMOOTHING_WINDOW; + } + if (!(config_.smoothingAlpha > 0.0f && config_.smoothingAlpha <= 1.0f)) { + config_.smoothingAlpha = 0.2f; + } + calibrationSamplesNeeded_ = config_.calibrationSamples == 0 ? 1 : config_.calibrationSamples; + calibrationSamplesDone_ = 0; + hasSample_ = false; + calibrated_ = false; + history_ = CpuMonitorDeque( + CpuMonitorAllocator(config_.usePSRAMBuffers) + ); + callbacks_ = CpuMonitorVector( + CpuMonitorAllocator(config_.usePSRAMBuffers) + ); + resetSmoothingState(); + resetTemperatureState(); + temperatureEnabled_ = false; #if ESPCM_HAS_TEMP_SENSOR_NEW - tempSensor_ = nullptr; + tempSensor_ = nullptr; #elif ESPCM_HAS_TEMP_SENSOR_OLD - tempSensorStarted_ = false; + tempSensorStarted_ = false; #endif - for (int i = 0; i < portNUM_PROCESSORS; ++i) { - prevIdle_[i] = 0; - idleBaseline_[i] = 0.0f; - lastSample_.perCore[i] = 0.0f; - s_idleCount[i] = 0; - } - lastSample_.average = 0.0f; - lastSample_.smoothedAverage = std::numeric_limits::quiet_NaN(); - lastSample_.temperatureC = std::numeric_limits::quiet_NaN(); - lastSample_.temperatureAvgC = std::numeric_limits::quiet_NaN(); + for (int i = 0; i < portNUM_PROCESSORS; ++i) { + prevIdle_[i] = 0; + idleBaseline_[i] = 0.0f; + lastSample_.perCore[i] = 0.0f; + s_idleCount[i] = 0; + } + lastSample_.average = 0.0f; + lastSample_.smoothedAverage = std::numeric_limits::quiet_NaN(); + lastSample_.temperatureC = std::numeric_limits::quiet_NaN(); + lastSample_.temperatureAvgC = std::numeric_limits::quiet_NaN(); } void ESPCpuMonitor::lock() const { - if (mutex_) { - xSemaphoreTake(mutex_, portMAX_DELAY); - } + if (mutex_) { + xSemaphoreTake(mutex_, portMAX_DELAY); + } } void ESPCpuMonitor::unlock() const { - if (mutex_) { - xSemaphoreGive(mutex_); - } + if (mutex_) { + xSemaphoreGive(mutex_); + } } bool ESPCpuMonitor::init(const CpuMonitorConfig &cfg) { - if (s_instance != nullptr) { - ESP_LOGW(TAG, "ESPCpuMonitor already running"); - return false; - } + if (s_instance != nullptr) { + ESP_LOGW(TAG, "ESPCpuMonitor already running"); + return false; + } - resetState(cfg); + resetState(cfg); - if (!mutex_) { - mutex_ = xSemaphoreCreateMutex(); - if (!mutex_) { - ESP_LOGE(TAG, "Failed to create mutex"); - return false; - } - } + if (!mutex_) { + mutex_ = xSemaphoreCreateMutex(); + if (!mutex_) { + ESP_LOGE(TAG, "Failed to create mutex"); + return false; + } + } - bool hook0 = false; - bool hook1 = false; + bool hook0 = false; + bool hook1 = false; #if portNUM_PROCESSORS == 1 - const auto res0 = esp_register_freertos_idle_hook(&ESPCpuMonitor::idleHookCore0); - hook0 = idleHookRegisteredSuccessfully(res0); - if (!hook0) { - ESP_LOGE(TAG, "Failed to register idle hook for core0 (err=%d)", idleHookErrorCode(res0)); - vSemaphoreDelete(mutex_); - mutex_ = nullptr; - return false; - } + const auto res0 = esp_register_freertos_idle_hook(&ESPCpuMonitor::idleHookCore0); + hook0 = idleHookRegisteredSuccessfully(res0); + if (!hook0) { + ESP_LOGE(TAG, "Failed to register idle hook for core0 (err=%d)", idleHookErrorCode(res0)); + vSemaphoreDelete(mutex_); + mutex_ = nullptr; + return false; + } #else - const auto res0 = esp_register_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore0, 0); - hook0 = idleHookRegisteredSuccessfully(res0); - if (!hook0) { - ESP_LOGE(TAG, "Failed to register idle hook for core0 (err=%d)", idleHookErrorCode(res0)); - vSemaphoreDelete(mutex_); - mutex_ = nullptr; - return false; - } - const auto res1 = esp_register_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore1, 1); - hook1 = idleHookRegisteredSuccessfully(res1); - if (!hook1) { - ESP_LOGE(TAG, "Failed to register idle hook for core1 (err=%d)", idleHookErrorCode(res1)); - esp_deregister_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore0, 0); - vSemaphoreDelete(mutex_); - mutex_ = nullptr; - return false; - } + const auto res0 = esp_register_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore0, 0); + hook0 = idleHookRegisteredSuccessfully(res0); + if (!hook0) { + ESP_LOGE(TAG, "Failed to register idle hook for core0 (err=%d)", idleHookErrorCode(res0)); + vSemaphoreDelete(mutex_); + mutex_ = nullptr; + return false; + } + const auto res1 = esp_register_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore1, 1); + hook1 = idleHookRegisteredSuccessfully(res1); + if (!hook1) { + ESP_LOGE(TAG, "Failed to register idle hook for core1 (err=%d)", idleHookErrorCode(res1)); + esp_deregister_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore0, 0); + vSemaphoreDelete(mutex_); + mutex_ = nullptr; + return false; + } #endif - s_instance = this; - - if (config_.sampleIntervalMs > 0) { - esp_timer_create_args_t args = {}; - args.callback = &ESPCpuMonitor::timerCallback; - args.arg = this; - args.dispatch_method = ESP_TIMER_TASK; - args.name = "cpu_monitor"; - - esp_err_t err = esp_timer_create(&args, &timer_); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_timer_create failed: %d", static_cast(err)); - deinit(); - return false; - } - - err = esp_timer_start_periodic(timer_, config_.sampleIntervalMs * 1000); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_timer_start_periodic failed: %d", static_cast(err)); - deinit(); - return false; - } - } - - if (config_.enableTemperature) { - temperatureEnabled_ = initTemperatureSensor(); - if (!temperatureEnabled_) { - ESP_LOGW(TAG, "CPU temperature sensor unavailable; temperature readings disabled"); - } - } - - ESP_LOGI(TAG, "ESPCpuMonitor started (period=%ums, calibration=%u)", - config_.sampleIntervalMs, calibrationSamplesNeeded_); - return true; + s_instance = this; + + if (config_.sampleIntervalMs > 0) { + esp_timer_create_args_t args = {}; + args.callback = &ESPCpuMonitor::timerCallback; + args.arg = this; + args.dispatch_method = ESP_TIMER_TASK; + args.name = "cpu_monitor"; + + esp_err_t err = esp_timer_create(&args, &timer_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_timer_create failed: %d", static_cast(err)); + deinit(); + return false; + } + + err = esp_timer_start_periodic(timer_, config_.sampleIntervalMs * 1000); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_timer_start_periodic failed: %d", static_cast(err)); + deinit(); + return false; + } + } + + if (config_.enableTemperature) { + temperatureEnabled_ = initTemperatureSensor(); + if (!temperatureEnabled_) { + ESP_LOGW(TAG, "CPU temperature sensor unavailable; temperature readings disabled"); + } + } + + ESP_LOGI( + TAG, + "ESPCpuMonitor started (period=%ums, calibration=%u)", + config_.sampleIntervalMs, + calibrationSamplesNeeded_ + ); + return true; } void ESPCpuMonitor::deinit() { - if (s_instance != this) { - return; - } + if (s_instance != this) { + return; + } - if (timer_) { - esp_timer_stop(timer_); - esp_timer_delete(timer_); - timer_ = nullptr; - } + if (timer_) { + esp_timer_stop(timer_); + esp_timer_delete(timer_); + timer_ = nullptr; + } - deinitTemperatureSensor(); + deinitTemperatureSensor(); #if portNUM_PROCESSORS == 1 - esp_deregister_freertos_idle_hook(&ESPCpuMonitor::idleHookCore0); + esp_deregister_freertos_idle_hook(&ESPCpuMonitor::idleHookCore0); #else - esp_deregister_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore0, 0); - esp_deregister_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore1, 1); + esp_deregister_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore0, 0); + esp_deregister_freertos_idle_hook_for_cpu(&ESPCpuMonitor::idleHookCore1, 1); #endif - if (mutex_) { - vSemaphoreDelete(mutex_); - mutex_ = nullptr; - } - - s_instance = nullptr; - calibrated_ = false; - hasSample_ = false; - // Swap with fresh empty containers so capacity is released during teardown. - CpuMonitorDeque(CpuMonitorAllocator(config_.usePSRAMBuffers)).swap(history_); - CpuMonitorVector(CpuMonitorAllocator(config_.usePSRAMBuffers)).swap(callbacks_); - resetSmoothingState(); - resetTemperatureState(); + if (mutex_) { + vSemaphoreDelete(mutex_); + mutex_ = nullptr; + } + + s_instance = nullptr; + calibrated_ = false; + hasSample_ = false; + // Swap with fresh empty containers so capacity is released during teardown. + CpuMonitorDeque(CpuMonitorAllocator(config_.usePSRAMBuffers)) + .swap(history_); + CpuMonitorVector( + CpuMonitorAllocator(config_.usePSRAMBuffers) + ) + .swap(callbacks_); + resetSmoothingState(); + resetTemperatureState(); } bool ESPCpuMonitor::isReady() const { - lock(); - bool ready = calibrated_ && hasSample_; - unlock(); - return ready; + lock(); + bool ready = calibrated_ && hasSample_; + unlock(); + return ready; } bool ESPCpuMonitor::getLastSample(CpuUsageSample &out) const { - if (!isReady()) { - return false; - } - lock(); - out = lastSample_; - unlock(); - return true; + if (!isReady()) { + return false; + } + lock(); + out = lastSample_; + unlock(); + return true; } float ESPCpuMonitor::getLastAverage() const { - CpuUsageSample sample{}; - if (getLastSample(sample)) { - return sample.average; - } - return -1.0f; + CpuUsageSample sample{}; + if (getLastSample(sample)) { + return sample.average; + } + return -1.0f; } float ESPCpuMonitor::getLastSmoothedAverage() const { - CpuUsageSample sample{}; - if (getLastSample(sample) && !std::isnan(sample.smoothedAverage)) { - return sample.smoothedAverage; - } - return -1.0f; + CpuUsageSample sample{}; + if (getLastSample(sample) && !std::isnan(sample.smoothedAverage)) { + return sample.smoothedAverage; + } + return -1.0f; } bool ESPCpuMonitor::getLastTemperature(float ¤tC, float &averageC) const { - if (!config_.enableTemperature) { - return false; - } - CpuUsageSample sample{}; - if (!getLastSample(sample)) { - return false; - } - if (std::isnan(sample.temperatureC) || std::isnan(sample.temperatureAvgC)) { - return false; - } - currentC = sample.temperatureC; - averageC = sample.temperatureAvgC; - return true; + if (!config_.enableTemperature) { + return false; + } + CpuUsageSample sample{}; + if (!getLastSample(sample)) { + return false; + } + if (std::isnan(sample.temperatureC) || std::isnan(sample.temperatureAvgC)) { + return false; + } + currentC = sample.temperatureC; + averageC = sample.temperatureAvgC; + return true; } std::vector ESPCpuMonitor::history() const { - lock(); - std::vector out(history_.begin(), history_.end()); - unlock(); - return out; + lock(); + std::vector out(history_.begin(), history_.end()); + unlock(); + return out; } bool ESPCpuMonitor::sampleNow(CpuUsageSample &out) { - if (!isInitialized()) { - ESP_LOGE(TAG, "Call init() before sampleNow()"); - return false; - } - - CpuMonitorVector callbacks{CpuMonitorAllocator(config_.usePSRAMBuffers)}; - bool ready = captureSample(out, callbacks); - for (const auto &cb : callbacks) { - if (cb) { - cb(out); - } - } - return ready; + if (!isInitialized()) { + ESP_LOGE(TAG, "Call init() before sampleNow()"); + return false; + } + + CpuMonitorVector callbacks{ + CpuMonitorAllocator(config_.usePSRAMBuffers) + }; + bool ready = captureSample(out, callbacks); + for (const auto &cb : callbacks) { + if (cb) { + cb(out); + } + } + return ready; } void ESPCpuMonitor::onSample(CpuSampleCallback cb) { - if (!cb) { - return; - } - lock(); - callbacks_.push_back(cb); - unlock(); + if (!cb) { + return; + } + lock(); + callbacks_.push_back(cb); + unlock(); } bool IRAM_ATTR ESPCpuMonitor::idleHookCore0() { - s_idleCount[0]++; - return true; + s_idleCount[0]++; + return true; } #if portNUM_PROCESSORS > 1 bool IRAM_ATTR ESPCpuMonitor::idleHookCore1() { - s_idleCount[1]++; - return true; + s_idleCount[1]++; + return true; } #endif void ESPCpuMonitor::timerCallback(void *arg) { - auto *self = static_cast(arg); - if (!self || !self->isInitialized()) { - return; - } - CpuUsageSample sample{}; - CpuMonitorVector callbacks{CpuMonitorAllocator(self->config_.usePSRAMBuffers)}; - if (self->captureSample(sample, callbacks)) { - for (const auto &cb : callbacks) { - if (cb) { - cb(sample); - } - } - } + auto *self = static_cast(arg); + if (!self || !self->isInitialized()) { + return; + } + CpuUsageSample sample{}; + CpuMonitorVector callbacks{ + CpuMonitorAllocator(self->config_.usePSRAMBuffers) + }; + if (self->captureSample(sample, callbacks)) { + for (const auto &cb : callbacks) { + if (cb) { + cb(sample); + } + } + } } -bool ESPCpuMonitor::captureSample(CpuUsageSample &out, CpuMonitorVector &callbacksCopy) { - bool ready = false; - lock(); - ready = computeSampleLocked(out); - if (ready) { - lastSample_ = out; - hasSample_ = true; - - if (config_.historySize > 0) { - history_.push_back(out); - if (history_.size() > config_.historySize) { - history_.pop_front(); - } - } - - callbacksCopy = callbacks_; - } - unlock(); - return ready; +bool ESPCpuMonitor::captureSample( + CpuUsageSample &out, CpuMonitorVector &callbacksCopy +) { + bool ready = false; + lock(); + ready = computeSampleLocked(out); + if (ready) { + lastSample_ = out; + hasSample_ = true; + + if (config_.historySize > 0) { + history_.push_back(out); + if (history_.size() > config_.historySize) { + history_.pop_front(); + } + } + + callbacksCopy = callbacks_; + } + unlock(); + return ready; } bool ESPCpuMonitor::computeSampleLocked(CpuUsageSample &out) { - float perCoreUsage[portNUM_PROCESSORS] = {}; - float avg = 0.0f; - uint64_t nowUs = esp_timer_get_time(); - - for (int i = 0; i < portNUM_PROCESSORS; ++i) { - uint64_t currentIdle = s_idleCount[i]; - uint64_t deltaIdle = currentIdle - prevIdle_[i]; - - if (!calibrated_) { - idleBaseline_[i] += static_cast(deltaIdle); - } else { - float baseline = idleBaseline_[i] <= 0.0f ? 1.0f : idleBaseline_[i]; - float idleRatio = baseline > 0.0f ? static_cast(deltaIdle) / baseline : 0.0f; - float usage = 100.0f * (1.0f - idleRatio); - if (usage < 0.0f) usage = 0.0f; - if (usage > 100.0f) usage = 100.0f; - perCoreUsage[i] = usage; - avg += usage; - } - - prevIdle_[i] = currentIdle; - } - - if (!calibrated_) { - calibrationSamplesDone_++; - if (calibrationSamplesDone_ >= calibrationSamplesNeeded_) { - for (int i = 0; i < portNUM_PROCESSORS; ++i) { - idleBaseline_[i] /= static_cast(calibrationSamplesNeeded_); - if (idleBaseline_[i] < 1.0f) { - idleBaseline_[i] = 1.0f; - } - } - calibrated_ = true; - ESP_LOGI(TAG, "CPU usage baseline calibrated after %u samples", calibrationSamplesNeeded_); - } - return false; - } - - avg /= static_cast(portNUM_PROCESSORS); - - out.timestampUs = nowUs; - for (int i = 0; i < portNUM_PROCESSORS; ++i) { - out.perCore[i] = config_.enablePerCore ? perCoreUsage[i] : avg; - } - out.average = avg; - out.smoothedAverage = computeSmoothedAverage(avg); - - float temperature = std::numeric_limits::quiet_NaN(); - float temperatureAvg = std::numeric_limits::quiet_NaN(); - if (readTemperature(temperature)) { - if (temperatureSamples_ == 0) { - temperatureAvg_ = temperature; - } else { - temperatureAvg_ += (temperature - temperatureAvg_) / static_cast(temperatureSamples_ + 1); - } - temperatureSamples_++; - lastTemperature_ = temperature; - temperatureAvg = temperatureAvg_; - } else if (temperatureSamples_ > 0) { - temperatureAvg = temperatureAvg_; - temperature = lastTemperature_; - } - out.temperatureC = temperature; - out.temperatureAvgC = temperatureAvg; - return true; + float perCoreUsage[portNUM_PROCESSORS] = {}; + float avg = 0.0f; + uint64_t nowUs = esp_timer_get_time(); + + for (int i = 0; i < portNUM_PROCESSORS; ++i) { + uint64_t currentIdle = s_idleCount[i]; + uint64_t deltaIdle = currentIdle - prevIdle_[i]; + + if (!calibrated_) { + idleBaseline_[i] += static_cast(deltaIdle); + } else { + float baseline = idleBaseline_[i] <= 0.0f ? 1.0f : idleBaseline_[i]; + float idleRatio = baseline > 0.0f ? static_cast(deltaIdle) / baseline : 0.0f; + float usage = 100.0f * (1.0f - idleRatio); + if (usage < 0.0f) + usage = 0.0f; + if (usage > 100.0f) + usage = 100.0f; + perCoreUsage[i] = usage; + avg += usage; + } + + prevIdle_[i] = currentIdle; + } + + if (!calibrated_) { + calibrationSamplesDone_++; + if (calibrationSamplesDone_ >= calibrationSamplesNeeded_) { + for (int i = 0; i < portNUM_PROCESSORS; ++i) { + idleBaseline_[i] /= static_cast(calibrationSamplesNeeded_); + if (idleBaseline_[i] < 1.0f) { + idleBaseline_[i] = 1.0f; + } + } + calibrated_ = true; + ESP_LOGI( + TAG, + "CPU usage baseline calibrated after %u samples", + calibrationSamplesNeeded_ + ); + } + return false; + } + + avg /= static_cast(portNUM_PROCESSORS); + + out.timestampUs = nowUs; + for (int i = 0; i < portNUM_PROCESSORS; ++i) { + out.perCore[i] = config_.enablePerCore ? perCoreUsage[i] : avg; + } + out.average = avg; + out.smoothedAverage = computeSmoothedAverage(avg); + + float temperature = std::numeric_limits::quiet_NaN(); + float temperatureAvg = std::numeric_limits::quiet_NaN(); + if (readTemperature(temperature)) { + if (temperatureSamples_ == 0) { + temperatureAvg_ = temperature; + } else { + temperatureAvg_ += + (temperature - temperatureAvg_) / static_cast(temperatureSamples_ + 1); + } + temperatureSamples_++; + lastTemperature_ = temperature; + temperatureAvg = temperatureAvg_; + } else if (temperatureSamples_ > 0) { + temperatureAvg = temperatureAvg_; + temperature = lastTemperature_; + } + out.temperatureC = temperature; + out.temperatureAvgC = temperatureAvg; + return true; } #if ESPCM_HAS_ARDUINOJSON void ESPCpuMonitor::toJson(const CpuUsageSample &sample, JsonDocument &doc) const { - doc["ts"] = sample.timestampUs; - auto coreArr = doc["core"].to(); - for (size_t i = 0; i < portNUM_PROCESSORS; ++i) { - coreArr.add(sample.perCore[i]); - } - doc["avg"] = sample.average; - if (!std::isnan(sample.smoothedAverage)) { - doc["avgSmoothed"] = sample.smoothedAverage; - } - if (!std::isnan(sample.temperatureC)) { - doc["temp"] = sample.temperatureC; - } - if (!std::isnan(sample.temperatureAvgC)) { - doc["tempAvg"] = sample.temperatureAvgC; - } + doc["ts"] = sample.timestampUs; + auto coreArr = doc["core"].to(); + for (size_t i = 0; i < portNUM_PROCESSORS; ++i) { + coreArr.add(sample.perCore[i]); + } + doc["avg"] = sample.average; + if (!std::isnan(sample.smoothedAverage)) { + doc["avgSmoothed"] = sample.smoothedAverage; + } + if (!std::isnan(sample.temperatureC)) { + doc["temp"] = sample.temperatureC; + } + if (!std::isnan(sample.temperatureAvgC)) { + doc["tempAvg"] = sample.temperatureAvgC; + } } #endif bool ESPCpuMonitor::initTemperatureSensor() { #if ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_NEW - temperature_sensor_config_t cfg{}; + temperature_sensor_config_t cfg{}; #if defined(TEMPERATURE_SENSOR_CONFIG_DEFAULT) && defined(TEMPERATURE_SENSOR_CLK_SRC_DEFAULT) - cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); + cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); #elif defined(TEMPERATURE_SENSOR_CONFIG_DEFAULT) - // Some Arduino ESP32 builds expose the macro but not the clock source enum; set only the range. - cfg.range_min = -10; - cfg.range_max = 80; + // Some Arduino ESP32 builds expose the macro but not the clock source enum; set only the range. + cfg.range_min = -10; + cfg.range_max = 80; #else - cfg.range_min = -10; - cfg.range_max = 80; + cfg.range_min = -10; + cfg.range_max = 80; #endif - esp_err_t err = temperature_sensor_install(&cfg, &tempSensor_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "temperature_sensor_install failed: %d", static_cast(err)); - tempSensor_ = nullptr; - return false; - } - err = temperature_sensor_enable(tempSensor_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "temperature_sensor_enable failed: %d", static_cast(err)); - temperature_sensor_uninstall(tempSensor_); - tempSensor_ = nullptr; - return false; - } - return true; + esp_err_t err = temperature_sensor_install(&cfg, &tempSensor_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "temperature_sensor_install failed: %d", static_cast(err)); + tempSensor_ = nullptr; + return false; + } + err = temperature_sensor_enable(tempSensor_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "temperature_sensor_enable failed: %d", static_cast(err)); + temperature_sensor_uninstall(tempSensor_); + tempSensor_ = nullptr; + return false; + } + return true; #elif ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_OLD - temp_sensor_config_t cfg = TSENS_CONFIG_DEFAULT(); - esp_err_t err = temp_sensor_set_config(cfg); - if (err != ESP_OK) { - ESP_LOGW(TAG, "temp_sensor_set_config failed: %d", static_cast(err)); - return false; - } - err = temp_sensor_start(); - if (err != ESP_OK) { - ESP_LOGW(TAG, "temp_sensor_start failed: %d", static_cast(err)); - return false; - } - tempSensorStarted_ = true; - return true; -#else // ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_NEW - return false; + temp_sensor_config_t cfg = TSENS_CONFIG_DEFAULT(); + esp_err_t err = temp_sensor_set_config(cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "temp_sensor_set_config failed: %d", static_cast(err)); + return false; + } + err = temp_sensor_start(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "temp_sensor_start failed: %d", static_cast(err)); + return false; + } + tempSensorStarted_ = true; + return true; +#else // ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_NEW + return false; #endif } void ESPCpuMonitor::deinitTemperatureSensor() { #if ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_NEW - if (tempSensor_) { - temperature_sensor_disable(tempSensor_); - temperature_sensor_uninstall(tempSensor_); - tempSensor_ = nullptr; - } + if (tempSensor_) { + temperature_sensor_disable(tempSensor_); + temperature_sensor_uninstall(tempSensor_); + tempSensor_ = nullptr; + } #elif ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_OLD - if (tempSensorStarted_) { - temp_sensor_stop(); - tempSensorStarted_ = false; - } + if (tempSensorStarted_) { + temp_sensor_stop(); + tempSensorStarted_ = false; + } #endif - temperatureEnabled_ = false; - resetTemperatureState(); + temperatureEnabled_ = false; + resetTemperatureState(); } bool ESPCpuMonitor::readTemperature(float &outC) { - if (!config_.enableTemperature || !temperatureEnabled_) { - return false; - } + if (!config_.enableTemperature || !temperatureEnabled_) { + return false; + } #if ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_NEW - if (!tempSensor_) { - return false; - } - float val = 0.0f; - if (temperature_sensor_get_celsius(tempSensor_, &val) == ESP_OK) { - outC = val; - return true; - } + if (!tempSensor_) { + return false; + } + float val = 0.0f; + if (temperature_sensor_get_celsius(tempSensor_, &val) == ESP_OK) { + outC = val; + return true; + } #elif ESPCM_TEMP_SENSOR_AVAILABLE && ESPCM_HAS_TEMP_SENSOR_OLD - float val = 0.0f; - if (temp_sensor_read_celsius(&val) == ESP_OK) { - outC = val; - return true; - } + float val = 0.0f; + if (temp_sensor_read_celsius(&val) == ESP_OK) { + outC = val; + return true; + } #endif - return false; + return false; } void ESPCpuMonitor::resetSmoothingState() { - smoothingWindowValues_.fill(0.0f); - smoothingWindowSize_ = config_.smoothingWindow; - if (smoothingWindowSize_ == 0) { - smoothingWindowSize_ = 1; - } - smoothingCount_ = 0; - smoothingIndex_ = 0; - smoothingSum_ = 0.0f; - ewmaState_ = std::numeric_limits::quiet_NaN(); + smoothingWindowValues_.fill(0.0f); + smoothingWindowSize_ = config_.smoothingWindow; + if (smoothingWindowSize_ == 0) { + smoothingWindowSize_ = 1; + } + smoothingCount_ = 0; + smoothingIndex_ = 0; + smoothingSum_ = 0.0f; + ewmaState_ = std::numeric_limits::quiet_NaN(); } float ESPCpuMonitor::computeSmoothedAverage(float rawAverage) { - if (config_.smoothingMode == CpuSmoothingMode::None) { - return std::numeric_limits::quiet_NaN(); - } - - if (config_.smoothingMode == CpuSmoothingMode::RollingMean) { - if (smoothingCount_ < smoothingWindowSize_) { - smoothingCount_++; - } else { - smoothingSum_ -= smoothingWindowValues_[smoothingIndex_]; - } - - smoothingWindowValues_[smoothingIndex_] = rawAverage; - smoothingSum_ += rawAverage; - smoothingIndex_ = static_cast((smoothingIndex_ + 1) % smoothingWindowSize_); - return smoothingSum_ / static_cast(smoothingCount_); - } - - if (std::isnan(ewmaState_)) { - ewmaState_ = rawAverage; - } else { - ewmaState_ += config_.smoothingAlpha * (rawAverage - ewmaState_); - } - return ewmaState_; + if (config_.smoothingMode == CpuSmoothingMode::None) { + return std::numeric_limits::quiet_NaN(); + } + + if (config_.smoothingMode == CpuSmoothingMode::RollingMean) { + if (smoothingCount_ < smoothingWindowSize_) { + smoothingCount_++; + } else { + smoothingSum_ -= smoothingWindowValues_[smoothingIndex_]; + } + + smoothingWindowValues_[smoothingIndex_] = rawAverage; + smoothingSum_ += rawAverage; + smoothingIndex_ = static_cast((smoothingIndex_ + 1) % smoothingWindowSize_); + return smoothingSum_ / static_cast(smoothingCount_); + } + + if (std::isnan(ewmaState_)) { + ewmaState_ = rawAverage; + } else { + ewmaState_ += config_.smoothingAlpha * (rawAverage - ewmaState_); + } + return ewmaState_; } void ESPCpuMonitor::resetTemperatureState() { - lastTemperature_ = std::numeric_limits::quiet_NaN(); - temperatureAvg_ = std::numeric_limits::quiet_NaN(); - temperatureSamples_ = 0; + lastTemperature_ = std::numeric_limits::quiet_NaN(); + temperatureAvg_ = std::numeric_limits::quiet_NaN(); + temperatureSamples_ = 0; } diff --git a/src/esp_cpu_monitor/cpu_monitor.h b/src/esp_cpu_monitor/cpu_monitor.h index b3867b6..01ae400 100644 --- a/src/esp_cpu_monitor/cpu_monitor.h +++ b/src/esp_cpu_monitor/cpu_monitor.h @@ -33,10 +33,11 @@ extern "C" { #include } -#include #include +#include -// Prefer the new temperature sensor driver when available; consumers can force the legacy driver if needed. +// Prefer the new temperature sensor driver when available; consumers can force the legacy driver if +// needed. #if defined(__has_include) #if defined(ESPCM_FORCE_LEGACY_TEMP_SENSOR) #if __has_include() @@ -84,111 +85,113 @@ extern "C" { #endif enum class CpuSmoothingMode : uint8_t { - None = 0, - RollingMean = 1, - Ewma = 2, + None = 0, + RollingMean = 1, + Ewma = 2, }; struct CpuMonitorConfig { - uint32_t sampleIntervalMs = 1000; // Periodic sampling interval (0 = manual only) - uint32_t calibrationSamples = 5; // Baseline windows that represent 100% idle - size_t historySize = 60; // Ring buffer depth (0 = disable history) - bool usePSRAMBuffers = false; // Prefer PSRAM for internal dynamic buffers when available - bool enablePerCore = true; // When false, only average is reported - bool enableTemperature = true; // When false, skip temperature readings - CpuSmoothingMode smoothingMode = CpuSmoothingMode::None; // Optional average smoothing - uint8_t smoothingWindow = 5; // Rolling mean window (1..ESPCM_MAX_SMOOTHING_WINDOW) - float smoothingAlpha = 0.2f; // EWMA alpha (0.0, 1.0] + uint32_t sampleIntervalMs = 1000; // Periodic sampling interval (0 = manual only) + uint32_t calibrationSamples = 5; // Baseline windows that represent 100% idle + size_t historySize = 60; // Ring buffer depth (0 = disable history) + bool usePSRAMBuffers = false; // Prefer PSRAM for internal dynamic buffers when available + bool enablePerCore = true; // When false, only average is reported + bool enableTemperature = true; // When false, skip temperature readings + CpuSmoothingMode smoothingMode = CpuSmoothingMode::None; // Optional average smoothing + uint8_t smoothingWindow = 5; // Rolling mean window (1..ESPCM_MAX_SMOOTHING_WINDOW) + float smoothingAlpha = 0.2f; // EWMA alpha (0.0, 1.0] }; struct CpuUsageSample { - uint64_t timestampUs = 0; - float perCore[portNUM_PROCESSORS]{}; - float average = 0.0f; - float smoothedAverage = std::numeric_limits::quiet_NaN(); - float temperatureC = std::numeric_limits::quiet_NaN(); - float temperatureAvgC = std::numeric_limits::quiet_NaN(); + uint64_t timestampUs = 0; + float perCore[portNUM_PROCESSORS]{}; + float average = 0.0f; + float smoothedAverage = std::numeric_limits::quiet_NaN(); + float temperatureC = std::numeric_limits::quiet_NaN(); + float temperatureAvgC = std::numeric_limits::quiet_NaN(); }; -using CpuSampleCallback = std::function; +using CpuSampleCallback = std::function; class ESPCpuMonitor { -public: - ESPCpuMonitor(); - ~ESPCpuMonitor(); + public: + ESPCpuMonitor(); + ~ESPCpuMonitor(); - ESPCpuMonitor(const ESPCpuMonitor&) = delete; - ESPCpuMonitor& operator=(const ESPCpuMonitor&) = delete; - ESPCpuMonitor(ESPCpuMonitor&&) = delete; - ESPCpuMonitor& operator=(ESPCpuMonitor&&) = delete; + ESPCpuMonitor(const ESPCpuMonitor &) = delete; + ESPCpuMonitor &operator=(const ESPCpuMonitor &) = delete; + ESPCpuMonitor(ESPCpuMonitor &&) = delete; + ESPCpuMonitor &operator=(ESPCpuMonitor &&) = delete; - bool init(const CpuMonitorConfig &cfg = {}); - void deinit(); - bool isInitialized() const { return s_instance == this; } + bool init(const CpuMonitorConfig &cfg = {}); + void deinit(); + bool isInitialized() const { + return s_instance == this; + } - bool isReady() const; - bool getLastSample(CpuUsageSample &out) const; - float getLastAverage() const; - float getLastSmoothedAverage() const; - bool getLastTemperature(float ¤tC, float &averageC) const; - std::vector history() const; + bool isReady() const; + bool getLastSample(CpuUsageSample &out) const; + float getLastAverage() const; + float getLastSmoothedAverage() const; + bool getLastTemperature(float ¤tC, float &averageC) const; + std::vector history() const; - // Trigger sampling immediately (useful when sampleIntervalMs == 0) - bool sampleNow(CpuUsageSample &out); + // Trigger sampling immediately (useful when sampleIntervalMs == 0) + bool sampleNow(CpuUsageSample &out); - void onSample(CpuSampleCallback cb); + void onSample(CpuSampleCallback cb); #if ESPCM_HAS_ARDUINOJSON - void toJson(const CpuUsageSample &sample, JsonDocument &doc) const; + void toJson(const CpuUsageSample &sample, JsonDocument &doc) const; #endif -private: - static bool IRAM_ATTR idleHookCore0(); + private: + static bool IRAM_ATTR idleHookCore0(); #if portNUM_PROCESSORS > 1 - static bool IRAM_ATTR idleHookCore1(); -#endif - static void timerCallback(void *arg); - - void resetState(const CpuMonitorConfig &cfg); - bool computeSampleLocked(CpuUsageSample &out); - bool captureSample(CpuUsageSample &out, CpuMonitorVector &callbacksCopy); - bool initTemperatureSensor(); - void deinitTemperatureSensor(); - bool readTemperature(float &outC); - void resetSmoothingState(); - float computeSmoothedAverage(float rawAverage); - void resetTemperatureState(); - void lock() const; - void unlock() const; - - static volatile uint64_t s_idleCount[portNUM_PROCESSORS]; - static ESPCpuMonitor *s_instance; - - CpuMonitorConfig config_{}; - uint64_t prevIdle_[portNUM_PROCESSORS]; - float idleBaseline_[portNUM_PROCESSORS]; - CpuUsageSample lastSample_{}; - bool hasSample_ = false; - bool calibrated_ = false; - uint32_t calibrationSamplesNeeded_ = 0; - uint32_t calibrationSamplesDone_ = 0; - esp_timer_handle_t timer_ = nullptr; - mutable SemaphoreHandle_t mutex_ = nullptr; - CpuMonitorDeque history_; - CpuMonitorVector callbacks_; - std::array smoothingWindowValues_{}; - uint8_t smoothingWindowSize_ = 1; - uint8_t smoothingCount_ = 0; - uint8_t smoothingIndex_ = 0; - float smoothingSum_ = 0.0f; - float ewmaState_ = std::numeric_limits::quiet_NaN(); - float lastTemperature_ = std::numeric_limits::quiet_NaN(); - float temperatureAvg_ = std::numeric_limits::quiet_NaN(); - uint32_t temperatureSamples_ = 0; - bool temperatureEnabled_ = false; + static bool IRAM_ATTR idleHookCore1(); +#endif + static void timerCallback(void *arg); + + void resetState(const CpuMonitorConfig &cfg); + bool computeSampleLocked(CpuUsageSample &out); + bool captureSample(CpuUsageSample &out, CpuMonitorVector &callbacksCopy); + bool initTemperatureSensor(); + void deinitTemperatureSensor(); + bool readTemperature(float &outC); + void resetSmoothingState(); + float computeSmoothedAverage(float rawAverage); + void resetTemperatureState(); + void lock() const; + void unlock() const; + + static volatile uint64_t s_idleCount[portNUM_PROCESSORS]; + static ESPCpuMonitor *s_instance; + + CpuMonitorConfig config_{}; + uint64_t prevIdle_[portNUM_PROCESSORS]; + float idleBaseline_[portNUM_PROCESSORS]; + CpuUsageSample lastSample_{}; + bool hasSample_ = false; + bool calibrated_ = false; + uint32_t calibrationSamplesNeeded_ = 0; + uint32_t calibrationSamplesDone_ = 0; + esp_timer_handle_t timer_ = nullptr; + mutable SemaphoreHandle_t mutex_ = nullptr; + CpuMonitorDeque history_; + CpuMonitorVector callbacks_; + std::array smoothingWindowValues_{}; + uint8_t smoothingWindowSize_ = 1; + uint8_t smoothingCount_ = 0; + uint8_t smoothingIndex_ = 0; + float smoothingSum_ = 0.0f; + float ewmaState_ = std::numeric_limits::quiet_NaN(); + float lastTemperature_ = std::numeric_limits::quiet_NaN(); + float temperatureAvg_ = std::numeric_limits::quiet_NaN(); + uint32_t temperatureSamples_ = 0; + bool temperatureEnabled_ = false; #if ESPCM_HAS_TEMP_SENSOR_NEW - temperature_sensor_handle_t tempSensor_ = nullptr; + temperature_sensor_handle_t tempSensor_ = nullptr; #elif ESPCM_HAS_TEMP_SENSOR_OLD - bool tempSensorStarted_ = false; + bool tempSensorStarted_ = false; #endif }; diff --git a/src/esp_cpu_monitor/cpu_monitor_allocator.h b/src/esp_cpu_monitor/cpu_monitor_allocator.h index 1a4d603..65d96af 100644 --- a/src/esp_cpu_monitor/cpu_monitor_allocator.h +++ b/src/esp_cpu_monitor/cpu_monitor_allocator.h @@ -20,86 +20,84 @@ namespace cpu_monitor_allocator_detail { inline void *allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { #if ESP_CPU_MONITOR_HAS_BUFFER_MANAGER - return ESPBufferManager::allocate(bytes, usePSRAMBuffers); + return ESPBufferManager::allocate(bytes, usePSRAMBuffers); #else - (void)usePSRAMBuffers; - return std::malloc(bytes); + (void)usePSRAMBuffers; + return std::malloc(bytes); #endif } inline void deallocate(void *ptr) noexcept { #if ESP_CPU_MONITOR_HAS_BUFFER_MANAGER - ESPBufferManager::deallocate(ptr); + ESPBufferManager::deallocate(ptr); #else - std::free(ptr); + std::free(ptr); #endif } -} // namespace cpu_monitor_allocator_detail - -template -class CpuMonitorAllocator { - public: - using value_type = T; - using propagate_on_container_copy_assignment = std::true_type; - using propagate_on_container_move_assignment = std::true_type; - using propagate_on_container_swap = std::true_type; - - CpuMonitorAllocator() noexcept = default; - explicit CpuMonitorAllocator(bool usePSRAMBuffers) noexcept : usePSRAMBuffers_(usePSRAMBuffers) {} - - template - CpuMonitorAllocator(const CpuMonitorAllocator &other) noexcept : usePSRAMBuffers_(other.usePSRAMBuffers()) {} - - T *allocate(std::size_t n) { - if (n == 0) { - return nullptr; - } - if (n > (std::numeric_limits::max() / sizeof(T))) { +} // namespace cpu_monitor_allocator_detail + +template class CpuMonitorAllocator { + public: + using value_type = T; + using propagate_on_container_copy_assignment = std::true_type; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + + CpuMonitorAllocator() noexcept = default; + explicit CpuMonitorAllocator(bool usePSRAMBuffers) noexcept + : usePSRAMBuffers_(usePSRAMBuffers) { + } + + template + CpuMonitorAllocator(const CpuMonitorAllocator &other) noexcept + : usePSRAMBuffers_(other.usePSRAMBuffers()) { + } + + T *allocate(std::size_t n) { + if (n == 0) { + return nullptr; + } + if (n > (std::numeric_limits::max() / sizeof(T))) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } + } - void *memory = cpu_monitor_allocator_detail::allocate(n * sizeof(T), usePSRAMBuffers_); - if (memory == nullptr) { + void *memory = cpu_monitor_allocator_detail::allocate(n * sizeof(T), usePSRAMBuffers_); + if (memory == nullptr) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } - return static_cast(memory); - } + } + return static_cast(memory); + } - void deallocate(T *ptr, std::size_t) noexcept { - cpu_monitor_allocator_detail::deallocate(ptr); - } + void deallocate(T *ptr, std::size_t) noexcept { + cpu_monitor_allocator_detail::deallocate(ptr); + } - bool usePSRAMBuffers() const noexcept { - return usePSRAMBuffers_; - } + bool usePSRAMBuffers() const noexcept { + return usePSRAMBuffers_; + } - template - bool operator==(const CpuMonitorAllocator &other) const noexcept { - return usePSRAMBuffers_ == other.usePSRAMBuffers(); - } + template bool operator==(const CpuMonitorAllocator &other) const noexcept { + return usePSRAMBuffers_ == other.usePSRAMBuffers(); + } - template - bool operator!=(const CpuMonitorAllocator &other) const noexcept { - return !(*this == other); - } + template bool operator!=(const CpuMonitorAllocator &other) const noexcept { + return !(*this == other); + } - private: - template - friend class CpuMonitorAllocator; + private: + template friend class CpuMonitorAllocator; - bool usePSRAMBuffers_ = false; + bool usePSRAMBuffers_ = false; }; -template -using CpuMonitorDeque = std::deque>; +template using CpuMonitorDeque = std::deque>; -template -using CpuMonitorVector = std::vector>; +template using CpuMonitorVector = std::vector>; diff --git a/test/test_cpu_monitor/test_cpu_monitor.cpp b/test/test_cpu_monitor/test_cpu_monitor.cpp index 145075f..15fc61d 100644 --- a/test/test_cpu_monitor/test_cpu_monitor.cpp +++ b/test/test_cpu_monitor/test_cpu_monitor.cpp @@ -5,77 +5,79 @@ namespace { CpuMonitorConfig testConfig() { - CpuMonitorConfig cfg{}; - cfg.sampleIntervalMs = 0; - cfg.calibrationSamples = 1; - cfg.historySize = 4; - cfg.enablePerCore = true; - cfg.enableTemperature = false; - cfg.smoothingMode = CpuSmoothingMode::None; - return cfg; + CpuMonitorConfig cfg{}; + cfg.sampleIntervalMs = 0; + cfg.calibrationSamples = 1; + cfg.historySize = 4; + cfg.enablePerCore = true; + cfg.enableTemperature = false; + cfg.smoothingMode = CpuSmoothingMode::None; + return cfg; } void test_deinit_is_safe_before_init() { - ESPCpuMonitor monitor; - TEST_ASSERT_FALSE(monitor.isInitialized()); + ESPCpuMonitor monitor; + TEST_ASSERT_FALSE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); } void test_deinit_is_idempotent() { - ESPCpuMonitor monitor; - TEST_ASSERT_TRUE(monitor.init(testConfig())); - TEST_ASSERT_TRUE(monitor.isInitialized()); + ESPCpuMonitor monitor; + TEST_ASSERT_TRUE(monitor.init(testConfig())); + TEST_ASSERT_TRUE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); } void test_reinit_after_deinit() { - ESPCpuMonitor monitor; - TEST_ASSERT_TRUE(monitor.init(testConfig())); - TEST_ASSERT_TRUE(monitor.isInitialized()); + ESPCpuMonitor monitor; + TEST_ASSERT_TRUE(monitor.init(testConfig())); + TEST_ASSERT_TRUE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); - TEST_ASSERT_TRUE(monitor.init(testConfig())); - TEST_ASSERT_TRUE(monitor.isInitialized()); - monitor.deinit(); + TEST_ASSERT_TRUE(monitor.init(testConfig())); + TEST_ASSERT_TRUE(monitor.isInitialized()); + monitor.deinit(); } void test_destructor_deinits_active_instance() { - { - ESPCpuMonitor first; - TEST_ASSERT_TRUE(first.init(testConfig())); - TEST_ASSERT_TRUE(first.isInitialized()); - } - - ESPCpuMonitor second; - TEST_ASSERT_TRUE(second.init(testConfig())); - TEST_ASSERT_TRUE(second.isInitialized()); - second.deinit(); + { + ESPCpuMonitor first; + TEST_ASSERT_TRUE(first.init(testConfig())); + TEST_ASSERT_TRUE(first.isInitialized()); + } + + ESPCpuMonitor second; + TEST_ASSERT_TRUE(second.init(testConfig())); + TEST_ASSERT_TRUE(second.isInitialized()); + second.deinit(); } -} // namespace +} // namespace -void setUp() {} -void tearDown() {} +void setUp() { +} +void tearDown() { +} void setup() { - delay(2000); - UNITY_BEGIN(); - RUN_TEST(test_deinit_is_safe_before_init); - RUN_TEST(test_deinit_is_idempotent); - RUN_TEST(test_reinit_after_deinit); - RUN_TEST(test_destructor_deinits_active_instance); - UNITY_END(); + delay(2000); + UNITY_BEGIN(); + RUN_TEST(test_deinit_is_safe_before_init); + RUN_TEST(test_deinit_is_idempotent); + RUN_TEST(test_reinit_after_deinit); + RUN_TEST(test_destructor_deinits_active_instance); + UNITY_END(); } void loop() { - delay(1000); + delay(1000); }