mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 15:52:50 +00:00
Add scheduled telemetry history packets
This commit is contained in:
@@ -69,7 +69,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Statistics | [`stats-radio`](cli_commands.md#radio-stats---noise-floor-last-rssisnr-airtime-receive-errors) | Local serial | Serial | Serial | Serial |
|
||||
| Statistics | [`stats-radio-diag`](#stats-radio-diag) | Local serial | Serial | Serial | Serial |
|
||||
| Statistics | [`stats-packets`](cli_commands.md#packet-stats---packet-counters-received-sent) | Local serial | Serial | Serial | Serial |
|
||||
| Statistics | [`get telemetry.temp/volt/gps`; `set telemetry.gps`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; remote access requires administrator | Yes | Yes | Yes |
|
||||
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS provider; remote access requires administrator | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data; other roles can return empty data | Yes | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes |
|
||||
@@ -225,7 +225,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| Statistics | [`stats-radio`](cli_commands.md#radio-stats---noise-floor-last-rssisnr-airtime-receive-errors) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`stats-radio-diag`](#stats-radio-diag) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`stats-packets`](cli_commands.md#packet-stats---packet-counters-received-sent) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Statistics | [`get telemetry.temp/volt/gps`; `set telemetry.gps`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; remote access requires administrator | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Statistics | [`get telemetry.temp/volt`; optional GPS history; `get/set telemetry.tx`](cli_commands.md#read-repeater-telemetry-history) | Non-STM32 repeater; GPS commands require a GPS provider; remote access requires administrator | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Logging | [`log start`; `log stop`; `log erase`](cli_commands.md#logging) | Storage-backed roles retain data | Yes | Yes | Yes | No | No | Yes | Yes |
|
||||
| Logging | [`log`](cli_commands.md#print-the-captured-log-to-the-serial-terminal) | Local serial | Serial | Serial | Serial | No | No | Serial | Serial |
|
||||
| Radio | [`get radio`; `set radio ...`](cli_commands.md#view-or-change-this-nodes-radio-parameters) | All text CLI roles | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
+56
-4
@@ -306,10 +306,11 @@ refresh is already in flight, the scope pass is queued behind it.
|
||||
### Read repeater telemetry history
|
||||
|
||||
Repeater firmware records one UTC-aligned sample every 30 minutes. Temperature
|
||||
and battery voltage retain 336 samples (seven rolling days). GPS retains three
|
||||
days by default. Sensor builds with an onboard GPS provider request a seven-day
|
||||
default at startup. The history and any runtime retention change are held in
|
||||
RAM and reset after a reboot.
|
||||
and battery voltage retain 336 samples (seven rolling days). GPS-capable builds
|
||||
retain three GPS days by default and request a seven-day default at startup.
|
||||
Builds without a GPS provider omit the GPS history commands to conserve flash.
|
||||
The history and any runtime retention change are held in RAM and reset after a
|
||||
reboot.
|
||||
|
||||
The feature is omitted from flash-constrained STM32 repeater images.
|
||||
|
||||
@@ -319,6 +320,10 @@ The feature is omitted from flash-constrained STM32 repeater images.
|
||||
- `get telemetry.volt [page]`
|
||||
- `get telemetry.gps [page]`
|
||||
- `set telemetry.gps <days>`
|
||||
- `get telemetry.tx`
|
||||
- `set telemetry.tx <off|direct|path>`
|
||||
- `set telemetry.tx schedule <off|1-30d>`
|
||||
- `send telemetry.tx now`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
@@ -331,6 +336,12 @@ The feature is omitted from flash-constrained STM32 repeater images.
|
||||
needed to leave at least 2048 bytes free and replies with the days and pages
|
||||
actually available. For example, a request can return
|
||||
`OK - telemetry.gps days=18 pages=36 requested=30`.
|
||||
- `direct`: Send the binary temperature and voltage snapshots zero-hop to a
|
||||
neighboring MQTT observer.
|
||||
- `path`: A comma-separated direct route using the same one-, two-, or
|
||||
three-byte hop hashes accepted by `set outpath`.
|
||||
- `schedule`: Automatic interval in whole days. The default is `2d`; `off`
|
||||
retains the configured direct path for manual test sends.
|
||||
|
||||
Local serial and remote administrator CLI sessions can read the history.
|
||||
Collection uses the MCU temperature, battery voltage, and an already-valid
|
||||
@@ -345,6 +356,47 @@ Use the browser-based [Telemetry history decoder](telemetry_decoder.md) to
|
||||
turn a reply into a timestamped table or downloadable CSV without uploading
|
||||
the data.
|
||||
|
||||
`telemetry.tx` is disabled by default. Its schedule and direct path are stored
|
||||
across reboots. Configuring `direct` or a routed path enables the default `2d`
|
||||
schedule; `set telemetry.tx schedule` changes it from one through 30 days or
|
||||
turns it off. An automatic run waits until 165 half-hour positions are
|
||||
available, then sends one maximum-size temperature packet and one maximum-size
|
||||
voltage packet. GPS is never included in this raw transmission. The history
|
||||
remains boot-local, so the first scheduled pair after a reboot needs about
|
||||
82.5 hours to fill. On the default two-day interval, each 82.5-hour packet
|
||||
overlaps the previous packet by 34.5 hours. A queue failure is retried after
|
||||
30 minutes only for the packet that did not queue; successfully queued packets
|
||||
are not duplicated.
|
||||
|
||||
`send telemetry.tx now` is an administrator-only test action. It immediately
|
||||
queues one temperature and one voltage direct packet over the configured path,
|
||||
using all currently available positions up to 165. It works while the schedule
|
||||
is off and does not move the next scheduled send time. RAW_CUSTOM direct
|
||||
packets do not enter the normal encrypted direct-message retry mechanism.
|
||||
|
||||
Full snapshots are 184-byte RAW_CUSTOM payloads. Manual tests can be shorter
|
||||
while history fills. Both formats are binary, not Base64 and not encrypted:
|
||||
|
||||
| Bytes | Meaning |
|
||||
|---|---|
|
||||
| `0`-`3` | ASCII magic `TTB1` for temperature or `TVB1` for voltage |
|
||||
| `4`-`11` | First eight bytes of the repeater public key |
|
||||
| `12`-`15` | First sample UTC epoch, unsigned 32-bit little-endian |
|
||||
| `16`-`17` | Sample interval in minutes, unsigned 16-bit little-endian (`30`) |
|
||||
| `18` | Sample count (`1`-`165`) |
|
||||
| `19` onward | Oldest-to-newest encoded samples for the selected series |
|
||||
|
||||
Temperature codes reserve `0` for no reading, `1` for below `-50 C`, and `2`
|
||||
for above `+77 C`. Codes `3` through `130` represent exact whole degrees from
|
||||
`-50 C` through `+77 C`; decode them as `code - 53`. Voltage codes use the
|
||||
same encoding as the paged voltage payload documented below.
|
||||
|
||||
Match bytes `4`-`11` to the first 16 hex characters of the repeater public key
|
||||
shown by its advert or `get public.key`. This compact identifier is useful for
|
||||
association but is not authenticated and can be spoofed. An MQTT observer can
|
||||
upload the received raw packet to LetsMesh Analyzer `/packets`; its packet hex
|
||||
contains the direct-route header and path followed by this payload.
|
||||
|
||||
Temperature payload (`0x11`, 61 bytes):
|
||||
|
||||
| Bytes | Meaning |
|
||||
|
||||
@@ -75,7 +75,7 @@ set flood.retry.ignore none
|
||||
|
||||
| Setting | What it does | How to use | Example |
|
||||
| --- | --- | --- | --- |
|
||||
| `telemetry.temp`, `telemetry.volt`, `telemetry.gps` | Records 30-minute whole-degree MCU temperature and battery samples for seven days. GPS defaults to three days, or requests seven days at startup on onboard-GPS sensor builds. Runtime GPS retention can be `1`-`30` days and is reduced if needed to retain 2 KB of free memory. Pages are 1-based and newest first. History and runtime sizing reset on reboot. | `get telemetry.temp [page]`, `get telemetry.volt [page]`, `get telemetry.gps [page]`, `set telemetry.gps <1-30>` | `set telemetry.gps 30` |
|
||||
| `telemetry.temp`, `telemetry.volt`, `telemetry.gps` | Records 30-minute MCU temperature and battery samples for seven days. GPS-capable builds retain GPS separately for authenticated paging. `telemetry.tx` sends one temperature RAW_CUSTOM packet and one voltage packet over a configured direct path, on a persistent 1-30 day schedule (default two days) or immediately for testing; GPS is never included. Scheduled packets contain 165 points. History resets on reboot. | `get telemetry.temp [page]`, `get telemetry.volt [page]`, `get telemetry.gps [page]`, `set telemetry.gps <1-30>`, `get telemetry.tx`, `set telemetry.tx <off|direct|path>`, `set telemetry.tx schedule <off|1-30d>`, `send telemetry.tx now` | `set telemetry.tx A1B2,C3D4` |
|
||||
| `battery.alert` | Sends opt-in, region-scoped low-battery warnings to `#repeaters` after 30 minutes of uptime. | `get battery.alert`, `get battery.alert.region`, `set battery.alert on [region]`, `set battery.alert off` | `set battery.alert on sea` |
|
||||
| `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` |
|
||||
| `battery.alert.critical` | Critical threshold percentage. Critical and warning alerts use the same 12-hour resend cooldown. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` |
|
||||
|
||||
@@ -157,6 +157,13 @@ static const char FLOOD_CHANNEL_SCOPE_USAGE[] =
|
||||
#define RX_INACTIVITY_WATCHDOG_INTERVAL (12UL * 60UL * 60UL * 1000UL)
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
#define TELEMETRY_GPS_HEAP_RESERVE_BYTES 2048U
|
||||
#define TELEMETRY_HISTORY_TX_PREFS_FILE "/telemetry_tx"
|
||||
static const uint64_t TELEMETRY_HISTORY_TX_RETRY_MILLIS =
|
||||
30ULL * 60ULL * 1000ULL;
|
||||
static const uint8_t TELEMETRY_HISTORY_TX_DEFAULT_DAYS = 2U;
|
||||
static const uint8_t TELEMETRY_HISTORY_TX_MAX_DAYS = 30U;
|
||||
static const uint8_t TELEMETRY_HISTORY_TX_TEMPERATURE = 1U;
|
||||
static const uint8_t TELEMETRY_HISTORY_TX_VOLTAGE = 2U;
|
||||
#endif
|
||||
|
||||
#define CLOCK_SYNC_VALID_YEARS 10
|
||||
@@ -2952,6 +2959,15 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
clock_sync_internet_requested_millis = 0;
|
||||
clock_sync_next_attempt_uptime = CLOCK_SYNC_STARTUP_DELAY_MILLIS;
|
||||
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
telemetry_history_tx_enabled = false;
|
||||
memset(telemetry_history_tx_path, 0, sizeof(telemetry_history_tx_path));
|
||||
telemetry_history_tx_path_len = OUT_PATH_UNKNOWN;
|
||||
telemetry_history_tx_interval_days = TELEMETRY_HISTORY_TX_DEFAULT_DAYS;
|
||||
telemetry_history_tx_pending = 0;
|
||||
telemetry_history_next_tx_uptime = 0;
|
||||
#endif
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
#endif
|
||||
@@ -3063,6 +3079,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
_fs = fs;
|
||||
// load persisted prefs
|
||||
_cli.loadPrefs(_fs);
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
loadTelemetryHistoryTxPrefs();
|
||||
#endif
|
||||
|
||||
#ifdef SIM_WIFI_SSID
|
||||
// Emulator builds (Wokwi) boot with fresh NVS every run. Seed WiFi so the
|
||||
@@ -10041,8 +10060,183 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
mesh::TelemetryHistory::SERIES_TEMPERATURE;
|
||||
static const char telemetry_temp_command[] = "get telemetry.temp";
|
||||
static const char telemetry_volt_command[] = "get telemetry.volt";
|
||||
#if MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
static const char telemetry_gps_command[] = "get telemetry.gps";
|
||||
static const char telemetry_gps_set_command[] = "set telemetry.gps";
|
||||
#endif
|
||||
static const char telemetry_tx_get_command[] = "get telemetry.tx";
|
||||
static const char telemetry_tx_set_command[] = "set telemetry.tx";
|
||||
static const char telemetry_tx_schedule_command[] =
|
||||
"set telemetry.tx schedule";
|
||||
static const char telemetry_tx_send_now_command[] = "send telemetry.tx now";
|
||||
|
||||
if (strcmp(command, telemetry_tx_send_now_command) == 0) {
|
||||
if (sender != NULL && !sender->isAdmin()) {
|
||||
strcpy(reply, "Err - not permitted");
|
||||
return;
|
||||
}
|
||||
if (telemetry_history_tx_path_len == OUT_PATH_UNKNOWN
|
||||
|| telemetry_history_tx_path_len == OUT_PATH_FORCE_FLOOD
|
||||
|| !mesh::Packet::isValidPathLen(telemetry_history_tx_path_len)) {
|
||||
strcpy(reply, "Err - configure telemetry.tx direct or path first");
|
||||
return;
|
||||
}
|
||||
if (telemetry_history.voltageSampleCount() == 0) {
|
||||
strcpy(reply, "Err - telemetry history is empty");
|
||||
return;
|
||||
}
|
||||
const bool temperature_queued = sendTelemetryHistorySnapshot(
|
||||
mesh::TelemetryHistory::SERIES_TEMPERATURE);
|
||||
const bool voltage_queued = sendTelemetryHistorySnapshot(
|
||||
mesh::TelemetryHistory::SERIES_VOLTAGE);
|
||||
if (temperature_queued && voltage_queued) {
|
||||
const uint16_t available = telemetry_history.voltageSampleCount();
|
||||
const unsigned sent = available < mesh::TelemetryHistory::BINARY_MAX_SAMPLES
|
||||
? (unsigned)available
|
||||
: (unsigned)mesh::TelemetryHistory::BINARY_MAX_SAMPLES;
|
||||
snprintf(reply, 160, "OK - telemetry.tx queued temp=%u volt=%u",
|
||||
sent, sent);
|
||||
} else {
|
||||
snprintf(reply, 160, "Err - telemetry.tx queue temp=%s volt=%s",
|
||||
temperature_queued ? "yes" : "no",
|
||||
voltage_queued ? "yes" : "no");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(command, telemetry_tx_get_command) == 0) {
|
||||
if (sender != NULL && !sender->isAdmin()) {
|
||||
strcpy(reply, "Err - not permitted");
|
||||
} else {
|
||||
formatTelemetryHistoryTxStatus(reply, 160);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (strncmp(command, telemetry_tx_schedule_command,
|
||||
sizeof(telemetry_tx_schedule_command) - 1U) == 0
|
||||
&& (command[sizeof(telemetry_tx_schedule_command) - 1U] == 0
|
||||
|| command[sizeof(telemetry_tx_schedule_command) - 1U] == ' ')) {
|
||||
if (sender != NULL && !sender->isAdmin()) {
|
||||
strcpy(reply, "Err - not permitted");
|
||||
return;
|
||||
}
|
||||
char* spec = trimSpaces(
|
||||
command + sizeof(telemetry_tx_schedule_command) - 1U);
|
||||
bool enable = false;
|
||||
unsigned long days = telemetry_history_tx_interval_days;
|
||||
if (strcmp(spec, "off") == 0) {
|
||||
enable = false;
|
||||
} else {
|
||||
char* end = NULL;
|
||||
days = strtoul(spec, &end, 10);
|
||||
if (end == spec) days = 0;
|
||||
if (*end == 'd') end++;
|
||||
end = trimSpaces(end);
|
||||
if (*end != 0 || days < 1U
|
||||
|| days > TELEMETRY_HISTORY_TX_MAX_DAYS) {
|
||||
strcpy(reply, "Err - use: set telemetry.tx schedule <off|1-30d>");
|
||||
return;
|
||||
}
|
||||
if (telemetry_history_tx_path_len == OUT_PATH_UNKNOWN
|
||||
|| telemetry_history_tx_path_len == OUT_PATH_FORCE_FLOOD
|
||||
|| !mesh::Packet::isValidPathLen(telemetry_history_tx_path_len)) {
|
||||
strcpy(reply, "Err - configure telemetry.tx direct or path first");
|
||||
return;
|
||||
}
|
||||
enable = true;
|
||||
}
|
||||
|
||||
const bool previous_enabled = telemetry_history_tx_enabled;
|
||||
const uint8_t previous_days = telemetry_history_tx_interval_days;
|
||||
const uint8_t previous_pending = telemetry_history_tx_pending;
|
||||
const uint64_t previous_next_tx = telemetry_history_next_tx_uptime;
|
||||
telemetry_history_tx_enabled = enable;
|
||||
if (enable) telemetry_history_tx_interval_days = (uint8_t)days;
|
||||
telemetry_history_tx_pending = 0;
|
||||
telemetry_history_next_tx_uptime = 0;
|
||||
if (!saveTelemetryHistoryTxPrefs()) {
|
||||
telemetry_history_tx_enabled = previous_enabled;
|
||||
telemetry_history_tx_interval_days = previous_days;
|
||||
telemetry_history_tx_pending = previous_pending;
|
||||
telemetry_history_next_tx_uptime = previous_next_tx;
|
||||
strcpy(reply, "Err - unable to save telemetry.tx schedule");
|
||||
} else if (enable) {
|
||||
snprintf(reply, 160, "OK - telemetry.tx schedule=%ud", (unsigned)days);
|
||||
} else {
|
||||
strcpy(reply, "OK - telemetry.tx schedule=off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (strncmp(command, telemetry_tx_set_command,
|
||||
sizeof(telemetry_tx_set_command) - 1U) == 0
|
||||
&& (command[sizeof(telemetry_tx_set_command) - 1U] == 0
|
||||
|| command[sizeof(telemetry_tx_set_command) - 1U] == ' ')) {
|
||||
if (sender != NULL && !sender->isAdmin()) {
|
||||
strcpy(reply, "Err - not permitted");
|
||||
return;
|
||||
}
|
||||
|
||||
char* spec = trimSpaces(
|
||||
command + sizeof(telemetry_tx_set_command) - 1U);
|
||||
if (*spec == 0) {
|
||||
strcpy(reply,
|
||||
"Err - use: set telemetry.tx <off|direct|path>");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool previous_enabled = telemetry_history_tx_enabled;
|
||||
const uint8_t previous_path_len = telemetry_history_tx_path_len;
|
||||
const uint8_t previous_pending = telemetry_history_tx_pending;
|
||||
const uint64_t previous_next_tx = telemetry_history_next_tx_uptime;
|
||||
uint8_t previous_path[MAX_PATH_SIZE];
|
||||
memcpy(previous_path, telemetry_history_tx_path, sizeof(previous_path));
|
||||
|
||||
if (strcmp(spec, "off") == 0) {
|
||||
telemetry_history_tx_enabled = false;
|
||||
telemetry_history_tx_pending = 0;
|
||||
telemetry_history_next_tx_uptime = 0;
|
||||
} else {
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
uint8_t path_len = OUT_PATH_UNKNOWN;
|
||||
const char* err = NULL;
|
||||
if (!parsePathCommand(spec, path, path_len, err)
|
||||
|| path_len == OUT_PATH_UNKNOWN
|
||||
|| path_len == OUT_PATH_FORCE_FLOOD) {
|
||||
strcpy(reply, err != NULL ? err
|
||||
: "Err - telemetry.tx needs a direct path");
|
||||
return;
|
||||
}
|
||||
telemetry_history_tx_enabled = true;
|
||||
telemetry_history_tx_path_len = path_len;
|
||||
telemetry_history_tx_pending = 0;
|
||||
telemetry_history_next_tx_uptime = 0;
|
||||
memset(telemetry_history_tx_path, 0,
|
||||
sizeof(telemetry_history_tx_path));
|
||||
if ((path_len & 63U) != 0) {
|
||||
mesh::Packet::copyPath(telemetry_history_tx_path, path, path_len);
|
||||
}
|
||||
}
|
||||
|
||||
if (!saveTelemetryHistoryTxPrefs()) {
|
||||
telemetry_history_tx_enabled = previous_enabled;
|
||||
telemetry_history_tx_path_len = previous_path_len;
|
||||
telemetry_history_tx_pending = previous_pending;
|
||||
telemetry_history_next_tx_uptime = previous_next_tx;
|
||||
memcpy(telemetry_history_tx_path, previous_path,
|
||||
sizeof(telemetry_history_tx_path));
|
||||
strcpy(reply, "Err - unable to save telemetry.tx");
|
||||
} else if (telemetry_history_tx_enabled) {
|
||||
snprintf(reply, 160, "OK - telemetry.tx schedule=%ud temp=165 volt=165",
|
||||
(unsigned)telemetry_history_tx_interval_days);
|
||||
} else {
|
||||
strcpy(reply, "OK - telemetry.tx off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
if (strncmp(command, telemetry_gps_set_command,
|
||||
sizeof(telemetry_gps_set_command) - 1U) == 0
|
||||
&& (command[sizeof(telemetry_gps_set_command) - 1U] == 0
|
||||
@@ -10066,6 +10260,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
(unsigned)requested_days);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strncmp(command, telemetry_temp_command,
|
||||
sizeof(telemetry_temp_command) - 1U) == 0
|
||||
@@ -10078,12 +10273,14 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
|| command[sizeof(telemetry_volt_command) - 1U] == ' ')) {
|
||||
telemetry_series = mesh::TelemetryHistory::SERIES_VOLTAGE;
|
||||
telemetry_args = command + sizeof(telemetry_volt_command) - 1U;
|
||||
#if MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
} else if (strncmp(command, telemetry_gps_command,
|
||||
sizeof(telemetry_gps_command) - 1U) == 0
|
||||
&& (command[sizeof(telemetry_gps_command) - 1U] == 0
|
||||
|| command[sizeof(telemetry_gps_command) - 1U] == ' ')) {
|
||||
telemetry_series = mesh::TelemetryHistory::SERIES_GPS;
|
||||
telemetry_args = command + sizeof(telemetry_gps_command) - 1U;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (telemetry_args != NULL) {
|
||||
@@ -10536,6 +10733,7 @@ void MyMesh::loop() {
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
#if MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
uint8_t MyMesh::resizeTelemetryGpsDays(uint8_t requested_days) {
|
||||
size_t free_bytes = telemetryFreeHeapBytes();
|
||||
const size_t allocation_budget = free_bytes > TELEMETRY_GPS_HEAP_RESERVE_BYTES
|
||||
@@ -10566,6 +10764,142 @@ uint8_t MyMesh::resizeTelemetryGpsDays(uint8_t requested_days) {
|
||||
}
|
||||
return actual_days;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MyMesh::loadTelemetryHistoryTxPrefs() {
|
||||
telemetry_history_tx_enabled = false;
|
||||
memset(telemetry_history_tx_path, 0, sizeof(telemetry_history_tx_path));
|
||||
telemetry_history_tx_path_len = OUT_PATH_UNKNOWN;
|
||||
telemetry_history_tx_interval_days = TELEMETRY_HISTORY_TX_DEFAULT_DAYS;
|
||||
telemetry_history_tx_pending = 0;
|
||||
telemetry_history_next_tx_uptime = 0;
|
||||
|
||||
if (_fs == NULL || !_fs->exists(TELEMETRY_HISTORY_TX_PREFS_FILE)) return;
|
||||
File file = openFloodSettingsRead(_fs, TELEMETRY_HISTORY_TX_PREFS_FILE);
|
||||
if (!file) return;
|
||||
|
||||
uint8_t magic[4];
|
||||
uint8_t enabled = 0;
|
||||
uint8_t path_len = OUT_PATH_UNKNOWN;
|
||||
uint8_t interval_days = TELEMETRY_HISTORY_TX_DEFAULT_DAYS;
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
bool valid = file.read(magic, sizeof(magic)) == sizeof(magic)
|
||||
&& memcmp(magic, "THT2", sizeof(magic)) == 0
|
||||
&& file.read(&enabled, sizeof(enabled)) == sizeof(enabled)
|
||||
&& file.read(&path_len, sizeof(path_len)) == sizeof(path_len)
|
||||
&& file.read(&interval_days, sizeof(interval_days))
|
||||
== sizeof(interval_days)
|
||||
&& file.read(path, sizeof(path)) == sizeof(path);
|
||||
file.close();
|
||||
|
||||
valid = valid && enabled <= 1 && interval_days >= 1
|
||||
&& interval_days <= TELEMETRY_HISTORY_TX_MAX_DAYS;
|
||||
if (enabled != 0) {
|
||||
valid = valid && path_len != OUT_PATH_UNKNOWN
|
||||
&& path_len != OUT_PATH_FORCE_FLOOD
|
||||
&& mesh::Packet::isValidPathLen(path_len);
|
||||
}
|
||||
if (!valid) return;
|
||||
|
||||
telemetry_history_tx_enabled = enabled != 0;
|
||||
telemetry_history_tx_path_len = path_len;
|
||||
telemetry_history_tx_interval_days = interval_days;
|
||||
memcpy(telemetry_history_tx_path, path, sizeof(telemetry_history_tx_path));
|
||||
}
|
||||
|
||||
bool MyMesh::saveTelemetryHistoryTxPrefs() {
|
||||
if (_fs == NULL) return false;
|
||||
File file = openFloodSettingsWrite(_fs, TELEMETRY_HISTORY_TX_PREFS_FILE);
|
||||
if (!file) return false;
|
||||
|
||||
const uint8_t magic[4] = {'T', 'H', 'T', '2'};
|
||||
const uint8_t enabled = telemetry_history_tx_enabled ? 1 : 0;
|
||||
bool success = file.write(magic, sizeof(magic)) == sizeof(magic)
|
||||
&& file.write(&enabled, sizeof(enabled)) == sizeof(enabled)
|
||||
&& file.write(&telemetry_history_tx_path_len,
|
||||
sizeof(telemetry_history_tx_path_len))
|
||||
== sizeof(telemetry_history_tx_path_len)
|
||||
&& file.write(&telemetry_history_tx_interval_days,
|
||||
sizeof(telemetry_history_tx_interval_days))
|
||||
== sizeof(telemetry_history_tx_interval_days)
|
||||
&& file.write(telemetry_history_tx_path,
|
||||
sizeof(telemetry_history_tx_path))
|
||||
== sizeof(telemetry_history_tx_path);
|
||||
file.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
void MyMesh::formatTelemetryHistoryTxStatus(char* reply,
|
||||
size_t reply_size) const {
|
||||
char source_id[mesh::TelemetryHistory::BINARY_SOURCE_ID_SIZE * 2U + 1U];
|
||||
mesh::Utils::toHex(source_id, self_id.pub_key,
|
||||
mesh::TelemetryHistory::BINARY_SOURCE_ID_SIZE);
|
||||
char path_reply[132];
|
||||
formatPathReply(telemetry_history_tx_path,
|
||||
telemetry_history_tx_path_len,
|
||||
path_reply, sizeof(path_reply));
|
||||
snprintf(reply, reply_size, "> %s%ud id=%s p=%s",
|
||||
telemetry_history_tx_enabled ? "on " : "off ",
|
||||
(unsigned)telemetry_history_tx_interval_days,
|
||||
source_id, path_reply[0] == '>' && path_reply[1] == ' '
|
||||
? path_reply + 2 : path_reply);
|
||||
}
|
||||
|
||||
bool MyMesh::sendTelemetryHistorySnapshot(
|
||||
mesh::TelemetryHistory::Series series) {
|
||||
uint8_t payload[mesh::TelemetryHistory::BINARY_PAYLOAD_SIZE];
|
||||
const size_t payload_len = series == mesh::TelemetryHistory::SERIES_VOLTAGE
|
||||
? telemetry_history.formatVoltageBinarySnapshot(
|
||||
self_id.pub_key, payload, sizeof(payload))
|
||||
: telemetry_history.formatTemperatureBinarySnapshot(
|
||||
self_id.pub_key, payload, sizeof(payload));
|
||||
if (payload_len <= mesh::TelemetryHistory::BINARY_HEADER_SIZE) return false;
|
||||
|
||||
mesh::Packet* packet = createRawData(payload, payload_len);
|
||||
if (packet == NULL) return false;
|
||||
return sendDirect(packet, telemetry_history_tx_path,
|
||||
telemetry_history_tx_path_len);
|
||||
}
|
||||
|
||||
void MyMesh::serviceTelemetryHistoryTx() {
|
||||
if (!telemetry_history_tx_enabled
|
||||
|| telemetry_history.voltageSampleCount()
|
||||
< mesh::TelemetryHistory::BINARY_MAX_SAMPLES) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t current_uptime_millis =
|
||||
uptime_millis + (uint32_t)(millis() - last_millis);
|
||||
if (telemetry_history_next_tx_uptime != 0
|
||||
&& current_uptime_millis < telemetry_history_next_tx_uptime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (telemetry_history_tx_pending == 0) {
|
||||
telemetry_history_tx_pending = TELEMETRY_HISTORY_TX_TEMPERATURE
|
||||
| TELEMETRY_HISTORY_TX_VOLTAGE;
|
||||
}
|
||||
if ((telemetry_history_tx_pending & TELEMETRY_HISTORY_TX_TEMPERATURE) != 0
|
||||
&& sendTelemetryHistorySnapshot(
|
||||
mesh::TelemetryHistory::SERIES_TEMPERATURE)) {
|
||||
telemetry_history_tx_pending &=
|
||||
(uint8_t)~TELEMETRY_HISTORY_TX_TEMPERATURE;
|
||||
}
|
||||
if ((telemetry_history_tx_pending & TELEMETRY_HISTORY_TX_VOLTAGE) != 0
|
||||
&& sendTelemetryHistorySnapshot(mesh::TelemetryHistory::SERIES_VOLTAGE)) {
|
||||
telemetry_history_tx_pending &= (uint8_t)~TELEMETRY_HISTORY_TX_VOLTAGE;
|
||||
}
|
||||
|
||||
if (telemetry_history_tx_pending == 0) {
|
||||
telemetry_history_next_tx_uptime =
|
||||
current_uptime_millis
|
||||
+ (uint64_t)telemetry_history_tx_interval_days
|
||||
* 24ULL * 60ULL * 60ULL * 1000ULL;
|
||||
} else {
|
||||
telemetry_history_next_tx_uptime =
|
||||
current_uptime_millis + TELEMETRY_HISTORY_TX_RETRY_MILLIS;
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::sampleTelemetryHistory() {
|
||||
const uint32_t now = rtc_clock.getCurrentTime();
|
||||
@@ -10613,6 +10947,7 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
|
||||
checkRxInactivityWatchdog();
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
sampleTelemetryHistory();
|
||||
serviceTelemetryHistoryTx();
|
||||
#endif
|
||||
#if !defined(PORTABLE_MQTT_OBSERVER)
|
||||
checkBatteryAlert();
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
// Builds without a GPS provider only collect missing GPS positions. Omit
|
||||
// their unused decoder and resize CLI to preserve flash for useful history.
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
#define MESH_ENABLE_TELEMETRY_GPS_HISTORY 1
|
||||
#else
|
||||
#define MESH_ENABLE_TELEMETRY_GPS_HISTORY 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MESH_ENABLE_RECENT_REPEATERS
|
||||
#define MESH_ENABLE_RECENT_REPEATERS 1
|
||||
#endif
|
||||
@@ -426,6 +436,12 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
CayenneLPP telemetry;
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
mesh::TelemetryHistory telemetry_history;
|
||||
bool telemetry_history_tx_enabled;
|
||||
uint8_t telemetry_history_tx_path[MAX_PATH_SIZE];
|
||||
uint8_t telemetry_history_tx_path_len;
|
||||
uint8_t telemetry_history_tx_interval_days;
|
||||
uint8_t telemetry_history_tx_pending;
|
||||
uint64_t telemetry_history_next_tx_uptime;
|
||||
#endif
|
||||
unsigned long _ota_update_at = 0; // deferred `ota update` fire time (0 = none scheduled)
|
||||
float active_bw; // live BW, including temporary radio overrides
|
||||
@@ -562,7 +578,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
void servicePostMeshLoop();
|
||||
#if MESH_ENABLE_TELEMETRY_HISTORY
|
||||
void sampleTelemetryHistory();
|
||||
#if MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
uint8_t resizeTelemetryGpsDays(uint8_t requested_days);
|
||||
#endif
|
||||
void loadTelemetryHistoryTxPrefs();
|
||||
bool saveTelemetryHistoryTxPrefs();
|
||||
void serviceTelemetryHistoryTx();
|
||||
bool sendTelemetryHistorySnapshot(mesh::TelemetryHistory::Series series);
|
||||
void formatTelemetryHistoryTxStatus(char* reply, size_t reply_size) const;
|
||||
#endif
|
||||
void sendSelfAdvertisementNow(uint32_t delay_millis, bool flood);
|
||||
bool sendRepeatersFloodText(const char* text, const TransportKey* scope = nullptr,
|
||||
|
||||
@@ -41,6 +41,14 @@ public:
|
||||
static constexpr uint8_t VOLTAGE_PAYLOAD_TYPE_V1 = 0x12;
|
||||
static constexpr uint8_t GPS_PAYLOAD_TYPE_V1 = 0x13;
|
||||
|
||||
// A raw custom temperature ("TTB1") or voltage ("TVB1") snapshot fills one
|
||||
// 184-byte mesh payload: 19 metadata bytes plus 165 chronological samples.
|
||||
static constexpr size_t BINARY_HEADER_SIZE = 19;
|
||||
static constexpr uint8_t BINARY_SOURCE_ID_SIZE = 8;
|
||||
static constexpr uint8_t BINARY_MAX_SAMPLES = 165;
|
||||
static constexpr size_t BINARY_PAYLOAD_SIZE =
|
||||
BINARY_HEADER_SIZE + BINARY_MAX_SAMPLES;
|
||||
|
||||
enum Series : uint8_t {
|
||||
SERIES_TEMPERATURE = 0,
|
||||
SERIES_VOLTAGE = 1,
|
||||
@@ -169,6 +177,68 @@ public:
|
||||
return (uint8_t)(_gps_capacity / GPS_SAMPLES_PER_PAGE);
|
||||
}
|
||||
|
||||
uint16_t voltageSampleCount() const {
|
||||
return _tv_count;
|
||||
}
|
||||
|
||||
size_t formatTemperatureBinarySnapshot(
|
||||
const uint8_t source_id[BINARY_SOURCE_ID_SIZE],
|
||||
uint8_t* dest, size_t dest_size) const {
|
||||
return formatBinarySnapshot(false, source_id, dest, dest_size);
|
||||
}
|
||||
|
||||
size_t formatVoltageBinarySnapshot(
|
||||
const uint8_t source_id[BINARY_SOURCE_ID_SIZE],
|
||||
uint8_t* dest, size_t dest_size) const {
|
||||
return formatBinarySnapshot(true, source_id, dest, dest_size);
|
||||
}
|
||||
|
||||
private:
|
||||
// Formats a RAW_CUSTOM payload without Base64. The source ID is normally
|
||||
// the first eight bytes of the repeater public key. Samples are oldest
|
||||
// first, and the payload contains as many as fit, capped at 165.
|
||||
size_t formatBinarySnapshot(
|
||||
bool voltage_series, const uint8_t source_id[BINARY_SOURCE_ID_SIZE],
|
||||
uint8_t* dest, size_t dest_size) const {
|
||||
if (source_id == NULL || dest == NULL
|
||||
|| dest_size <= BINARY_HEADER_SIZE || !_has_bucket) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t sample_count = dest_size - BINARY_HEADER_SIZE;
|
||||
if (sample_count > BINARY_MAX_SAMPLES) {
|
||||
sample_count = BINARY_MAX_SAMPLES;
|
||||
}
|
||||
if (sample_count > _tv_count) sample_count = _tv_count;
|
||||
if (sample_count == 0) return 0;
|
||||
|
||||
memcpy(dest, voltage_series ? "TVB1" : "TTB1", 4);
|
||||
memcpy(&dest[4], source_id, BINARY_SOURCE_ID_SIZE);
|
||||
const uint16_t oldest_offset = (uint16_t)(sample_count - 1U);
|
||||
putUint32LE(&dest[12], startEpochForOffset(oldest_offset));
|
||||
putUint16LE(&dest[16], (uint16_t)(SAMPLE_INTERVAL_SECONDS / 60U));
|
||||
dest[18] = (uint8_t)sample_count;
|
||||
|
||||
for (uint16_t slot = 0; slot < sample_count; slot++) {
|
||||
uint8_t temperature = 0;
|
||||
uint8_t temperature_status = TEMPERATURE_NONE;
|
||||
uint8_t voltage = 0;
|
||||
tvAtOffset((uint16_t)(oldest_offset - slot), temperature,
|
||||
temperature_status, voltage);
|
||||
uint8_t value = voltage;
|
||||
if (!voltage_series) {
|
||||
if (temperature_status == TEMPERATURE_NONE) value = 0;
|
||||
else if (temperature_status == TEMPERATURE_LOW) value = 1;
|
||||
else if (temperature_status == TEMPERATURE_HIGH) value = 2;
|
||||
else value = (uint8_t)(temperature + 3U);
|
||||
}
|
||||
dest[BINARY_HEADER_SIZE + slot] = value;
|
||||
}
|
||||
return BINARY_HEADER_SIZE + sample_count;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
// Changes the logical GPS retention and preserves the newest samples. The
|
||||
// heap budget is the maximum additional allocation allowed for this call.
|
||||
// Requests above the budget are reduced one day at a time. The returned
|
||||
@@ -244,6 +314,8 @@ public:
|
||||
cursor = skipSpaces(cursor);
|
||||
if (*cursor != 0) return formatUsage(series, reply, reply_size);
|
||||
|
||||
#if !defined(MESH_ENABLE_TELEMETRY_GPS_HISTORY) \
|
||||
|| MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
||||
if (series == SERIES_GPS) {
|
||||
if (page < 1U || page > gpsPageCount()) {
|
||||
snprintf(reply, reply_size, "Err - telemetry.gps page must be 1-%u",
|
||||
@@ -252,6 +324,7 @@ public:
|
||||
}
|
||||
return formatGpsPage((uint8_t)page, reply, reply_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (page < 1U || page > TV_PAGE_COUNT) {
|
||||
copyReply(reply, reply_size, series == SERIES_TEMPERATURE
|
||||
@@ -506,6 +579,11 @@ private:
|
||||
dest[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
static void putUint16LE(uint8_t* dest, uint16_t value) {
|
||||
dest[0] = (uint8_t)value;
|
||||
dest[1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
static void putInt32LE(uint8_t* dest, int32_t value) {
|
||||
putUint32LE(dest, (uint32_t)value);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ uint32_t uint32LE(const uint8_t* source) {
|
||||
| ((uint32_t)source[3] << 24);
|
||||
}
|
||||
|
||||
uint16_t uint16LE(const uint8_t* source) {
|
||||
return (uint16_t)source[0] | ((uint16_t)source[1] << 8);
|
||||
}
|
||||
|
||||
int32_t int32LE(const uint8_t* source) {
|
||||
return (int32_t)uint32LE(source);
|
||||
}
|
||||
@@ -191,6 +195,95 @@ TEST(TelemetryHistory, RetainsExactlySevenTemperatureVoltageDays) {
|
||||
EXPECT_EQ(3U, voltage_payload[7]);
|
||||
}
|
||||
|
||||
TEST(TelemetryHistory, FormatsMaximumRawVoltageSnapshot) {
|
||||
using mesh::TelemetryHistory;
|
||||
TelemetryHistory history;
|
||||
const uint32_t first_bucket = 500000U;
|
||||
const uint32_t total_samples = 200U;
|
||||
for (uint32_t sample = 0; sample < total_samples; sample++) {
|
||||
history.record((first_bucket + sample)
|
||||
* TelemetryHistory::SAMPLE_INTERVAL_SECONDS,
|
||||
20, true,
|
||||
(uint16_t)(1880U + (sample % 253U) * 10U),
|
||||
0, 0, false);
|
||||
}
|
||||
|
||||
const uint8_t source_id[TelemetryHistory::BINARY_SOURCE_ID_SIZE] = {
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
|
||||
uint8_t payload[TelemetryHistory::BINARY_PAYLOAD_SIZE];
|
||||
ASSERT_EQ(sizeof(payload), history.formatVoltageBinarySnapshot(
|
||||
source_id, payload, sizeof(payload)));
|
||||
EXPECT_EQ(0, memcmp(payload, "TVB1", 4));
|
||||
EXPECT_EQ(0, memcmp(&payload[4], source_id, sizeof(source_id)));
|
||||
|
||||
const uint32_t oldest_sample =
|
||||
total_samples - TelemetryHistory::BINARY_MAX_SAMPLES;
|
||||
EXPECT_EQ((first_bucket + oldest_sample)
|
||||
* TelemetryHistory::SAMPLE_INTERVAL_SECONDS,
|
||||
uint32LE(&payload[12]));
|
||||
EXPECT_EQ(30, uint16LE(&payload[16]));
|
||||
EXPECT_EQ(TelemetryHistory::BINARY_MAX_SAMPLES, payload[18]);
|
||||
for (uint16_t slot = 0;
|
||||
slot < TelemetryHistory::BINARY_MAX_SAMPLES; slot++) {
|
||||
const uint32_t sample = oldest_sample + slot;
|
||||
EXPECT_EQ(TelemetryHistory::encodeVoltage(
|
||||
(uint16_t)(1880U + (sample % 253U) * 10U)),
|
||||
payload[TelemetryHistory::BINARY_HEADER_SIZE + slot]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TelemetryHistory, FormatsMaximumRawTemperatureSnapshot) {
|
||||
using mesh::TelemetryHistory;
|
||||
TelemetryHistory history;
|
||||
const uint32_t first_bucket = 700000U;
|
||||
const uint32_t total_samples = TelemetryHistory::BINARY_MAX_SAMPLES;
|
||||
for (uint32_t sample = 0; sample < total_samples; sample++) {
|
||||
const int16_t temperature = sample == 0 ? -51
|
||||
: sample == 1 ? 78 : (int16_t)(-50 + sample % 128U);
|
||||
history.record((first_bucket + sample)
|
||||
* TelemetryHistory::SAMPLE_INTERVAL_SECONDS,
|
||||
temperature, sample != 2, 3700, 0, 0, false);
|
||||
}
|
||||
|
||||
const uint8_t source_id[TelemetryHistory::BINARY_SOURCE_ID_SIZE] = {
|
||||
0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE};
|
||||
uint8_t payload[TelemetryHistory::BINARY_PAYLOAD_SIZE];
|
||||
ASSERT_EQ(sizeof(payload), history.formatTemperatureBinarySnapshot(
|
||||
source_id, payload, sizeof(payload)));
|
||||
EXPECT_EQ(0, memcmp(payload, "TTB1", 4));
|
||||
EXPECT_EQ(0, memcmp(&payload[4], source_id, sizeof(source_id)));
|
||||
EXPECT_EQ(first_bucket * TelemetryHistory::SAMPLE_INTERVAL_SECONDS,
|
||||
uint32LE(&payload[12]));
|
||||
EXPECT_EQ(30, uint16LE(&payload[16]));
|
||||
EXPECT_EQ(TelemetryHistory::BINARY_MAX_SAMPLES, payload[18]);
|
||||
EXPECT_EQ(1, payload[TelemetryHistory::BINARY_HEADER_SIZE]);
|
||||
EXPECT_EQ(2, payload[TelemetryHistory::BINARY_HEADER_SIZE + 1U]);
|
||||
EXPECT_EQ(0, payload[TelemetryHistory::BINARY_HEADER_SIZE + 2U]);
|
||||
for (uint16_t slot = 3; slot < TelemetryHistory::BINARY_MAX_SAMPLES; slot++) {
|
||||
EXPECT_EQ((uint8_t)(slot % 128U + 3U),
|
||||
payload[TelemetryHistory::BINARY_HEADER_SIZE + slot]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TelemetryHistory, RawVoltageSnapshotRequiresHistoryAndDataSpace) {
|
||||
using mesh::TelemetryHistory;
|
||||
TelemetryHistory history;
|
||||
const uint8_t source_id[TelemetryHistory::BINARY_SOURCE_ID_SIZE] = {};
|
||||
uint8_t payload[TelemetryHistory::BINARY_PAYLOAD_SIZE];
|
||||
|
||||
EXPECT_EQ(0U, history.formatVoltageBinarySnapshot(
|
||||
source_id, payload, sizeof(payload)));
|
||||
history.record(1800000000U, 20, true, 3700, 0, 0, false);
|
||||
EXPECT_EQ(0U, history.formatVoltageBinarySnapshot(
|
||||
source_id, payload,
|
||||
TelemetryHistory::BINARY_HEADER_SIZE));
|
||||
EXPECT_EQ(TelemetryHistory::BINARY_HEADER_SIZE + 1U,
|
||||
history.formatVoltageBinarySnapshot(
|
||||
source_id, payload,
|
||||
TelemetryHistory::BINARY_HEADER_SIZE + 1U));
|
||||
EXPECT_EQ(1, payload[18]);
|
||||
}
|
||||
|
||||
TEST(TelemetryHistory, EmitsZeroGpsOriginAndDeltasWithoutFixes) {
|
||||
using mesh::TelemetryHistory;
|
||||
TelemetryHistory history;
|
||||
|
||||
Reference in New Issue
Block a user