From d2b473340645b06c3aaa294336b3c72f75c81c37 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Sun, 22 Feb 2026 13:09:34 -0500 Subject: [PATCH 1/2] feat: add stats-extpower CLI command for INA3221 power monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a serial-only `stats-extpower` command that returns per-channel voltage and current readings from the INA3221 external power monitoring board as JSON. Returns an error message if no supported hardware is detected. Changes: - New stats-extpower CLI command in CommonCLI.cpp - formatExtPowerStatsReply virtual method added through the callback chain (CommonCLI.h → MyMesh) - formatExtPowerStats implementation in EnvironmentSensorManager that reads INA3221 channels - CLI documentation update --- docs/cli_commands.md | 11 ++++++++ examples/simple_repeater/MyMesh.cpp | 8 +++++- examples/simple_repeater/MyMesh.h | 1 + src/helpers/CommonCLI.cpp | 2 ++ src/helpers/CommonCLI.h | 3 +++ src/helpers/SensorManager.h | 1 + .../sensors/EnvironmentSensorManager.cpp | 25 +++++++++++++++++++ .../sensors/EnvironmentSensorManager.h | 1 + 8 files changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index c316bd6c7..903641b71 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -127,6 +127,17 @@ --- +### External Power Monitoring Stats - Per-channel voltage and current +**Usage:** `stats-extpower` + +**Serial Only:** Yes + +**Note:** Returns JSON with only enabled channels, e.g. `{"ch1_voltage_mv":12450,"ch1_current_ma":150,"ch2_voltage_mv":3800,"ch2_current_ma":85}` + +**Note:** Returns "No external power monitoring board detected" if no supported hardware is present + +--- + ## Logging ### Begin capture of rx log to node storage diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b6d855f68..80b109721 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1015,10 +1015,16 @@ void MyMesh::formatRadioStatsReply(char *reply) { } void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), getNumRecvFlood(), getNumRecvDirect()); } +void MyMesh::formatExtPowerStatsReply(char *reply) { + if (!sensors.formatExtPowerStats(reply)) { + strcpy(reply, "No external power monitoring board detected"); + } +} + void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) IdentityStore store(*_fs, ""); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index f0e7cc10e..77de66b60 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -207,6 +207,7 @@ public: void formatStatsReply(char *reply) override; void formatRadioStatsReply(char *reply) override; void formatPacketStatsReply(char *reply) override; + void formatExtPowerStatsReply(char *reply) override; mesh::LocalIdentity& getSelfId() override { return self_id; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 263eb6652..16bc9d870 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -789,6 +789,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _callbacks->formatRadioStatsReply(reply); } else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) { _callbacks->formatStatsReply(reply); + } else if (sender_timestamp == 0 && memcmp(command, "stats-extpower", 14) == 0 && (command[14] == 0 || command[14] == ' ')) { + _callbacks->formatExtPowerStatsReply(reply); } else { strcpy(reply, "Unknown command"); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 146e1c6e2..4eda9e19d 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -75,6 +75,9 @@ public: virtual void formatStatsReply(char *reply) = 0; virtual void formatRadioStatsReply(char *reply) = 0; virtual void formatPacketStatsReply(char *reply) = 0; + virtual void formatExtPowerStatsReply(char *reply) { + strcpy(reply, "No external power monitoring board detected"); + }; virtual mesh::LocalIdentity& getSelfId() = 0; virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c22..062be546f 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -23,6 +23,7 @@ public: virtual const char* getSettingValue(int i) const { return NULL; } virtual bool setSettingValue(const char* name, const char* value) { return false; } virtual LocationProvider* getLocationProvider() { return NULL; } + virtual bool formatExtPowerStats(char* reply) { return false; } // Helper functions to manage setting by keys (useful in many places ...) const char* getSettingByKey(const char* key) { diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index a75d378c8..df1fe9dca 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -489,6 +489,31 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen } +bool EnvironmentSensorManager::formatExtPowerStats(char* reply) { +#if ENV_INCLUDE_INA3221 + if (!INA3221_initialized) return false; + char* dp = reply; + *dp++ = '{'; + bool first = true; + for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) { + if (INA3221.isChannelEnabled(i)) { + int ch = i + 1; + int voltage_mv = (int)(INA3221.getBusVoltage(i) * 1000); + int current_ma = (int)(INA3221.getCurrentAmps(i) * 1000); + if (!first) *dp++ = ','; + sprintf(dp, "\"ch%d_voltage_mv\":%d,\"ch%d_current_ma\":%d", ch, voltage_mv, ch, current_ma); + dp = strchr(dp, 0); + first = false; + } + } + *dp++ = '}'; + *dp = 0; + return true; +#else + return false; +#endif +} + int EnvironmentSensorManager::getNumSettings() const { int settings = 0; #if ENV_INCLUDE_GPS diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index f176a33f5..9c2a4cb1c 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -55,4 +55,5 @@ public: const char* getSettingName(int i) const override; const char* getSettingValue(int i) const override; bool setSettingValue(const char* name, const char* value) override; + bool formatExtPowerStats(char* reply) override; }; From 76968c4d799bb54ae2c9b6d2fd0861af49d56b89 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Sun, 22 Feb 2026 21:11:24 -0500 Subject: [PATCH 2/2] fix: wrap error in JSON and add reply buffer guard --- examples/simple_repeater/MyMesh.cpp | 2 +- src/helpers/CommonCLI.h | 2 +- src/helpers/sensors/EnvironmentSensorManager.cpp | 10 +++++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 80b109721..1ba972e28 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1021,7 +1021,7 @@ void MyMesh::formatPacketStatsReply(char *reply) { void MyMesh::formatExtPowerStatsReply(char *reply) { if (!sensors.formatExtPowerStats(reply)) { - strcpy(reply, "No external power monitoring board detected"); + strcpy(reply, "{\"err\":\"No external power monitoring board detected\"}"); } } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 4eda9e19d..de40d31cb 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -76,7 +76,7 @@ public: virtual void formatRadioStatsReply(char *reply) = 0; virtual void formatPacketStatsReply(char *reply) = 0; virtual void formatExtPowerStatsReply(char *reply) { - strcpy(reply, "No external power monitoring board detected"); + strcpy(reply, "{\"err\":\"No external power monitoring board detected\"}"); }; virtual mesh::LocalIdentity& getSelfId() = 0; virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index df1fe9dca..c38a121a3 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -493,16 +493,20 @@ bool EnvironmentSensorManager::formatExtPowerStats(char* reply) { #if ENV_INCLUDE_INA3221 if (!INA3221_initialized) return false; char* dp = reply; + int remaining = 160; // max reply buffer length *dp++ = '{'; + remaining--; bool first = true; for (int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) { if (INA3221.isChannelEnabled(i)) { int ch = i + 1; int voltage_mv = (int)(INA3221.getBusVoltage(i) * 1000); int current_ma = (int)(INA3221.getCurrentAmps(i) * 1000); - if (!first) *dp++ = ','; - sprintf(dp, "\"ch%d_voltage_mv\":%d,\"ch%d_current_ma\":%d", ch, voltage_mv, ch, current_ma); - dp = strchr(dp, 0); + if (!first) { *dp++ = ','; remaining--; } + int n = snprintf(dp, remaining, "\"ch%d_voltage_mv\":%d,\"ch%d_current_ma\":%d", ch, voltage_mv, ch, current_ma); + if (n >= remaining) break; + dp += n; + remaining -= n; first = false; } }