Add saved telemetry packet type controls
This commit is contained in:
1 parent
553957fff8
commit
bbe29fbf95
7 files changed
+519
-88
No files matched your search
+54
-15
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user