Add saved telemetry packet type controls

This commit is contained in:
mikecarper committed 2026-10-23 21:32:47 -07:00
1 parent 553957fff8
commit bbe29fbf95
7 files changed
+519 -88

No files matched your search

+54 -15
View File
@@ -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 <off|direct|path>`
- `set telemetry.tx schedule <off|1-30d>`
- `get telemetry.tx.types`
- `set telemetry.tx.types <on|off>`
- `get telemetry.tx.<type>`
- `set telemetry.tx.<type> <on|off>`
- `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`,
+54 -15
View File
@@ -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 <on|off>`, 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.
+135 -40
View File
@@ -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;
+5
View File
@@ -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);
+11 -5
View File
@@ -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<uint64_t, 8> persisted = state();
std::array<uint64_t, 9> persisted = state();
std::array<uint64_t, 8> state() const {
std::array<uint64_t, 9> 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<uint64_t, 8>& before) {
static void assertUnchanged(const Fixture& f, const std::array<uint64_t, 9>& 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;
+252 -10
View File
@@ -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<mesh::Packet> packets;
std::vector<uint64_t> packet_times;
std::vector<std::string> 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<uint64_t, 8> state() const {
std::array<uint64_t, 9> 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<char> 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<uint8_t> current(bool enabled = true, uint8_t days = 7) {
static std::vector<uint8_t> 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<uint8_t> previous(bool enabled = true, uint8_t days = 7) {
return {'T', 'H', 'T', '3', uint8_t(enabled), days};
}
static std::vector<uint8_t> 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<uint8_t>(length, 0); f.boot();
f.expectFault(); assert(f.fs.files.at(target) == std::vector<uint8_t>(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<uint8_t>& prior,
const std::array<uint64_t, 8>& state) {
const std::array<uint64_t, 9>& 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<uint8_t>& 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<std::string> selectedMagics(uint8_t types) {
std::vector<std::string> 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<std::string>{"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<std::string>({"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");
}
+8 -3
View File
@@ -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'