#include #include "FileRead.h" #include "FilePresence.h" #include "PersistentStoreFormat.h" #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #define ATOMIC_FILE_WRITER_IMPLEMENTATION #include "AtomicFileWriter.h" #else #include "ContactFileTransaction.h" #endif #include #include #include #include #include #include #define MAX_PATH_SIZE 64 #define OUT_PATH_UNKNOWN 0xff #define OUT_PATH_FORCE_FLOOD 0xfe #define MESH_DEBUG_PRINTLN(...) do {} while (0) static uint32_t test_millis = 0; static uint32_t millis() { return test_millis; } #if defined(ESP32_PLATFORM) static MemoryFS* metadata_fs = nullptr; // Exercise production FilePresence's stat/error handling without a real mount. extern "C" int stat(const char* path, struct stat*) noexcept { if (!metadata_fs || metadata_fs->fail_metadata) { errno = EIO; return -1; } assert(!strncmp(path, "/spiffs", 7)); if (metadata_fs->exists(path + 7)) return 0; errno = ENOENT; return -1; } #endif namespace mesh { struct Packet { uint8_t payload[184] = {}; size_t payload_len = 0; uint8_t path_len = 0, path[MAX_PATH_SIZE] = {}; static bool isValidPathLen(uint8_t); static size_t writePath(uint8_t*, const uint8_t*, uint8_t); static uint8_t copyPath(uint8_t*, const uint8_t*, uint8_t); }; struct Utils { static void toHex(char*, const uint8_t*, size_t); }; struct DataRouteState; } class RegionMap; class CommonCLI { public: mesh::DataRouteState* _data_route = nullptr; bool adoptLegacyDataTxPath(const uint8_t*, uint8_t); bool getDataTxPath(const uint8_t*&, uint8_t&) const; }; struct ClientInfo { bool admin = true; bool isAdmin() const { return admin; } }; class MyMesh { public: FILESYSTEM* _fs; CommonCLI _cli; struct Identity { uint8_t pub_key[32] = {}; } self_id; mesh::TelemetryHistory telemetry_history; mesh::ExternalVoltageHistory external_voltage_history; 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_pending = 0; bool telemetry_history_tx_manual = false; uint8_t telemetry_history_tx_external_channel = 0; uint8_t telemetry_history_tx_external_chunk = 0; uint64_t telemetry_history_next_tx_uptime = 0; uint64_t telemetry_history_tx_resume_uptime = 0; uint64_t uptime_millis = 0; uint32_t last_millis = 0; std::vector packets; explicit MyMesh(FILESYSTEM& fs) : _fs(&fs) {} void loadTelemetryHistoryTxPrefs(); bool saveTelemetryHistoryTxPrefs(); void serviceTelemetryHistoryTx(); bool sendTelemetryHistorySnapshot(mesh::TelemetryHistory::Series); bool sendExternalVoltageHistorySnapshot(uint8_t, uint8_t); void formatTelemetryHistoryTxStatus(char*, size_t) const; void handle(char*, char*, ClientInfo*); // Radio queue boundary. Payload formatting and producer scheduling are real. mesh::Packet* createRawData(const uint8_t* data, size_t size) { assert(size <= 184); auto* packet = new mesh::Packet; memcpy(packet->payload, data, size); packet->payload_len = size; return packet; } bool sendDirect(mesh::Packet* packet, const uint8_t* path, uint8_t length) { packet->path_len = mesh::Packet::copyPath(packet->path, path, length); packets.push_back(*packet); delete packet; return true; } std::array state() const { return {{telemetry_history_tx_enabled, telemetry_history_tx_interval_days, telemetry_history_tx_pending, telemetry_history_tx_manual, telemetry_history_tx_external_channel, telemetry_history_tx_external_chunk, telemetry_history_next_tx_uptime, telemetry_history_tx_resume_uptime}}; } std::string command(const char* text, ClientInfo* sender = nullptr) { std::vector input(text, text + strlen(text) + 1); char reply[160] = {}; handle(input.data(), reply, sender); return reply; } }; #include "production.inc" static const char target[] = "/telemetry_tx"; static const char temp[] = "/telemetry_tx.tmp"; static const char backup[] = "/telemetry_tx.bak"; static std::vector current(bool enabled = true, uint8_t days = 7) { return {'T', 'H', 'T', '3', uint8_t(enabled), days}; } static std::vector legacy(bool enabled = true, uint8_t length = 1, uint8_t days = 7) { std::vector image(7 + MAX_PATH_SIZE, 0); memcpy(image.data(), "THT2", 4); image[4] = enabled; image[5] = length; image[6] = days; for (size_t i = 0; i < MAX_PATH_SIZE; ++i) image[7 + i] = uint8_t(i + 0x12); return image; } struct Fixture { MemoryFS fs; mesh::DataRouteState route; MyMesh node; Fixture() : node(fs) { #if defined(ESP32_PLATFORM) metadata_fs = &fs; #endif route.fs = &fs; node._cli._data_route = &route; } void boot() { route = mesh::DataRouteState(); route.fs = &fs; route.healthy = mesh::dataRouteLoad(route); node.loadTelemetryHistoryTxPrefs(); } void seed(const std::vector& image = current()) { fs.files[target] = image; boot(); } void history() { for (uint32_t i = 0; i < mesh::TelemetryHistory::BINARY_MAX_SAMPLES; ++i) { node.telemetry_history.record(1000000 + i * 1800, 22, true, 3750, 0, 0, false); } } void expectFault() { assert(!node.telemetry_history_tx_prefs_healthy); const auto before = node.state(); const auto writes = fs.writes; node.serviceTelemetryHistoryTx(); 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); char reply[160]; node.formatTelemetryHistoryTxStatus(reply, sizeof(reply)); assert(strstr(reply, " fault=prefs")); ClientInfo unauthorized; unauthorized.admin = false; assert(node.command("send telemetry.tx now", &unauthorized) == "Err - not permitted"); assert(!node.saveTelemetryHistoryTxPrefs() && fs.writes == writes); } }; 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.fs.files.empty()); for (const bool enabled : {false, true}) { for (const uint8_t days : {uint8_t(1), uint8_t(2), uint8_t(30)}) { f.seed(current(enabled, days)); 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.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)); assert(!strstr(reply, "fault=")); } } f.node._fs = nullptr; f.node.loadTelemetryHistoryTxPrefs(); f.expectFault(); } static void checkLoadFailures() { for (size_t length = 0; length <= 80; ++length) { Fixture f; f.fs.files[target] = std::vector(length, 0); f.boot(); f.expectFault(); assert(f.fs.files.at(target) == std::vector(length, 0)); } for (const auto& image : {current(), legacy()}) { for (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); f.fs.clearFaults(); f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy); } Fixture f; f.fs.files[target] = image; f.fs.fail_read_open.insert(target); f.boot(); f.expectFault(); assert(f.fs.files.at(target) == image); } for (unsigned value = 0; value <= 255; ++value) { for (const bool old : {false, true}) { for (const unsigned offset : {4U, old ? 6U : 5U}) { Fixture f; auto image = old ? legacy() : current(); image[offset] = uint8_t(value); f.fs.files[target] = image; f.boot(); const bool valid = offset == 4 ? value <= 1 : value >= 1 && value <= 30; assert(f.node.telemetry_history_tx_prefs_healthy == valid); if (!valid) f.expectFault(); } } } #if !defined(RP2040_PLATFORM) Fixture metadata; metadata.fs.files[target] = current(); metadata.fs.fail_metadata = true; metadata.boot(); metadata.expectFault(); assert(metadata.fs.files.at(target) == current()); #endif } static void checkLegacyMigration() { 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.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)); assert(f.fs.files.at(target) == image); // loading never rewrites the legacy file f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy && f.route.path_len == length); assert(f.node.saveTelemetryHistoryTxPrefs()); assert(f.fs.files.at(target) == current()); } for (const uint8_t length : {uint8_t(0), uint8_t(0x42), uint8_t(OUT_PATH_UNKNOWN)}) { Fixture f; f.route.path_len = length; memset(f.route.path, 0xab, sizeof(f.route.path)); assert(mesh::dataRouteSave(f.route)); const auto saved_route = f.fs.files.at("/data_tx"); f.fs.files[target] = legacy(); const auto writes = f.fs.writes; f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy && f.route.path_len == length); assert(f.fs.files.at("/data_tx") == saved_route && f.fs.writes == writes); } Fixture disabled; disabled.seed(legacy(false, OUT_PATH_UNKNOWN)); assert(disabled.node.telemetry_history_tx_prefs_healthy && !disabled.route.persisted); for (unsigned length = 0; length <= 255; ++length) { Fixture f; f.seed(legacy(true, uint8_t(length))); const bool valid = length != OUT_PATH_UNKNOWN && length != OUT_PATH_FORCE_FLOOD && mesh::Packet::isValidPathLen(uint8_t(length)); assert(f.node.telemetry_history_tx_prefs_healthy == valid); if (!valid) f.expectFault(); } // The actual adoption helper must fail before applying enabled legacy state. for (const int failure : {0, 1, 2}) { Fixture f; f.fs.files[target] = legacy(); if (failure == 0) f.fs.fail_write_open.insert("/data_tx.tmp"); if (failure == 1) f.fs.write_limit["/data_tx.tmp"] = 107; if (failure == 2) f.fs.fail_rename.insert({"/data_tx.tmp", "/data_tx"}); f.boot(); f.history(); f.expectFault(); assert(!f.node.telemetry_history_tx_enabled && !f.route.persisted); assert(f.fs.files.at(target) == legacy() && !f.fs.exists("/data_tx")); f.fs.clearFaults(); f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy && f.route.persisted && f.route.path_len == 1); assert(f.fs.files.at(target) == legacy()); } Fixture bad_route; bad_route.fs.files["/data_tx"] = {'b', 'a', 'd'}; bad_route.seed(legacy()); bad_route.expectFault(); assert(bad_route.fs.files.at(target) == legacy()); } static void verifyRollback(Fixture& f, const std::vector& prior, const std::array& state) { assert(f.node.state() == state && f.node.telemetry_history_tx_prefs_healthy); assert(f.fs.files.at(target) == prior); assert(!f.fs.exists(temp)); f.fs.clearFaults(); f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy && f.node.telemetry_history_tx_enabled); assert(f.node.telemetry_history_tx_interval_days == 7); assert(f.node.command("set telemetry.tx schedule off") == "OK - telemetry.tx schedule=off"); assert(f.fs.files.at(target) == current(false)); f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy && !f.node.telemetry_history_tx_enabled); } static void checkWriteAndValidationFailures() { for (const auto& prior : {current(), legacy()}) { for (size_t limit = 0; limit < 6; ++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"); verifyRollback(f, prior, before); } for (const int failure : {0, 1}) { Fixture f; f.seed(prior); const auto before = f.node.state(); if (failure == 0) f.fs.fail_write_open.insert(temp); else f.fs.fail_read_open.insert(temp); 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) { 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"); verifyRollback(f, prior, before); } Fixture f; f.seed(prior); const auto before = f.node.state(); f.fs.fail_rename.insert({temp, target}); assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); verifyRollback(f, prior, before); } for (const int failure : {0, 1}) { Fixture f; f.seed(); if (failure == 0) f.fs.fail_read_open.insert(target); else f.fs.read_limit[target] = 5; const auto writes = f.fs.writes; assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); f.expectFault(); assert(f.fs.writes == writes && f.fs.files.at(target) == current()); } // A failed fresh-install transaction is retryable only when still absent. Fixture fresh; fresh.boot(); fresh.fs.write_limit[temp] = 5; assert(fresh.node.command("set telemetry.tx schedule 5d") == "Err - unable to save telemetry.tx schedule"); assert(fresh.node.telemetry_history_tx_prefs_healthy && !fresh.fs.exists(target)); fresh.fs.clearFaults(); assert(fresh.node.command("set telemetry.tx schedule 5d") == "OK - telemetry.tx schedule=5d"); assert(fresh.fs.files.at(target) == current(true, 5)); } static void checkRecoveryAndRenameFailures() { #if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) Fixture recovered; recovered.fs.files[backup] = current(); recovered.boot(); assert(recovered.node.telemetry_history_tx_prefs_healthy); assert(recovered.fs.files.at(target) == current() && !recovered.fs.exists(backup)); Fixture failed; failed.fs.files[backup] = current(); failed.fs.fail_rename.insert({backup, target}); failed.boot(); failed.expectFault(); assert(!failed.fs.exists(target) && failed.fs.files.at(backup) == current()); failed.fs.clearFaults(); failed.boot(); assert(failed.node.telemetry_history_tx_prefs_healthy); Fixture corrupt; corrupt.fs.files[target] = {'b', 'a', 'd'}; corrupt.fs.files[backup] = current(); corrupt.boot(); corrupt.expectFault(); assert(corrupt.fs.files.at(target) == std::vector({'b', 'a', 'd'})); assert(corrupt.fs.files.at(backup) == current()); for (const int failure : {0, 1, 2}) { Fixture f; f.seed(); const auto before = f.node.state(); if (failure == 0) { f.fs.files[temp] = {1}; f.fs.fail_remove.insert(temp); } if (failure == 1) { f.fs.files[backup] = {1}; f.fs.fail_remove.insert(backup); } if (failure == 2) f.fs.fail_rename.insert({target, backup}); assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); assert(f.node.telemetry_history_tx_prefs_healthy && f.node.state() == before); assert(f.fs.files.at(target) == current()); f.fs.clearFaults(); assert(f.node.command("set telemetry.tx schedule off") == "OK - telemetry.tx schedule=off"); } Fixture gap; gap.seed(); gap.history(); const auto before = gap.node.state(); gap.fs.fail_rename.insert({temp, target}); gap.fs.fail_rename.insert({backup, target}); assert(gap.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); assert(gap.node.state() == before && gap.fs.files.at(backup) == current()); assert(!gap.fs.exists(target)); gap.expectFault(); gap.boot(); gap.expectFault(); // failed recovery remains fail-closed on reboot gap.fs.clearFaults(); gap.boot(); assert(gap.node.telemetry_history_tx_prefs_healthy && gap.node.telemetry_history_tx_enabled); assert(gap.fs.files.at(target) == current()); #endif // Stale uncommitted candidates are never mistaken for a fresh durable image. Fixture stale; stale.fs.files[temp] = current(); stale.boot(); assert(stale.node.telemetry_history_tx_prefs_healthy && !stale.node.telemetry_history_tx_enabled); assert(stale.node.command("set telemetry.tx schedule 5d") == "OK - telemetry.tx schedule=5d"); assert(stale.fs.files.at(target) == current(true, 5) && !stale.fs.exists(temp)); } static void checkAmbiguousPublicationAndServiceGate() { for (const int failure : {0, 1}) { Fixture f; f.seed(); f.history(); f.node.telemetry_history_tx_pending = TELEMETRY_HISTORY_TX_TEMPERATURE | TELEMETRY_HISTORY_TX_VOLTAGE; f.node.telemetry_history_tx_manual = true; f.node.telemetry_history_tx_resume_uptime = 10000000; const auto before = f.node.state(); if (failure == 0) f.fs.fail_final_read = true; else f.fs.corrupt_publish = true; assert(f.node.command("set telemetry.tx schedule off") == "Err - unable to save telemetry.tx schedule"); assert(f.node.state() == before); f.expectFault(); const auto published = f.fs.files.at(target); f.boot(); f.expectFault(); assert(f.fs.files.at(target) == published); f.fs.clearFaults(); f.boot(); if (failure == 0) { assert(f.node.telemetry_history_tx_prefs_healthy && !f.node.telemetry_history_tx_enabled); assert(f.fs.files.at(target) == current(false)); } else f.expectFault(); } for (int limit = 0; limit < 6; ++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"); assert(f.fs.files.at(target) == current(false)); f.expectFault(); f.boot(); f.expectFault(); assert(f.fs.files.at(target) == current(false)); f.fs.clearFaults(); f.boot(); assert(f.node.telemetry_history_tx_prefs_healthy && !f.node.telemetry_history_tx_enabled); } Fixture healthy; healthy.seed(); healthy.history(); healthy.node.serviceTelemetryHistoryTx(); assert(healthy.node.packets.size() == 1); assert(healthy.node.packets[0].payload_len == 184); assert(!memcmp(healthy.node.packets[0].payload, "TTB1", 4)); test_millis += 2000; healthy.node.serviceTelemetryHistoryTx(); test_millis = 0; assert(healthy.node.packets.size() == 2 && !memcmp(healthy.node.packets[1].payload, "TVB1", 4)); assert(healthy.node.command("send telemetry.tx now").find("OK - telemetry.tx queued") == 0); } int main() { checkFreshAndValid(); checkLoadFailures(); checkLegacyMigration(); checkWriteAndValidationFailures(); checkRecoveryAndRenameFailures(); checkAmbiguousPublicationAndServiceGate(); puts("telemetry persistence checks passed"); }