From bbe29fbf95e4e46cdf6946b65358c76830fd9a6a Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 23 Oct 2026 21:32:47 -0700 Subject: [PATCH] Add saved telemetry packet type controls --- docs/cli_commands.md | 69 ++++- docs/telemetry_decoder.md | 69 ++++- examples/simple_repeater/MyMesh.cpp | 175 +++++++++--- examples/simple_repeater/MyMesh.h | 5 + .../fixtures/telemetry_tx_command/fixture.cpp | 16 +- .../telemetry_tx_persistence/fixture.cpp | 262 +++++++++++++++++- test/test_telemetry_tx_persistence.py | 11 +- 7 files changed, 519 insertions(+), 88 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8f4a1f403..3497c1ad1 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -756,6 +756,8 @@ estimates rather than received-packet RSSI. Main-radio and radio2 history are kept separate; failed measurements retain a Missing position in the timeline. The feature is omitted from flash-constrained STM32 repeater and room images. +The `telemetry.tx` commands, including packet-type selections, are repeater-only. +Room servers provide local history and its CLI reads, without batch transmission. **Usage:** @@ -769,6 +771,10 @@ The feature is omitted from flash-constrained STM32 repeater and room images. - `get telemetry.tx` - `set telemetry.tx ` - `set telemetry.tx schedule ` +- `get telemetry.tx.types` +- `set telemetry.tx.types ` +- `get telemetry.tx.` +- `set telemetry.tx. ` - `send telemetry.tx now` - `get data.tx` - `get data.tx path` @@ -806,6 +812,7 @@ The feature is omitted from flash-constrained STM32 repeater and room images. - `schedule`: Automatic interval in whole days. Automatic transmission is off on a fresh install. The retained cadence defaults to `2d`; `off` retains the shared route for manual test sends. +- `type`: `temp`, `volt`, `volt.i2c`, `noise`, or `noise2`, as listed below. Local serial and remote administrator CLI sessions can read the history on both roles. Collection uses the MCU temperature, battery voltage, external I2C @@ -845,12 +852,40 @@ zero-hop `direct` plus `region=auto`, but it does not enable telemetry or any other producer. For compatibility, `set telemetry.tx direct|path` updates the shared path and enables the default `2d` schedule; new configuration should use `set data.tx path ...` followed by `set telemetry.tx schedule ...`. -An automatic run waits until 165 half-hour positions are -available, then sends one maximum-size temperature packet, one maximum-size -battery-voltage packet, and up to three `IVB1` packets for every populated I2C -voltage channel, plus the collected noise-floor snapshots. GPS is never included -in this raw transmission. The history -remains boot-local, so the first scheduled run after a reboot needs about 82.5 + +Each packet type has a separate saved on/off setting. All five default to +`on`; this does not enable the automatic schedule. These settings select both +manual and scheduled transmissions while history sampling and CLI reads +continue normally. + +| Setting | Selected packets | +|---|---| +| `telemetry.tx.temp` | MCU temperature (`TTB1`) | +| `telemetry.tx.volt` | Battery voltage (`TVB1`) | +| `telemetry.tx.volt.i2c` | External I2C voltage (`IVB1`), all monitor channels | +| `telemetry.tx.noise` | Main-radio noise floor (`N1B1`) | +| `telemetry.tx.noise2` | Radio2 noise floor (`N2B1`) | + +For example, omit noise packets while keeping the other selections: + +```text +set telemetry.tx.noise off +set telemetry.tx.noise2 off +get telemetry.tx.types +``` + +`get telemetry.tx.noise` reads one selection. `set telemetry.tx.types on` +restores all five selections; `set telemetry.tx.types off` disables them all. +Turning a type off cancels its batch work that has not yet been queued. Packets +already queued can still transmit. The remaining batch keeps its 90-second +pacing. Turning a type on includes it in the next batch. + +An automatic run still waits until 165 half-hour positions are available, +regardless of the selected types. It then sends the selected temperature and +battery-voltage snapshots, up to three `IVB1` packets for every populated I2C +voltage channel when selected, and the selected noise-floor snapshots. GPS is +never included in this raw transmission. The history remains boot-local, so +the first scheduled run after a reboot needs about 82.5 hours to fill. On the default two-day interval, each 82.5-hour temperature or battery packet overlaps its predecessor by 34.5 hours. Packets are queued 90 seconds apart. A queue failure is retried after 30 minutes only for the packet @@ -870,16 +905,20 @@ Queue acceptance and relayed telemetry do not update it, and it does not confirm observer receipt. It shows `last=never` until the first completed send after a reboot. The age uses uptime, so clock corrections do not affect it. -`send telemetry.tx now` is an administrator-only test action. It queues -temperature and battery-voltage snapshots plus the available I2C voltage -chunks over the configured path. It also queues one main-radio `N1B1` packet -once a noise bucket has completed and one `N2B1` packet when radio2 is enabled -and has a completed bucket. Missing positions are included; an all-Missing -history is still sent. It uses all currently available base -positions up to 165 and all available I2C positions up to 192 per populated -channel, plus the latest 255 positions per noise profile (63 hours 45 minutes). +`send telemetry.tx now` is an administrator-only test action. It queues the +selected temperature and battery-voltage snapshots plus selected, available +I2C voltage chunks over the configured path. When selected, it also queues +one main-radio `N1B1` packet once a noise bucket has completed and one `N2B1` +packet when radio2 is enabled and has a completed bucket. Missing positions +are included; an all-Missing history is still sent. It uses all currently +available base positions up to 165 and all available I2C positions up to 192 +per populated channel, plus the latest 255 positions per noise profile +(63 hours 45 minutes). It works while the schedule is off and does not move the next -scheduled send time. +scheduled send time. Selected I2C or noise data can be sent without any +temperature or battery history. With all types off, it returns +`Err - no telemetry packet types enabled`. If selected types have no data +available, it returns `Err - no enabled telemetry data available`. With a non-empty hop path, `TTB1`, `TVB1`, `IVB1`, `N1B1`, and `N2B1` telemetry use the existing direct-message echo retries. They follow `direct.retry`, diff --git a/docs/telemetry_decoder.md b/docs/telemetry_decoder.md index 214d2d1e7..83258e259 100644 --- a/docs/telemetry_decoder.md +++ b/docs/telemetry_decoder.md @@ -22,6 +22,9 @@ so it is available even when only the payload was copied. ## Send raw telemetry packets +These transmission commands and packet-type selections are repeater-only. +Room servers provide local history and CLI reads without transmission batches. + From an administrator CLI session, first configure a direct route to the MQTT observer that will receive and upload the raw packets. Use `direct` when the observer is a zero-hop neighbor: @@ -55,24 +58,60 @@ set telemetry.tx schedule off send telemetry.tx now ``` -The `send telemetry.tx now` command queues one `TTB1` temperature packet, one -`TVB1` battery-voltage packet, and `IVB1` chunks for every connected I2C voltage -channel. `TTB1` and `TVB1` carry up to 165 samples. Each `IVB1` carries 64, so a -full 192-point/four-day channel takes three packets. Channels whose retained -history is entirely zero are treated as disconnected and are not sent. The -command requires at least one collected base sample and works even when the -automatic schedule is off. +Choose which packet types to send with these saved settings: + +| Setting | Data | +|---|---| +| `telemetry.tx.temp` | Temperature (`TTB1`) | +| `telemetry.tx.volt` | Battery voltage (`TVB1`) | +| `telemetry.tx.volt.i2c` | External I2C voltage (`IVB1`), all channels | +| `telemetry.tx.noise` | Main-radio noise floor (`N1B1`) | +| `telemetry.tx.noise2` | Radio2 noise floor (`N2B1`) | + +All five default to `on`. Read one with `get telemetry.tx.temp`, change it +with `set telemetry.tx.temp `, or inspect all five with +`get telemetry.tx.types`. `set telemetry.tx.types on|off` changes all five. +The choices apply to manual and scheduled sends. They do not enable the master +schedule or stop history collection and CLI reads. GPS is never included in +these packets. + +For example, send only battery voltage: + +```text +set telemetry.tx.types off +set telemetry.tx.volt on +send telemetry.tx now +``` + +With all types off, a manual send returns +`Err - no telemetry packet types enabled`. If the selected types have no data +available, it returns `Err - no enabled telemetry data available`. + +With all types selected, `send telemetry.tx now` queues one `TTB1` temperature +packet, one `TVB1` battery-voltage packet, and `IVB1` chunks for every connected +I2C voltage channel. `TTB1` and `TVB1` carry up to 165 samples. Each `IVB1` +carries 64, so a full 192-point/four-day channel takes three packets. Channels +whose retained history is entirely zero are treated as disconnected and are +not sent. The +command works even when the automatic schedule is off. Selected I2C or noise +data can be sent without a collected temperature or battery sample. Packets in manual and scheduled batches are queued 90 seconds apart. Channel -activity and airtime limits can delay actual transmission further. +activity and airtime limits can delay actual transmission further. Turning a +type off cancels only its batch work that has not yet been queued; packets +already queued can still transmit. Remaining work keeps the same pacing. +Turning a type on includes it in the next batch. -It also queues a `N1B1` noise-floor packet once the main radio has completed a -15-minute history bucket. With radio2 enabled, it queues a separate `N2B1` -packet for that profile once it has a completed bucket. Each holds the latest -255 positions: up to 63 hours 45 minutes at 15-minute resolution. A position -can be Missing; failed measurements do not silently shorten the timeline. -The existing automatic transmission cadence is unchanged. Recording a new -noise-floor position every 15 minutes does not transmit a packet every 15 minutes. +When selected, it also queues a `N1B1` noise-floor packet once the main radio +has completed a 15-minute history bucket. With radio2 enabled and `noise2` +selected, it queues a separate `N2B1` packet once that profile has a completed +bucket. Each holds the latest 255 positions: up to 63 hours 45 minutes at +15-minute resolution. A position can be Missing; failed measurements do not +silently shorten the timeline. +The automatic transmission cadence and startup wait for 165 half-hour base +positions (about 82.5 hours) are unchanged, including when only noise or I2C +transmission is selected. Recording a new noise-floor position every 15 minutes +does not transmit a packet every 15 minutes. Use `get telemetry.tx` to see the source ID, path, and `last=30s`, `last=30m`, or `last=2h` for the age of this node's latest completed telemetry transmission. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 34fed4fca..dc345efce 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -247,6 +247,7 @@ static const uint8_t TELEMETRY_HISTORY_TX_VOLTAGE = 2U; static const uint8_t TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE = 4U; static const uint8_t TELEMETRY_HISTORY_TX_NOISE = 8U; static const uint8_t TELEMETRY_HISTORY_TX_NOISE2 = 16U; +static const uint8_t TELEMETRY_HISTORY_TX_ALL_TYPES = 31U; #endif #define CLOCK_SYNC_VALID_YEARS 10 @@ -3743,6 +3744,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc telemetry_history_tx_manual = false; telemetry_history_tx_external_channel = 0; telemetry_history_tx_external_chunk = 0; + telemetry_history_tx_types = TELEMETRY_HISTORY_TX_ALL_TYPES; telemetry_history_next_tx_uptime = 0; telemetry_history_tx_resume_uptime = 0; telemetry_history_last_tx_uptime = UINT64_MAX; @@ -12380,6 +12382,9 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * "set telemetry.tx schedule"; static const char telemetry_tx_send_now_command[] = "send telemetry.tx now"; + if (handleTelemetryHistoryTxTypesCommand(command, reply, + sender == NULL || sender->isAdmin())) return; + if (strcmp(command, telemetry_tx_send_now_command) == 0) { if (sender != NULL && !sender->isAdmin()) { strcpy(reply, "Err - not permitted"); @@ -12399,21 +12404,19 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * strcpy(reply, "OK - telemetry.tx already queued"); return; } - if (telemetry_history.voltageSampleCount() == 0) { - strcpy(reply, "Err - telemetry history is empty"); + if (telemetry_history_tx_types == 0) { + strcpy(reply, "Err - no telemetry packet types enabled"); return; } - telemetry_history_tx_pending = TELEMETRY_HISTORY_TX_TEMPERATURE - | TELEMETRY_HISTORY_TX_VOLTAGE; - if (noise_floor_history.sampleCount(0) != 0) - telemetry_history_tx_pending |= TELEMETRY_HISTORY_TX_NOISE; - if (isDualRadioActive() && noise_floor_history.sampleCount(1) != 0) - telemetry_history_tx_pending |= TELEMETRY_HISTORY_TX_NOISE2; - const uint8_t external_channels = - external_voltage_history.populatedChannelCount(); - if (external_channels != 0) { - telemetry_history_tx_pending |= TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE; + const uint8_t pending = availableTelemetryHistoryTxTypes(); + if (pending == 0) { + strcpy(reply, "Err - no enabled telemetry data available"); + return; } + telemetry_history_tx_pending = pending; + const uint8_t external_channels = + pending & TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE + ? external_voltage_history.populatedChannelCount() : 0; if (!telemetry_history_tx_manual) { telemetry_history_tx_resume_uptime = telemetry_history_next_tx_uptime; } @@ -12430,7 +12433,9 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * * external_voltage_history.binaryChunkCount(); snprintf(reply, 160, "OK - telemetry.tx queued temp=%u volt=%u i2c=%u/%u noise=%u/%u", - sent, sent, (unsigned)external_channels, external_packets, + pending & TELEMETRY_HISTORY_TX_TEMPERATURE ? sent : 0U, + pending & TELEMETRY_HISTORY_TX_VOLTAGE ? sent : 0U, + (unsigned)external_channels, external_packets, (telemetry_history_tx_pending & TELEMETRY_HISTORY_TX_NOISE) != 0, (telemetry_history_tx_pending & TELEMETRY_HISTORY_TX_NOISE2) != 0); return; @@ -12576,7 +12581,7 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * telemetry_history_tx_resume_uptime = previous_resume_tx; 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", + snprintf(reply, 160, "OK - telemetry.tx schedule=%ud", (unsigned)telemetry_history_tx_interval_days); } else { strcpy(reply, "OK - telemetry.tx off"); @@ -13252,6 +13257,98 @@ uint8_t MyMesh::resizeTelemetryGpsDays(uint8_t requested_days) { } #endif +uint8_t MyMesh::availableTelemetryHistoryTxTypes() const { + uint8_t available = 0; + if (telemetry_history.voltageSampleCount() != 0) + available |= TELEMETRY_HISTORY_TX_TEMPERATURE | TELEMETRY_HISTORY_TX_VOLTAGE; + if (external_voltage_history.populatedChannelCount() != 0) + available |= TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE; + if (noise_floor_history.sampleCount(0) != 0) + available |= TELEMETRY_HISTORY_TX_NOISE; + if (isDualRadioActive() && noise_floor_history.sampleCount(1) != 0) + available |= TELEMETRY_HISTORY_TX_NOISE2; + return available & telemetry_history_tx_types; +} + +void MyMesh::finishTelemetryHistoryTxBatch(uint64_t now) { + if (telemetry_history_tx_manual) { + telemetry_history_next_tx_uptime = telemetry_history_tx_resume_uptime; + telemetry_history_tx_manual = false; + telemetry_history_tx_resume_uptime = 0; + } else { + telemetry_history_next_tx_uptime = telemetry_history_tx_enabled + ? now + (uint64_t)telemetry_history_tx_interval_days + * 24ULL * 60ULL * 60ULL * 1000ULL + : 0; + } +} + +bool MyMesh::handleTelemetryHistoryTxTypesCommand(char* command, char* reply, + bool permitted) { + const bool get = strncmp(command, "get telemetry.tx.", 17) == 0; + const bool set = strncmp(command, "set telemetry.tx.", 17) == 0; + if (!get && !set) return false; + if (!permitted) { strcpy(reply, "Err - not permitted"); return true; } + + static const char* const names[] = { + "temp", "volt", "volt.i2c", "noise", "noise2", "types"}; + char* key = command + 17; + char* value = strchr(key, ' '); + const size_t length = value ? (size_t)(value - key) : strlen(key); + unsigned index = 0; + while (index < sizeof(names) / sizeof(names[0]) + && (strlen(names[index]) != length || memcmp(key, names[index], length))) ++index; + if (index == sizeof(names) / sizeof(names[0])) { + strcpy(reply, "Err - unknown telemetry packet type"); + return true; + } + const uint8_t bit = index == 5 ? TELEMETRY_HISTORY_TX_ALL_TYPES : uint8_t(1U << index); + if (get && value) { strcpy(reply, "Err - unexpected arguments"); return true; } + if (set) { + value = value ? trimSpaces(value) : NULL; + if (!value || (strcmp(value, "on") && strcmp(value, "off"))) { + strcpy(reply, "Err - use on or off"); + return true; + } + } + if (!telemetry_history_tx_prefs_healthy) { + strcpy(reply, "Err - telemetry.tx prefs unavailable"); + return true; + } + if (get) { + if (index == 5) { + snprintf(reply, 160, "> temp=%s volt=%s volt.i2c=%s noise=%s noise2=%s", + telemetry_history_tx_types & TELEMETRY_HISTORY_TX_TEMPERATURE ? "on" : "off", + telemetry_history_tx_types & TELEMETRY_HISTORY_TX_VOLTAGE ? "on" : "off", + telemetry_history_tx_types & TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE ? "on" : "off", + telemetry_history_tx_types & TELEMETRY_HISTORY_TX_NOISE ? "on" : "off", + telemetry_history_tx_types & TELEMETRY_HISTORY_TX_NOISE2 ? "on" : "off"); + } else { + snprintf(reply, 160, "> %s", telemetry_history_tx_types & bit ? "on" : "off"); + } + return true; + } + const uint8_t previous = telemetry_history_tx_types; + telemetry_history_tx_types = !strcmp(value, "on") ? previous | bit : previous & ~bit; + if (telemetry_history_tx_types != previous && !saveTelemetryHistoryTxPrefs()) { + telemetry_history_tx_types = previous; + strcpy(reply, "Err - unable to save telemetry.tx types"); + return true; + } + // Already admitted radio packets belong to the TX queue. Only future batch + // work can be cancelled, and newly enabled types join the next batch. + const uint8_t pending = telemetry_history_tx_pending; + telemetry_history_tx_pending &= telemetry_history_tx_types; + if (!(telemetry_history_tx_types & TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE)) { + telemetry_history_tx_external_channel = 0; + telemetry_history_tx_external_chunk = 0; + } + if (pending && !telemetry_history_tx_pending) + finishTelemetryHistoryTxBatch(uptime_millis + (uint32_t)(millis() - last_millis)); + snprintf(reply, 160, "OK - telemetry.tx.%s=%s", names[index], value); + return true; +} + static bool recoverTelemetryHistoryTxPrefs(FILESYSTEM* fs) { if (fs == NULL) return false; #if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) @@ -13268,12 +13365,15 @@ static bool readTelemetryHistoryTxPrefsImage(FILESYSTEM* fs, File file = openFloodSettingsRead(fs, TELEMETRY_HISTORY_TX_PREFS_FILE); if (!file) return false; size = file.size(); - const bool complete = (size == 6 || size == 7 + MAX_PATH_SIZE) + const bool complete = (size == 6 || size == 7 || size == 7 + MAX_PATH_SIZE) && file.read(image, size) == size; file.close(); if (!complete) return false; - const bool legacy = size == 7 + MAX_PATH_SIZE; - if (memcmp(image, legacy ? "THT2" : "THT3", 4) || image[4] > 1) return false; + const bool legacy = size == 7 + MAX_PATH_SIZE && !memcmp(image, "THT2", 4); + const bool previous = size == 6 && !memcmp(image, "THT3", 4); + const bool selective = size == 7 && !memcmp(image, "THT4", 4); + if ((!legacy && !previous && !selective) || image[4] > 1 + || (selective && (image[6] & ~TELEMETRY_HISTORY_TX_ALL_TYPES))) return false; const uint8_t days = image[legacy ? 6 : 5]; if (days < 1 || days > TELEMETRY_HISTORY_TX_MAX_DAYS) return false; return !legacy || (image[5] == OUT_PATH_UNKNOWN ? image[4] == 0 @@ -13288,6 +13388,7 @@ void MyMesh::loadTelemetryHistoryTxPrefs() { telemetry_history_tx_manual = false; telemetry_history_tx_external_channel = 0; telemetry_history_tx_external_chunk = 0; + telemetry_history_tx_types = TELEMETRY_HISTORY_TX_ALL_TYPES; telemetry_history_next_tx_uptime = 0; telemetry_history_tx_resume_uptime = 0; @@ -13297,13 +13398,14 @@ void MyMesh::loadTelemetryHistoryTxPrefs() { if (!present) { telemetry_history_tx_prefs_healthy = true; return; } uint8_t image[7 + MAX_PATH_SIZE]; size_t size = 0; if (!readTelemetryHistoryTxPrefsImage(_fs, image, size)) return; - const bool legacy = size == sizeof(image); + const bool legacy = !memcmp(image, "THT2", 4); // Preserve the only durable old route until shared-route adoption succeeds. // Otherwise an enabled legacy producer would silently use fresh zero-hop. if (legacy && image[5] != OUT_PATH_UNKNOWN && !_cli.adoptLegacyDataTxPath(image + 7, image[5])) return; telemetry_history_tx_enabled = image[4] != 0; telemetry_history_tx_interval_days = image[legacy ? 6 : 5]; + if (!memcmp(image, "THT4", 4)) telemetry_history_tx_types = image[6]; telemetry_history_tx_prefs_healthy = true; } @@ -13316,8 +13418,9 @@ bool MyMesh::saveTelemetryHistoryTxPrefs() { || (previous_present && !readTelemetryHistoryTxPrefsImage(_fs, previous, previous_size))) { telemetry_history_tx_prefs_healthy = false; return false; } - const uint8_t image[] = {'T', 'H', 'T', '3', uint8_t(telemetry_history_tx_enabled), - telemetry_history_tx_interval_days}; + const uint8_t image[] = {'T', 'H', 'T', '4', uint8_t(telemetry_history_tx_enabled), + telemetry_history_tx_interval_days, + telemetry_history_tx_types}; bool success; { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -13439,6 +13542,14 @@ void MyMesh::serviceTelemetryHistoryTx() { if (!telemetry_history_tx_prefs_healthy) return; const uint64_t current_uptime_millis = uptime_millis + (uint32_t)(millis() - last_millis); + if (telemetry_history_tx_pending != 0) { + telemetry_history_tx_pending &= telemetry_history_tx_types; + if (telemetry_history_tx_pending == 0) { + finishTelemetryHistoryTxBatch(current_uptime_millis); + return; + } + } + if (telemetry_history_tx_types == 0) return; if (telemetry_history_next_tx_uptime != 0 && current_uptime_millis < telemetry_history_next_tx_uptime) { return; @@ -13450,20 +13561,14 @@ void MyMesh::serviceTelemetryHistoryTx() { < mesh::TelemetryHistory::BINARY_MAX_SAMPLES) { return; } + const uint8_t available = availableTelemetryHistoryTxTypes(); + if (available == 0) return; const uint8_t* data_path = NULL; uint8_t data_path_len = OUT_PATH_UNKNOWN; if (!_cli.getDataTxPath(data_path, data_path_len)) return; telemetry_history_tx_manual = false; telemetry_history_tx_resume_uptime = 0; - telemetry_history_tx_pending = TELEMETRY_HISTORY_TX_TEMPERATURE - | TELEMETRY_HISTORY_TX_VOLTAGE; - if (noise_floor_history.sampleCount(0) != 0) - telemetry_history_tx_pending |= TELEMETRY_HISTORY_TX_NOISE; - if (isDualRadioActive() && noise_floor_history.sampleCount(1) != 0) - telemetry_history_tx_pending |= TELEMETRY_HISTORY_TX_NOISE2; - if (external_voltage_history.populatedChannelCount() != 0) { - telemetry_history_tx_pending |= TELEMETRY_HISTORY_TX_EXTERNAL_VOLTAGE; - } + telemetry_history_tx_pending = available; telemetry_history_tx_external_channel = 0; telemetry_history_tx_external_chunk = 0; } @@ -13526,17 +13631,7 @@ void MyMesh::serviceTelemetryHistoryTx() { } if (telemetry_history_tx_pending == 0) { - if (telemetry_history_tx_manual) { - telemetry_history_next_tx_uptime = telemetry_history_tx_resume_uptime; - telemetry_history_tx_manual = false; - telemetry_history_tx_resume_uptime = 0; - } else { - telemetry_history_next_tx_uptime = telemetry_history_tx_enabled - ? current_uptime_millis - + (uint64_t)telemetry_history_tx_interval_days - * 24ULL * 60ULL * 60ULL * 1000ULL - : 0; - } + finishTelemetryHistoryTxBatch(current_uptime_millis); } else { telemetry_history_next_tx_uptime = current_uptime_millis + TELEMETRY_HISTORY_TX_PACKET_SPACING_MILLIS; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 4e3e6d3eb..a939d4e9a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -593,6 +593,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks bool telemetry_history_tx_manual; uint8_t telemetry_history_tx_external_channel; uint8_t telemetry_history_tx_external_chunk; + uint8_t telemetry_history_tx_types; uint64_t telemetry_history_next_tx_uptime; uint64_t telemetry_history_tx_resume_uptime; uint64_t telemetry_history_last_tx_uptime; @@ -878,6 +879,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks #endif void loadTelemetryHistoryTxPrefs(); bool saveTelemetryHistoryTxPrefs(); + uint8_t availableTelemetryHistoryTxTypes() const; + void finishTelemetryHistoryTxBatch(uint64_t now); + bool handleTelemetryHistoryTxTypesCommand(char* command, char* reply, + bool permitted); void serviceTelemetryHistoryTx(); bool sendTelemetryHistorySnapshot(mesh::TelemetryHistory::Series series); bool sendNoiseFloorHistorySnapshot(uint8_t profile); diff --git a/test/fixtures/telemetry_tx_command/fixture.cpp b/test/fixtures/telemetry_tx_command/fixture.cpp index 8450b4d05..d8d93e5a7 100644 --- a/test/fixtures/telemetry_tx_command/fixture.cpp +++ b/test/fixtures/telemetry_tx_command/fixture.cpp @@ -29,6 +29,7 @@ struct Fixture { SharedRoute _cli; bool telemetry_history_tx_enabled = true; uint8_t telemetry_history_tx_interval_days = 7; + uint8_t telemetry_history_tx_types = 31; uint8_t telemetry_history_tx_pending = 7; bool telemetry_history_tx_manual = true; uint8_t telemetry_history_tx_external_channel = 2; @@ -37,13 +38,14 @@ struct Fixture { uint64_t telemetry_history_tx_resume_uptime = 20000000000ULL; bool fail_save = false; unsigned saves = 0; - std::array persisted = state(); + std::array persisted = state(); - std::array state() const { + std::array state() const { return {{telemetry_history_tx_enabled, telemetry_history_tx_interval_days, telemetry_history_tx_pending, telemetry_history_tx_manual, telemetry_history_tx_external_channel, telemetry_history_tx_external_chunk, - telemetry_history_next_tx_uptime, telemetry_history_tx_resume_uptime}}; + telemetry_history_next_tx_uptime, telemetry_history_tx_resume_uptime, + telemetry_history_tx_types}}; } bool saveTelemetryHistoryTxPrefs() { ++saves; @@ -63,20 +65,24 @@ struct Fixture { // Includes the complete actual schedule-command branch and trim helper. #include "production_telemetry_tx.inc" -static void assertUnchanged(const Fixture& f, const std::array& before) { +static void assertUnchanged(const Fixture& f, const std::array& before) { assert(f.state() == before && f.persisted == before && f.saves == 0); } int main() { + for (uint8_t types = 0; types < 32; ++types) { for (const std::string value : {"1", "30", "30d", " 0005d ", " 5 "}) { Fixture f; + f.telemetry_history_tx_types = types; const unsigned expected = value.find('3') != std::string::npos ? 30 : value.find('5') != std::string::npos ? 5 : 1; assert(f.command(value) == "OK - telemetry.tx schedule=" + std::to_string(expected) + "d"); assert(f.telemetry_history_tx_enabled && f.telemetry_history_tx_interval_days == expected); assert(f.saves == 1 && f.persisted == f.state()); const auto state = f.state(); - for (size_t i = 2; i < state.size(); ++i) assert(state[i] == 0); + for (size_t i = 2; i < 8; ++i) assert(state[i] == 0); + assert(state[8] == types); // schedule edits preserve every independent selection + } } { Fixture f; diff --git a/test/fixtures/telemetry_tx_persistence/fixture.cpp b/test/fixtures/telemetry_tx_persistence/fixture.cpp index 3b82a5cdd..c55ef40d7 100644 --- a/test/fixtures/telemetry_tx_persistence/fixture.cpp +++ b/test/fixtures/telemetry_tx_persistence/fixture.cpp @@ -98,6 +98,7 @@ public: bool telemetry_history_tx_prefs_healthy = false; bool telemetry_history_tx_enabled = false; uint8_t telemetry_history_tx_interval_days = 2; + uint8_t telemetry_history_tx_types = 31; uint8_t telemetry_history_tx_pending = 0; bool telemetry_history_tx_manual = false; uint8_t telemetry_history_tx_external_channel = 0; @@ -108,12 +109,16 @@ public: uint64_t uptime_millis = 0; uint32_t last_millis = 0; std::vector packets; + std::vector packet_times; std::vector attempts; std::string reject_magic; unsigned rejected_sends = 0; explicit MyMesh(FILESYSTEM& fs) : _fs(&fs) {} void loadTelemetryHistoryTxPrefs(); bool saveTelemetryHistoryTxPrefs(); + uint8_t availableTelemetryHistoryTxTypes() const; + void finishTelemetryHistoryTxBatch(uint64_t); + bool handleTelemetryHistoryTxTypesCommand(char*, char*, bool); void serviceTelemetryHistoryTx(); bool sendTelemetryHistorySnapshot(mesh::TelemetryHistory::Series); bool sendExternalVoltageHistorySnapshot(uint8_t, uint8_t); @@ -135,13 +140,16 @@ public: --rejected_sends; delete packet; return false; } packet->path_len = mesh::Packet::copyPath(packet->path, path, length); - packets.push_back(*packet); delete packet; return true; + packets.push_back(*packet); + packet_times.push_back(uptime_millis + uint32_t(millis() - last_millis)); + delete packet; return true; } - std::array state() const { + std::array state() const { return {{telemetry_history_tx_enabled, telemetry_history_tx_interval_days, telemetry_history_tx_pending, telemetry_history_tx_manual, telemetry_history_tx_external_channel, telemetry_history_tx_external_chunk, - telemetry_history_next_tx_uptime, telemetry_history_tx_resume_uptime}}; + telemetry_history_next_tx_uptime, telemetry_history_tx_resume_uptime, + telemetry_history_tx_types}}; } std::string command(const char* text, ClientInfo* sender = nullptr) { std::vector input(text, text + strlen(text) + 1); @@ -154,7 +162,11 @@ public: static const char target[] = "/telemetry_tx"; static const char temp[] = "/telemetry_tx.tmp"; static const char backup[] = "/telemetry_tx.bak"; -static std::vector current(bool enabled = true, uint8_t days = 7) { +static std::vector current(bool enabled = true, uint8_t days = 7, + uint8_t types = 31) { + return {'T', 'H', 'T', '4', uint8_t(enabled), days, types}; +} +static std::vector previous(bool enabled = true, uint8_t days = 7) { return {'T', 'H', 'T', '3', uint8_t(enabled), days}; } static std::vector legacy(bool enabled = true, uint8_t length = 1, @@ -212,6 +224,12 @@ struct Fixture { assert(node.packets.empty() && node.state() == before); assert(node.command("send telemetry.tx now") == "Err - telemetry.tx prefs unavailable"); assert(node.state() == before && fs.writes == writes); + for (const char* key : {"temp", "volt", "volt.i2c", "noise", "noise2", "types"}) { + const std::string prefix = std::string("telemetry.tx.") + key; + assert(node.command(("get " + prefix).c_str()) == "Err - telemetry.tx prefs unavailable"); + assert(node.command(("set " + prefix + " off").c_str()) == "Err - telemetry.tx prefs unavailable"); + assert(node.state() == before && fs.writes == writes); + } char reply[160]; node.formatTelemetryHistoryTxStatus(reply, sizeof(reply)); assert(strstr(reply, " fault=prefs")); ClientInfo unauthorized; unauthorized.admin = false; @@ -224,6 +242,7 @@ static void checkFreshAndValid() { Fixture f; f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy); assert(!f.node.telemetry_history_tx_enabled && f.node.telemetry_history_tx_interval_days == 2); + assert(f.node.telemetry_history_tx_types == 31); assert(f.fs.files.empty()); for (const bool enabled : {false, true}) { for (const uint8_t days : {uint8_t(1), uint8_t(2), uint8_t(30)}) { @@ -231,6 +250,7 @@ static void checkFreshAndValid() { assert(f.node.telemetry_history_tx_prefs_healthy); assert(f.node.telemetry_history_tx_enabled == enabled); assert(f.node.telemetry_history_tx_interval_days == days); + assert(f.node.telemetry_history_tx_types == 31); assert(f.node.saveTelemetryHistoryTxPrefs() && f.fs.files.at(target) == current(enabled, days)); assert(!f.fs.exists(temp) && !f.fs.exists(backup)); char reply[160]; f.node.formatTelemetryHistoryTxStatus(reply, sizeof(reply)); @@ -245,7 +265,7 @@ static void checkLoadFailures() { Fixture f; f.fs.files[target] = std::vector(length, 0); f.boot(); f.expectFault(); assert(f.fs.files.at(target) == std::vector(length, 0)); } - for (const auto& image : {current(), legacy()}) { + for (const auto& image : {current(), previous(), legacy()}) { for (size_t limit = 0; limit < image.size(); ++limit) { Fixture f; f.fs.files[target] = image; f.fs.read_limit[target] = limit; f.boot(); f.expectFault(); assert(f.fs.files.at(target) == image); @@ -274,9 +294,22 @@ static void checkLoadFailures() { } static void checkLegacyMigration() { + for (const bool enabled : {false, true}) { + for (const uint8_t days : {uint8_t(1), uint8_t(30)}) { + Fixture f; const auto image = previous(enabled, days); f.seed(image); + assert(f.node.telemetry_history_tx_prefs_healthy); + assert(f.node.telemetry_history_tx_enabled == enabled); + assert(f.node.telemetry_history_tx_interval_days == days); + assert(f.node.telemetry_history_tx_types == 31); + assert(f.fs.files.at(target) == image); // old files are not rewritten on boot + assert(f.node.saveTelemetryHistoryTxPrefs()); + assert(f.fs.files.at(target) == current(enabled, days)); + } + } for (const uint8_t length : {uint8_t(0), uint8_t(1), uint8_t(0x42), uint8_t(0x82)}) { Fixture f; const auto image = legacy(true, length); f.seed(image); assert(f.node.telemetry_history_tx_prefs_healthy && f.node.telemetry_history_tx_enabled); + assert(f.node.telemetry_history_tx_types == 31); assert(f.route.persisted && f.route.path_len == length); const size_t bytes = (length & 63) * ((length >> 6) + 1); assert(!memcmp(f.route.path, image.data() + 7, bytes)); @@ -322,7 +355,7 @@ static void checkLegacyMigration() { } static void verifyRollback(Fixture& f, const std::vector& prior, - const std::array& state) { + const std::array& state) { assert(f.node.state() == state && f.node.telemetry_history_tx_prefs_healthy); assert(f.fs.files.at(target) == prior); assert(!f.fs.exists(temp)); @@ -335,8 +368,8 @@ static void verifyRollback(Fixture& f, const std::vector& prior, } static void checkWriteAndValidationFailures() { - for (const auto& prior : {current(), legacy()}) { - for (size_t limit = 0; limit < 6; ++limit) { + for (const auto& prior : {current(), previous(), legacy()}) { + for (size_t limit = 0; limit < current().size(); ++limit) { Fixture f; f.seed(prior); const auto before = f.node.state(); f.fs.write_limit[temp] = limit; assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); @@ -349,7 +382,7 @@ static void checkWriteAndValidationFailures() { assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); verifyRollback(f, prior, before); } - for (size_t limit = 0; limit < 6; ++limit) { + for (size_t limit = 0; limit < current().size(); ++limit) { Fixture f; f.seed(prior); const auto before = f.node.state(); f.fs.read_limit[temp] = limit; assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); @@ -439,7 +472,7 @@ static void checkAmbiguousPublicationAndServiceGate() { assert(f.fs.files.at(target) == current(false)); } else f.expectFault(); } - for (int limit = 0; limit < 6; ++limit) { + for (int limit = 0; limit < int(current().size()); ++limit) { Fixture f; f.seed(); f.history(); f.fs.final_read_limit = limit; assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); @@ -836,6 +869,211 @@ static void checkLastTxSurvivesScheduleAndStatusTruncation() { expectLastTx(restarted.node, "never"); // boot-local; do not invent a timestamp } +static const char* const type_keys[] = {"temp", "volt", "volt.i2c", "noise", "noise2"}; + +static void externalHistory(Fixture& f) { + const uint8_t channels[] = {7, 9}; + assert(f.node.external_voltage_history.configure(channels, 2)); + const mesh::ExternalVoltageHistory::Reading readings[] = {{7, 12.5f, true}, {9, 5.0f, true}}; + for (unsigned i = 0; i < mesh::ExternalVoltageHistory::RETENTION_SAMPLES; ++i) + f.node.external_voltage_history.record(1000800 + i * 1800, readings, 2); + assert(f.node.external_voltage_history.binaryChunkCount() == 3); +} + +static std::vector selectedMagics(uint8_t types) { + std::vector expected; + if (types & 1) expected.push_back("TTB1"); + if (types & 2) expected.push_back("TVB1"); + if (types & 8) expected.push_back("N1B1"); + if (types & 16) expected.push_back("N2B1"); + if (types & 4) for (unsigned i = 0; i < 6; ++i) expected.push_back("IVB1"); + return expected; +} + +static void checkTypePreferencesAndCommands() { + for (unsigned value = 0; value <= 255; ++value) { + Fixture f; const auto image = current(true, 7, uint8_t(value)); f.seed(image); + assert(f.fs.files.at(target) == image); // no eager format rewrite + assert(f.node.telemetry_history_tx_prefs_healthy == (value < 32)); + if (value >= 32) { f.expectFault(); continue; } + assert(f.node.telemetry_history_tx_types == value); + assert(f.node.saveTelemetryHistoryTxPrefs() && f.fs.files.at(target) == image); + f.boot(); assert(f.node.telemetry_history_tx_types == value); + } + Fixture f; f.seed(); + assert(f.node.command("get telemetry.tx.types") == + "> temp=on volt=on volt.i2c=on noise=on noise2=on"); + for (unsigned i = 0; i < 5; ++i) { + const std::string key = std::string("telemetry.tx.") + type_keys[i]; + assert(f.node.command(("get " + key).c_str()) == "> on"); + assert(f.node.command(("set " + key + " off").c_str()) == "OK - " + key + "=off"); + assert(f.node.telemetry_history_tx_types == uint8_t(31 & ~(1U << i))); + assert(f.fs.files.at(target) == current(true, 7, uint8_t(31 & ~(1U << i)))); + f.boot(); assert(f.node.command(("get " + key).c_str()) == "> off"); + const auto writes = f.fs.writes; + assert(f.node.command(("set " + key + " off").c_str()) == "OK - " + key + "=off"); + assert(f.fs.writes == writes); // idempotent settings don't wear flash + assert(f.node.command(("set " + key + " on").c_str()) == "OK - " + key + "=on"); + assert(f.node.telemetry_history_tx_types == 31); + for (const auto& command : {"get " + key, "set " + key + " off"}) { + ClientInfo unauthorized; unauthorized.admin = false; + const auto before = f.node.state(); const auto files = f.fs.files; + assert(f.node.command(command.c_str(), &unauthorized) == "Err - not permitted"); + assert(f.node.state() == before && f.fs.files == files); + } + } + for (const char* command : {"set telemetry.tx.types off", "set telemetry.tx.types on"}) { + const uint8_t expected = strstr(command, " off") ? 0 : 31; + assert(f.node.command(command).find("OK -") == 0); + assert(f.node.telemetry_history_tx_types == expected); + f.boot(); assert(f.node.telemetry_history_tx_types == expected); + } + for (const char* command : {"set telemetry.tx.temp", "set telemetry.tx.temp ", + "set telemetry.tx.temp true", "set telemetry.tx.temp 1", "set telemetry.tx.temp OFF", + "set telemetry.tx.temp on junk", "set telemetry.tx.noise2x off", + "get telemetry.tx.noise extra", "get telemetry.tx.types ", + "set telemetry.tx.volt.i2cx off", "set telemetry.tx.gps on", "get telemetry.tx.gps"}) { + const auto before = f.node.state(); const auto files = f.fs.files; const auto writes = f.fs.writes; + assert(f.node.command(command).find("Err -") == 0); + assert(f.node.state() == before && f.fs.files == files && f.fs.writes == writes); + } + ClientInfo unauthorized; unauthorized.admin = false; + for (const char* command : {"get telemetry.tx.types", "set telemetry.tx.types off"}) + assert(f.node.command(command, &unauthorized) == "Err - not permitted"); +} + +static void checkSelectedManualAndScheduledBatches() { + test_millis = 0; + for (const bool manual : {false, true}) { + for (uint8_t types = 0; types < 32; ++types) { + Fixture f; f.seed(current(!manual, 2, types)); f.history(); f.noiseHistory(); externalHistory(f); + f.node.dual = true; + if (manual) { + const auto reply = f.node.command("send telemetry.tx now"); + if (types == 0) assert(reply == "Err - no telemetry packet types enabled"); + else { + const std::string expected = "OK - telemetry.tx queued temp=" + std::to_string(types & 1 ? 165 : 0) + + " volt=" + std::to_string(types & 2 ? 165 : 0) + + " i2c=" + std::to_string(types & 4 ? 2 : 0) + "/" + std::to_string(types & 4 ? 6 : 0) + + " noise=" + std::to_string(bool(types & 8)) + "/" + std::to_string(bool(types & 16)); + assert(reply == expected); + } + } + serviceDue(f.node); finishPending(f.node); + assert(f.node.attempts == selectedMagics(types)); + assert(f.node.packets.size() == selectedMagics(types).size()); + for (size_t i = 1; i < f.node.packet_times.size(); ++i) + assert(f.node.packet_times[i] - f.node.packet_times[i - 1] == 90000); + assert(!f.node.telemetry_history_tx_pending && !f.node.telemetry_history_tx_manual); + assert(f.node.telemetry_history_tx_types == types); + } + } + // Manual selections use their own available history, without requiring base samples. + for (const uint8_t types : {uint8_t(4), uint8_t(8), uint8_t(16)}) { + Fixture f; f.seed(current(false, 2, types)); f.node.dual = true; + if (types == 4) externalHistory(f); else f.noiseHistory(1); + assert(f.node.telemetry_history.voltageSampleCount() == 0); + assert(f.node.command("send telemetry.tx now").find("OK - telemetry.tx queued temp=0 volt=0") == 0); + serviceDue(f.node); finishPending(f.node); + assert(f.node.packets.size() == (types == 4 ? 6U : 1U)); + assert(f.node.attempts.front() == (types == 4 ? "IVB1" : types == 8 ? "N1B1" : "N2B1")); + } + for (const uint8_t types : {uint8_t(1), uint8_t(2), uint8_t(4), uint8_t(8), uint8_t(16)}) { + Fixture f; f.seed(current(false, 2, types)); const auto before = f.node.state(); + assert(f.node.command("send telemetry.tx now") == "Err - no enabled telemetry data available"); + assert(f.node.state() == before && f.node.packets.empty()); + } + Fixture warmup; warmup.seed(current(true, 2, 8)); warmup.noiseHistory(1); + warmup.node.telemetry_history.record(1000000, 22, true, 3750, 0, 0, false); + warmup.node.serviceTelemetryHistoryTx(); assert(warmup.node.packets.empty()); + warmup.history(); warmup.node.serviceTelemetryHistoryTx(); + assert(warmup.node.attempts == std::vector{"N1B1"}); +} + +static void checkTypeSaveRollbackAndAmbiguity() { + for (const bool all : {false, true}) { + for (unsigned failure = 0; failure < 6; ++failure) { + Fixture f; f.seed(); f.history(); externalHistory(f); + f.node.telemetry_history_tx_pending = 31; f.node.telemetry_history_tx_manual = true; + f.node.telemetry_history_tx_external_channel = 1; f.node.telemetry_history_tx_external_chunk = 2; + f.node.telemetry_history_next_tx_uptime = 90000; f.node.telemetry_history_tx_resume_uptime = 9000000; + const auto before = f.node.state(); const auto prior = f.fs.files.at(target); + if (failure == 0) f.fs.fail_write_open.insert(temp); + if (failure == 1) f.fs.write_limit[temp] = 6; + if (failure == 2) f.fs.fail_rename.insert({temp, target}); + if (failure == 3) f.fs.fail_final_read = true; + if (failure == 4) f.fs.final_read_limit = 6; + if (failure == 5) f.fs.corrupt_publish = true; + assert(f.node.command(all ? "set telemetry.tx.types off" : "set telemetry.tx.volt.i2c off") == + "Err - unable to save telemetry.tx types"); + assert(f.node.state() == before); // failed publication cannot prune work or reset timers/cursors + if (failure < 3) { + assert(f.node.telemetry_history_tx_prefs_healthy && f.fs.files.at(target) == prior); + f.fs.clearFaults(); + assert(f.node.command(all ? "set telemetry.tx.types off" : "set telemetry.tx.volt.i2c off").find("OK -") == 0); + assert(f.fs.files.at(target) == current(true, 7, all ? 0 : 27)); + } else { + assert(!f.node.telemetry_history_tx_prefs_healthy); f.expectFault(); + f.boot(); f.expectFault(); // ambiguous storage remains closed across reboot + f.fs.clearFaults(); f.boot(); + if (failure == 5) f.expectFault(); // corrupt published bytes never become authoritative + else { + assert(f.node.telemetry_history_tx_prefs_healthy); + assert(f.node.telemetry_history_tx_types == (all ? 0 : 27)); + assert(f.node.telemetry_history_tx_enabled && f.node.telemetry_history_tx_interval_days == 7); + assert(!f.node.telemetry_history_tx_pending && !f.node.telemetry_history_tx_manual); + } + } + } + } +} + +static void checkTypeChangesDuringBatches() { + test_millis = 0; + Fixture f; f.seed(current(false, 2, 12)); f.noiseHistory(1); externalHistory(f); + f.node.telemetry_history_next_tx_uptime = 9000000; + assert(f.node.command("send telemetry.tx now").find("OK -") == 0); + serviceDue(f.node); serviceDue(f.node); // N1B1, then first I2C chunk + assert(f.node.attempts == std::vector({"N1B1", "IVB1"})); + assert(f.node.telemetry_history_tx_external_chunk == 1); + const auto due = f.node.telemetry_history_next_tx_uptime; + assert(f.node.command("set telemetry.tx.noise off").find("OK -") == 0); + assert(f.node.command("set telemetry.tx.noise on").find("OK -") == 0); + assert(f.node.telemetry_history_tx_pending == 4); // enabling adds nothing to the current batch + assert(f.node.telemetry_history_next_tx_uptime == due && f.node.telemetry_history_tx_external_chunk == 1); + f.node.uptime_millis = due - 1; f.node.last_millis = test_millis; + f.node.serviceTelemetryHistoryTx(); assert(f.node.packets.size() == 2); + serviceDue(f.node); // second I2C chunk, never repeat chunk zero + assert(read32(f.node.packets[2].payload + 12) == read32(f.node.packets[1].payload + 12) + 64 * 1800); + const size_t admitted = f.node.packets.size(); + assert(f.node.command("set telemetry.tx.volt.i2c off").find("OK -") == 0); + assert(f.node.telemetry_history_tx_pending == 0 && !f.node.telemetry_history_tx_manual); + assert(f.node.telemetry_history_tx_external_channel == 0 && f.node.telemetry_history_tx_external_chunk == 0); + assert(f.node.telemetry_history_next_tx_uptime == 9000000 && !f.node.telemetry_history_tx_resume_uptime); + assert(f.node.packets.size() == admitted); // producer cancellation cannot retract admitted radio packets + f.node.uptime_millis += 90000; f.node.serviceTelemetryHistoryTx(); + assert(f.node.packets.size() == admitted); + assert(f.node.command("set telemetry.tx.volt.i2c on").find("OK -") == 0); + assert(!f.node.telemetry_history_tx_pending); + assert(f.node.command("send telemetry.tx now").find("noise=1/0") != std::string::npos); + assert(f.node.telemetry_history_tx_pending == 12); + + Fixture automatic; automatic.seed(); automatic.history(); + automatic.node.serviceTelemetryHistoryTx(); assert(automatic.node.packets.size() == 1); + const uint64_t now = automatic.node.uptime_millis; + assert(automatic.node.command("set telemetry.tx.volt off").find("OK -") == 0); + assert(!automatic.node.telemetry_history_tx_pending); + assert(automatic.node.telemetry_history_next_tx_uptime == now + 7ULL * 24 * 60 * 60 * 1000); + automatic.node.serviceTelemetryHistoryTx(); assert(automatic.node.packets.size() == 1); + + Fixture stale; stale.seed(current(false)); stale.history(); + assert(stale.node.command("send telemetry.tx now").find("OK -") == 0); + stale.node.telemetry_history_tx_types = 0; // defensive service filtering also works before its old due time + stale.node.telemetry_history_next_tx_uptime = 1000000; + stale.node.serviceTelemetryHistoryTx(); + assert(stale.node.packets.empty() && !stale.node.telemetry_history_tx_pending && !stale.node.telemetry_history_tx_manual); +} + int main() { checkFreshAndValid(); checkLoadFailures(); @@ -850,5 +1088,9 @@ int main() { checkLastTxCompletionAndOwnership(); checkLastTxAgeAndClockWrap(); checkLastTxSurvivesScheduleAndStatusTruncation(); + checkTypePreferencesAndCommands(); + checkSelectedManualAndScheduledBatches(); + checkTypeSaveRollbackAndAmbiguity(); + checkTypeChangesDuringBatches(); puts("telemetry persistence checks passed"); } diff --git a/test/test_telemetry_tx_persistence.py b/test/test_telemetry_tx_persistence.py index bb5068eb6..080bc2d60 100644 --- a/test/test_telemetry_tx_persistence.py +++ b/test/test_telemetry_tx_persistence.py @@ -40,21 +40,26 @@ class TelemetryPersistenceTests(unittest.TestCase): "bool CommonCLI::handleDataTxCommand(")) + "\n" generated += '#define TELEMETRY_HISTORY_TX_PREFS_FILE "/telemetry_tx"\n' for name in ("RETRY_MILLIS", "PACKET_SPACING_MILLIS", "DEFAULT_DAYS", "MAX_DAYS", - "TEMPERATURE", "VOLTAGE", "EXTERNAL_VOLTAGE", "NOISE", "NOISE2"): + "TEMPERATURE", "VOLTAGE", "EXTERNAL_VOLTAGE", "NOISE", "NOISE2", "ALL_TYPES"): generated += re.search(r"static const uint(?:8|64)_t TELEMETRY_HISTORY_TX_" + name + r"[^;]*;", source).group() + "\n" generated += "\n".join(extract_braced(source, signature) for signature in ( + "static char* trimSpaces(", "static File openFloodSettingsRead(", "static void formatPathReply(", "static bool recoverTelemetryHistoryTxPrefs(", "static bool readTelemetryHistoryTxPrefsImage(", "void MyMesh::loadTelemetryHistoryTxPrefs(", "bool MyMesh::saveTelemetryHistoryTxPrefs(", "bool MyMesh::sendTelemetryHistorySnapshot(", + "uint8_t MyMesh::availableTelemetryHistoryTxTypes(", + "void MyMesh::finishTelemetryHistoryTxBatch(", + "bool MyMesh::handleTelemetryHistoryTxTypesCommand(", "void MyMesh::recordTelemetryHistoryTx(", "bool MyMesh::sendNoiseFloorHistorySnapshot(", "bool MyMesh::sendExternalVoltageHistorySnapshot(", "void MyMesh::serviceTelemetryHistoryTx(", - "void MyMesh::formatTelemetryHistoryTxStatus(", "static char* trimSpaces(")) + "\n" + "void MyMesh::formatTelemetryHistoryTxStatus(")) + "\n" generated += ('void MyMesh::handle(char* command, char* reply, ClientInfo* sender) {\n' ' static const char telemetry_tx_schedule_command[] = "set telemetry.tx schedule";\n' - ' static const char telemetry_tx_send_now_command[] = "send telemetry.tx now";\n') + ' static const char telemetry_tx_send_now_command[] = "send telemetry.tx now";\n' + ' if (handleTelemetryHistoryTxTypesCommand(command, reply, !sender || sender->isAdmin())) return;\n') generated += extract_braced(source, "if (strcmp(command, telemetry_tx_send_now_command)") + "\n" generated += extract_braced(source, "if (strncmp(command, telemetry_tx_schedule_command,") generated += '\n strcpy(reply, "Err - unrecognized command");\n}\n'