diff --git a/docs/custom-cli.md b/docs/custom-cli.md index 1c16403b..18df9bf4 100644 --- a/docs/custom-cli.md +++ b/docs/custom-cli.md @@ -88,6 +88,7 @@ Legacy dotted aliases are also accepted: - `get battery.reporting`: shows whether board battery reporting is enabled. Support is board-dependent. - `set battery.reporting on|off`: enables or disables battery voltage reporting on supported boards. This is currently useful for Heltec V3 boards where USB-only power can produce misleading battery readings. If your board needs this too, open an issue and support can be added board-by-board. +- On repeater MQTT builds, background battery sampling used for MQTT/status history is rate-limited to about once per minute. Explicit status and telemetry requests still refresh the reading immediately. ### T-Beam 1W Fan Control diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 1ca1e9fc..585d9177 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -28,44 +28,14 @@ namespace { -struct WebSensorSnapshot { - bool has_supply_voltage = false; - float supply_voltage_v = NAN; - - bool has_sensor_temp = false; - float sensor_temp_c = NAN; - - bool has_mcu_temp = false; - float mcu_temp_c = NAN; - - bool has_humidity = false; - float humidity_pct = NAN; - - bool has_pressure = false; - float pressure_hpa = NAN; - - bool has_pressure_altitude = false; - float pressure_altitude_m = NAN; - - bool has_gps = false; - bool gps_enabled = false; - bool gps_fix = false; - bool has_gps_lat = false; - float gps_lat = NAN; - bool has_gps_lon = false; - float gps_lon = NAN; - bool has_gps_altitude = false; - float gps_altitude_m = NAN; - bool has_satellites = false; - long satellites = 0; -}; - -WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager& sensors) { +WebSensorSnapshot collectWebSensorSnapshot(mesh::MainBoard& board, SensorManager& sensors, uint16_t battery_mv) { WebSensorSnapshot snapshot; + snapshot.has_battery = true; + snapshot.battery_mv = battery_mv; CayenneLPP sensor_telemetry(200); sensor_telemetry.reset(); - sensor_telemetry.addVoltage(TELEM_CHANNEL_SELF, static_cast(board.getBattMilliVolts()) / 1000.0f); + sensor_telemetry.addVoltage(TELEM_CHANNEL_SELF, static_cast(battery_mv) / 1000.0f); sensors.querySensors(0xFF, sensor_telemetry); const float board_temp_c = board.getMCUTemperature(); @@ -567,7 +537,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now RepeaterStats stats; - stats.batt_milli_volts = board.getBattMilliVolts(); + stats.batt_milli_volts = getBatteryMilliVolts(true); stats.curr_tx_queue_len = _mgr->getOutboundTotal(); stats.noise_floor = (int16_t)_radio->getNoiseFloor(); stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); @@ -593,7 +563,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)getBatteryMilliVolts(true) / 1000.0f); // query other sensors -- target specific if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { @@ -1219,12 +1189,15 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _archive = nullptr; uptime_millis = 0; next_archive_neighbours_flush_ms = 0; + next_battery_sample_ms = 0; next_history_sample_ms = 0; next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; _archive_neighbours_dirty = false; + _battery_sample_valid = false; + _battery_mv_cache = 0; region_load_active = false; memset(&_stats_state, 0, sizeof(_stats_state)); @@ -1399,6 +1372,25 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3 } } +uint16_t MyMesh::getBatteryMilliVolts(bool force_refresh) { + constexpr unsigned long kBatterySampleIntervalMs = 60000UL; + + if (board.supportsBatteryReporting() && !board.isBatteryReportingEnabled()) { + _battery_sample_valid = true; + _battery_mv_cache = 0; + next_battery_sample_ms = 0; + return 0; + } + + if (force_refresh || !_battery_sample_valid || next_battery_sample_ms == 0 || millisHasNowPassed(next_battery_sample_ms)) { + _battery_mv_cache = board.getBattMilliVolts(); + _battery_sample_valid = true; + next_battery_sample_ms = millis() + kBatterySampleIntervalMs; + } + + return _battery_mv_cache; +} + void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) { set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params pending_freq = freq; @@ -1534,7 +1526,13 @@ void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) { } void MyMesh::formatStatsReply(char *reply, size_t reply_size) { - StatsFormatHelper::formatCoreStats(reply, reply_size, board, *_ms, _err_flags, _mgr); + snprintf(reply, + reply_size, + "{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}", + getBatteryMilliVolts(true), + _ms->getMillis() / 1000, + _err_flags, + _mgr->getOutboundTotal()); } void MyMesh::startRegionsLoad() { @@ -1863,13 +1861,14 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) { #endif if (next_history_sample_ms == 0 || millisHasNowPassed(next_history_sample_ms)) { if (!live_stats_headroom_low) { - WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors); + const uint16_t battery_mv = getBatteryMilliVolts(); + WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv); HistorySample sample{}; sample.epoch_secs = getRTCClock()->getCurrentTime(); sample.uptime_secs = static_cast(uptime_millis / 1000); sample.packets_sent = radio_driver.getPacketsSent(); sample.packets_recv = radio_driver.getPacketsRecv(); - sample.battery_mv = board.getBattMilliVolts(); + sample.battery_mv = battery_mv; sample.queue_len = static_cast(_mgr->getOutboundTotal()); sample.error_flags = _err_flags; sample.recv_errors = static_cast(min(radio_driver.getPacketsRecvErrors(), 0xFFFF)); @@ -2488,7 +2487,9 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { wifi_signal[sizeof(wifi_signal) - 1] = 0; #endif + const uint16_t battery_mv = getBatteryMilliVolts(true); const int battery_pct = board.getBatteryPercent(); + const WebSensorSnapshot sensor_snapshot = collectWebSensorSnapshot(board, sensors, battery_mv); const bool archive_available = (_archive != nullptr) && _archive->isMounted(); #ifdef WITH_MQTT_UPLINK const bool mqtt_connected = mqtt.isAnyBrokerConnected(); @@ -2544,7 +2545,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { archive_type, static_cast(_archive != nullptr ? _archive->getTotalBytes() : 0), static_cast(_archive != nullptr ? _archive->getUsedBytes() : 0), - board.getBattMilliVolts(), + battery_mv, battery_pct, static_cast(uptime_millis / 1000), _err_flags, @@ -2594,7 +2595,7 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) { } offset += snprintf(&reply[offset], reply_size - offset, ","); - if (!appendJsonSensors(reply, reply_size, offset)) { + if (!appendJsonSensors(reply, reply_size, offset, sensor_snapshot)) { return false; } offset += snprintf(&reply[offset], reply_size - offset, ","); @@ -2634,10 +2635,8 @@ bool MyMesh::formatWebStatsSeriesJson(const char* series, char* reply, size_t re #endif } -bool MyMesh::appendJsonSensors(char* reply, size_t reply_size, size_t& offset) const { +bool MyMesh::appendJsonSensors(char* reply, size_t reply_size, size_t& offset, const WebSensorSnapshot& snapshot) const { #if defined(ESP_PLATFORM) && WITH_WEB_PANEL - WebSensorSnapshot snapshot = collectWebSensorSnapshot(board, sensors); - bool needs_comma = false; const int open_written = snprintf(&reply[offset], reply_size - offset, "\"sensors\":{"); if (open_written < 0 || static_cast(open_written) >= (reply_size - offset)) { @@ -2776,7 +2775,7 @@ void MyMesh::loop() { #endif #ifdef WITH_MQTT_UPLINK MQTTStatusSnapshot mqtt_status{}; - mqtt_status.battery_mv = static_cast(board.getBattMilliVolts()); + mqtt_status.battery_mv = static_cast(getBatteryMilliVolts()); mqtt_status.uptime_secs = static_cast(uptime_millis / 1000); mqtt_status.error_flags = _err_flags; mqtt_status.queue_len = static_cast(_mgr->getOutboundTotal()); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 18912e95..8d15bb96 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -65,6 +65,41 @@ struct RepeaterStats { uint32_t n_recv_errors; }; +struct WebSensorSnapshot { + bool has_battery = false; + uint16_t battery_mv = 0; + + bool has_supply_voltage = false; + float supply_voltage_v = NAN; + + bool has_sensor_temp = false; + float sensor_temp_c = NAN; + + bool has_mcu_temp = false; + float mcu_temp_c = NAN; + + bool has_humidity = false; + float humidity_pct = NAN; + + bool has_pressure = false; + float pressure_hpa = NAN; + + bool has_pressure_altitude = false; + float pressure_altitude_m = NAN; + + bool has_gps = false; + bool gps_enabled = false; + bool gps_fix = false; + bool has_gps_lat = false; + float gps_lat = NAN; + bool has_gps_lon = false; + float gps_lon = NAN; + bool has_gps_altitude = false; + float gps_altitude_m = NAN; + bool has_satellites = false; + long satellites = 0; +}; + #ifndef MAX_CLIENTS #define MAX_CLIENTS 32 #endif @@ -94,10 +129,13 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm uint32_t last_millis; uint64_t uptime_millis; unsigned long next_archive_neighbours_flush_ms; + unsigned long next_battery_sample_ms; unsigned long next_history_sample_ms; unsigned long next_local_advert, next_flood_advert; bool _logging; bool _archive_neighbours_dirty; + bool _battery_sample_valid; + uint16_t _battery_mv_cache; NodePrefs _prefs; ClientACL acl; CommonCLI _cli; @@ -159,7 +197,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm void recordStatsEvent(uint8_t type, int16_t value = 0); bool appendJsonEvents(char* reply, size_t reply_size, size_t& offset) const; bool appendJsonNeighbours(char* reply, size_t reply_size, size_t& offset) const; - bool appendJsonSensors(char* reply, size_t reply_size, size_t& offset) const; + bool appendJsonSensors(char* reply, size_t reply_size, size_t& offset, const WebSensorSnapshot& snapshot) const; + uint16_t getBatteryMilliVolts(bool force_refresh = false); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); diff --git a/release-notes.yml b/release-notes.yml index 6fc267c3..7b7d762b 100644 --- a/release-notes.yml +++ b/release-notes.yml @@ -401,3 +401,24 @@ releases: area: docs text: "Updated the web panel, API, and custom CLI docs to cover the environment summary/trends, stats persistence behaviour, and existing T-Beam 1W fan controls." breaking_changes: [] + + - track: repeater-mqtt + version: "1.3.9" + tag: "repeater-mqtt-eastmesh-v1.3.9" + date: "2026-04-22" + previous_version: "1.3.8" + summary: "Reduced unnecessary repeater battery polling by removing hot-loop MQTT sampling and reusing throttled battery reads across stats and web status paths." + changes: + - type: fixed + area: mqtt + text: "Stopped rebuilding the MQTT status battery reading from a fresh ADC sample on every repeater loop iteration and switched that path to a throttled cached value instead." + - type: changed + area: stats + text: "Reused a single battery sample across the web stats history and summary builders so one logical stats operation no longer performs multiple immediate battery reads." + - type: fixed + area: board-support + text: "Reduced Heltec V3 battery-sense activity while MQTT and web features are active, while still keeping explicit status and telemetry requests on a fresh reading path." + - type: docs + area: docs + text: "Updated the custom CLI docs to note that repeater MQTT background battery sampling is now rate-limited while explicit status and telemetry requests still refresh immediately." + breaking_changes: []