Add opt-in battery alert flood text

This commit is contained in:
mikecarper committed 2026-05-22 14:28:03 -07:00
1 parent c0e3860c3c
commit f693bdb212
5 files changed
+158 -39

No files matched your search

+17
View File
@@ -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 <state>`
**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`
+125 -38
View File
@@ -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, &timestamp, 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 <message>");
} 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, &timestamp, 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 <on|off>");
}
} 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();
+5
View File
@@ -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[]);
+10 -1
View File
@@ -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();
}
+1
View File
@@ -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 {