diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d200cdd1..e8d7a20a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -126,6 +126,23 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### View or change automatic low-battery alerts to `#repeaters` + +**Usage:** +- `get battery.alert` +- `set battery.alert ` + +**Parameters:** +- `state`: `on` (enable) or `off` (disable) + +**Default:** `off` + +**Notes:** +- When enabled, sends a `#repeaters` flood text warning if voltage is above `1 V` and the battery estimate is below `20%`. +- Warnings repeat every `24` hours, or every `12` hours below `10%`. + +--- + ### Get or set recent repeater fallback prefix/SNR **Usage:** - `get recent.repeater` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a2fb3162..6dc8bb7d 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -66,6 +66,19 @@ #ifndef REPEATERS_CHANNEL_KEY_HEX #define REPEATERS_CHANNEL_KEY_HEX "89db441e2814dccf0dbd2e8cc5f501a3" #endif +#ifndef BATT_MIN_MILLIVOLTS + #define BATT_MIN_MILLIVOLTS 3000 +#endif +#ifndef BATT_MAX_MILLIVOLTS + #define BATT_MAX_MILLIVOLTS 4200 +#endif + +#define LOW_BATTERY_MIN_VALID_MV 1000 +#define LOW_BATTERY_WARN_PERCENT 20 +#define LOW_BATTERY_CRITICAL_PERCENT 10 +#define LOW_BATTERY_CHECK_INTERVAL (60UL * 1000UL) +#define LOW_BATTERY_WARN_INTERVAL (24UL * 60UL * 60UL * 1000UL) +#define LOW_BATTERY_CRITICAL_INTERVAL (12UL * 60UL * 60UL * 1000UL) static void formatRecentRepeaterPrefix(const SimpleMeshTables::RecentRepeaterInfo* info, char* out, size_t out_len) { if (out == NULL || out_len == 0) { @@ -560,6 +573,17 @@ static bool buildRepeatersChannel(mesh::GroupChannel& channel) { return true; } +static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) { + const int min_mv = BATT_MIN_MILLIVOLTS; + const int max_mv = BATT_MAX_MILLIVOLTS; + if (max_mv <= min_mv) return 100; + + int pct = (((int)batt_mv - min_mv) * 100) / (max_mv - min_mv); + if (pct < 0) return 0; + if (pct > 100) return 100; + return (uint8_t)pct; +} + mesh::Packet* MyMesh::createPacketCopy(const mesh::Packet* packet, const char* caller) { if (packet == NULL) return NULL; @@ -1970,6 +1994,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc last_millis = 0; uptime_millis = 0; next_local_advert = next_flood_advert = 0; + next_battery_alert_check = 0; + last_battery_alert_sent = 0; + battery_alert_sent = false; dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; @@ -1996,6 +2023,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_retry_path_gate = 2; _prefs.flood_retry_bridge_enabled = 0; _prefs.flood_retry_advert_enabled = 0; + _prefs.battery_alert_enabled = 0; _prefs.direct_retry_cr4_snr_x4 = DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr5_snr_x4 = DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT; _prefs.direct_retry_cr7_snr_x4 = DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT; @@ -2114,6 +2142,82 @@ void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint3 } } +bool MyMesh::sendRepeatersFloodText(const char* text) { + if (text == NULL || *text == 0) return false; + + mesh::GroupChannel channel; + if (!buildRepeatersChannel(channel)) { + return false; + } + + uint8_t temp[MAX_PACKET_PAYLOAD]; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + memcpy(temp, ×tamp, 4); + temp[4] = (TXT_TYPE_PLAIN << 2); + + const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; + const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; + int prefix_written = prefix_cap > 0 + ? snprintf((char*)&temp[5], prefix_cap, "%s: ", _prefs.node_name) + : -1; + if (prefix_written < 0) { + return false; + } + + size_t prefix_len = (size_t)prefix_written; + if (prefix_len >= prefix_cap) { + prefix_len = prefix_cap - 1; + } + + size_t text_len = strlen(text); + size_t max_text_len = max_data_len - 5 - prefix_len; + if (text_len > max_text_len) { + text_len = max_text_len; + } + memcpy(&temp[5 + prefix_len], text, text_len); + + auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); + if (pkt == NULL) { + return false; + } + + sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1); + return true; +} + +void MyMesh::checkBatteryAlert() { + if (!_prefs.battery_alert_enabled) { + battery_alert_sent = false; + return; + } + + if (next_battery_alert_check && !millisHasNowPassed(next_battery_alert_check)) { + return; + } + next_battery_alert_check = futureMillis(LOW_BATTERY_CHECK_INTERVAL); + + uint16_t batt_mv = board.getBattMilliVolts(); + uint8_t batt_pct = batteryPercentFromMilliVolts(batt_mv); + if (batt_mv <= LOW_BATTERY_MIN_VALID_MV || batt_pct >= LOW_BATTERY_WARN_PERCENT) { + battery_alert_sent = false; + return; + } + + unsigned long interval = batt_pct < LOW_BATTERY_CRITICAL_PERCENT + ? LOW_BATTERY_CRITICAL_INTERVAL + : LOW_BATTERY_WARN_INTERVAL; + if (battery_alert_sent && !millisHasNowPassed(last_battery_alert_sent + interval)) { + return; + } + + char text[96]; + snprintf(text, sizeof(text), "LOW BATTERY %u%% (%u mV)", (uint32_t)batt_pct, (uint32_t)batt_mv); + if (sendRepeatersFloodText(text)) { + battery_alert_sent = true; + last_battery_alert_sent = millis(); + } +} + 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; @@ -2519,45 +2623,27 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * char* text = trimSpaces(command + 16); if (*text == 0) { strcpy(reply, "Err - usage: send text.flood "); + } else if (sendRepeatersFloodText(text)) { + strcpy(reply, "OK"); } else { - mesh::GroupChannel channel; - if (!buildRepeatersChannel(channel)) { - strcpy(reply, "Err - invalid #repeaters key"); - } else { - uint8_t temp[MAX_PACKET_PAYLOAD]; - uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); - memcpy(temp, ×tamp, 4); - temp[4] = (TXT_TYPE_PLAIN << 2); - - const size_t max_data_len = MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE; - const size_t prefix_cap = max_data_len > 5 ? max_data_len - 5 + 1 : 0; - int prefix_written = prefix_cap > 0 - ? snprintf((char*)&temp[5], prefix_cap, "%s: ", _prefs.node_name) - : -1; - if (prefix_written < 0) { - strcpy(reply, "Err - unable to create message"); - } else { - size_t prefix_len = (size_t)prefix_written; - if (prefix_len >= prefix_cap) { - prefix_len = prefix_cap - 1; - } - - size_t text_len = strlen(text); - size_t max_text_len = max_data_len - 5 - prefix_len; - if (text_len > max_text_len) { - text_len = max_text_len; - } - memcpy(&temp[5 + prefix_len], text, text_len); - - auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len); - if (pkt) { - sendFloodScoped(default_scope, pkt, 0, _prefs.path_hash_mode + 1); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Err - unable to create packet"); - } - } - } + strcpy(reply, "Err - unable to create packet"); + } + } else if (strcmp(command, "get battery.alert") == 0) { + sprintf(reply, "> %s", _prefs.battery_alert_enabled ? "on" : "off"); + } else if (strncmp(command, "set battery.alert ", 18) == 0) { + const char* value = command + 18; + if (strcmp(value, "on") == 0) { + _prefs.battery_alert_enabled = 1; + next_battery_alert_check = 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (strcmp(value, "off") == 0) { + _prefs.battery_alert_enabled = 0; + battery_alert_sent = false; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - usage: set battery.alert "); } } else if (strncmp(command, "get recent.repeater", 19) == 0 || strncmp(command, "set recent.repeater", 19) == 0 @@ -2759,6 +2845,7 @@ void MyMesh::loop() { #endif mesh::Mesh::loop(); + checkBatteryAlert(); if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet *pkt = createSelfAdvert(); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0e6c5f70..f55d1d40 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -85,6 +85,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint32_t last_millis; uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; + unsigned long next_battery_alert_check; + unsigned long last_battery_alert_sent; + bool battery_alert_sent; bool _logging; NodePrefs _prefs; ClientACL acl; @@ -163,6 +166,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); + bool sendRepeatersFloodText(const char* text); + void checkBatteryAlert(); File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index db56c558..3611c05d 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -556,6 +556,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 retry_cr_read += file.read((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 + _prefs->battery_alert_enabled = 0; + size_t battery_alert_read = file.read((uint8_t *)&_prefs->battery_alert_enabled, + sizeof(_prefs->battery_alert_enabled)); // 664 // PowerSaving-only prefs stored radio_fem_rxgain at 291, before direct retry timing existed. if (radio_fem_rxgain_read != sizeof(_prefs->radio_fem_rxgain) && legacy_retry_attempts_read == sizeof(legacy_retry_attempts_or_radio_fem_rxgain) @@ -654,6 +657,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } else { sanitizeDirectRetryCrThresholds(_prefs); } + if (battery_alert_read != sizeof(_prefs->battery_alert_enabled)) { + _prefs->battery_alert_enabled = 0; + } else { + _prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1); + } file.close(); } @@ -739,7 +747,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->direct_retry_cr5_snr_x4, sizeof(_prefs->direct_retry_cr5_snr_x4)); // 661 file.write((uint8_t *)&_prefs->direct_retry_cr7_snr_x4, sizeof(_prefs->direct_retry_cr7_snr_x4)); // 662 file.write((uint8_t *)&_prefs->direct_retry_cr8_snr_x4, sizeof(_prefs->direct_retry_cr8_snr_x4)); // 663 - // next: 664 + file.write((uint8_t *)&_prefs->battery_alert_enabled, sizeof(_prefs->battery_alert_enabled)); // 664 + // next: 665 file.close(); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 104a7150..4b7d910c 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -132,6 +132,7 @@ struct NodePrefs { // persisted to file int8_t direct_retry_cr5_snr_x4; int8_t direct_retry_cr7_snr_x4; int8_t direct_retry_cr8_snr_x4; + uint8_t battery_alert_enabled; }; class CommonCLICallbacks {