diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 296e4d695..8e74b1118 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -2,6 +2,7 @@ #include #include #include "DataStore.h" +#include "PrefsStorageLayout.h" #include #include #if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) @@ -1094,6 +1095,19 @@ bool DataStore::loadPrefsInt(const char *filename, return false; } bool success = true; +#if defined(STM32_PLATFORM) && defined(__GNUC__) + using namespace mesh::companion_prefs; + uint8_t pad[4]; + for (size_t i = 0; i < FIELD_COUNT; ++i) { + const PrefField& field = PREF_FIELDS[i]; + if (i >= MANDATORY_FIELDS && file.available() == 0) continue; + // Only this normally constructed, mutable snapshot is a destination. + void* destination = const_cast(prefValue(field, loaded_prefs, + loaded_lat, loaded_lon, pad)); + if (success && file.read(static_cast(destination), field.length) + != field.length) success = false; + } +#else auto readField = [&file, &success](void* dest, size_t size) -> bool { if (!success || file.read(static_cast(dest), size) != size) { @@ -1226,6 +1240,7 @@ bool DataStore::loadPrefsInt(const char *filename, sizeof(loaded_prefs.gps_sync_interval_hours)); readOptionalField(&loaded_prefs.usb_debug_enabled, sizeof(loaded_prefs.usb_debug_enabled)); +#endif // Any bytes left over form only part of a historically appended field. // Preserve the file and defaults rather than treating that tail as EOF. @@ -1260,6 +1275,24 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou File file = openWrite(_fs, "/new_prefs"); #endif if (file) { +#if defined(STM32_PLATFORM) && defined(__GNUC__) + using namespace mesh::companion_prefs; + uint8_t image[235]; + const uint8_t pad[4] = {}; + size_t used = 0; + for (const PrefField& field : PREF_FIELDS) { + if (field.length > sizeof(image) - used) { + // Latch failure and stop before any out-of-bounds pointer arithmetic. + used = sizeof(image) + 1; + break; + } + memcpy(image + used, prefValue(field, _prefs, node_lat, node_lon, pad), + field.length); + used += field.length; + } + bool success = used == sizeof(image) + && file.write(image, sizeof(image)) == sizeof(image); +#else #if defined(STM32_PLATFORM) // Keep the explicit wire field list, but make one bounded atomic write. // Other platforms retain direct writes and their existing stack footprint. @@ -1406,6 +1439,7 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou success = success && image_length == sizeof(image) && file.write(image, sizeof(image)) == sizeof(image); #endif +#endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) success = file.commit(success); if (!success) MESH_DEBUG_PRINTLN("DataStore: atomic preferences write failed"); diff --git a/examples/companion_radio/PrefsStorageLayout.h b/examples/companion_radio/PrefsStorageLayout.h new file mode 100644 index 000000000..307d95b47 --- /dev/null +++ b/examples/companion_radio/PrefsStorageLayout.h @@ -0,0 +1,139 @@ +#pragma once + +#if defined(STM32_PLATFORM) && defined(__GNUC__) +#include "NodePrefs.h" +#include +#include + +#if defined(TBEAM_1W) || defined(RP2040_PLATFORM) +#error "The compact STM32 preference table needs an explicit board layout" +#endif + +namespace mesh { +namespace companion_prefs { + +struct PrefField { uint8_t offset, length; }; +enum : uint8_t { PrefsLat = 253, PrefsLon = 254, PrefsPad = 255 }; +static constexpr size_t MANDATORY_FIELDS = 21; + +// GNU supports offsetof for this adapter-bearing, non-standard-layout class. +// Use it only on the STM32 GNU ABI, and verify every individual value field; +// neither the class padding nor either runtime adapter is a wire image. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +template +constexpr PrefField prefField() { + static_assert(std::is_trivially_copyable::value, + "only scalar/array preferences are persisted"); + static_assert(sizeof(Value) == Length, "published preference size"); + static_assert(Offset < PrefsLat && Length <= UINT8_MAX, + "compact preference field bounds"); + static_assert(Offset + Length <= + __builtin_offsetof(CompanionNodePrefs, usb_debug_enabled) + + sizeof(CompanionNodePrefs::usb_debug_enabled), + "preference field stays before runtime adapters"); + static_assert(Offset + Length <= sizeof(CompanionNodePrefs), + "preference field stays inside its object"); + return {uint8_t(Offset), uint8_t(Length)}; +} +template +constexpr PrefField prefSpecial() { + static_assert(Source == PrefsLat || Source == PrefsLon || Source == PrefsPad, + "known auxiliary preference source"); + static_assert(Source == PrefsPad ? Length <= 4 : Length == sizeof(double), + "coordinate/padding source bounds"); + return {Source, uint8_t(Length)}; +} +#define PREF_FIELD(name, length) prefField() +// Explicit published wire order, deliberately independent of member order. +static constexpr PrefField PREF_FIELDS[] = { + PREF_FIELD(airtime_factor, 4), + PREF_FIELD(node_name, 32), + prefSpecial(), + prefSpecial(), + prefSpecial(), + PREF_FIELD(freq, 4), + PREF_FIELD(sf, 1), + PREF_FIELD(cr, 1), + PREF_FIELD(client_repeat, 1), + PREF_FIELD(manual_add_contacts, 1), + PREF_FIELD(bw, 4), + PREF_FIELD(tx_power_dbm, 1), + PREF_FIELD(telemetry_mode_base, 1), + PREF_FIELD(telemetry_mode_loc, 1), + PREF_FIELD(telemetry_mode_env, 1), + PREF_FIELD(rx_delay_base, 4), + PREF_FIELD(advert_loc_policy, 1), + PREF_FIELD(multi_acks, 1), + PREF_FIELD(path_hash_mode, 1), + prefSpecial(), + PREF_FIELD(ble_pin, 4), + PREF_FIELD(buzzer_quiet, 1), + PREF_FIELD(gps_enabled, 1), + PREF_FIELD(gps_interval, 4), + PREF_FIELD(autoadd_config, 1), + PREF_FIELD(autoadd_max_hops, 1), + PREF_FIELD(rx_boosted_gain, 1), + PREF_FIELD(default_scope_name, 31), + PREF_FIELD(default_scope_key, 16), + PREF_FIELD(radio_fem_rxgain, 1), + PREF_FIELD(radio_fem_rxgain_override, 1), + PREF_FIELD(vibe_quiet, 1), + PREF_FIELD(radio_fem_txgain, 1), + PREF_FIELD(rx_powersaving_enabled, 1), + PREF_FIELD(rx_ps_rx_us, 4), + PREF_FIELD(rx_ps_sleep_us, 4), + PREF_FIELD(rx_ps_level, 1), + PREF_FIELD(rx_ps_preamble, 1), + PREF_FIELD(powersaving_enabled, 1), + PREF_FIELD(wifi_enabled, 1), + PREF_FIELD(powersaving_policy_version, 1), + PREF_FIELD(usb_logging_enabled, 1), + PREF_FIELD(bluetooth_name, 32), + PREF_FIELD(display_rotation_degrees, 2), + PREF_FIELD(cad_enabled, 1), + PREF_FIELD(cad_scan_timeout_ms, 2), + PREF_FIELD(cad_retry_delay_ms, 2), + PREF_FIELD(cad_max_duration_ms, 2), + PREF_FIELD(bluetooth_mac_mode, 1), + PREF_FIELD(bluetooth_mac, 6), + PREF_FIELD(bluetooth_stealth_peer_type, 1), + PREF_FIELD(bluetooth_stealth_peer, 6), + PREF_FIELD(bluetooth_stealth_mode, 1), + PREF_FIELD(tx_delay_factor, 4), + PREF_FIELD(direct_tx_delay_factor, 4), + PREF_FIELD(interference_threshold, 1), + PREF_FIELD(agc_reset_interval, 1), + PREF_FIELD(tz_offset, 1), + PREF_FIELD(flood_retry_attempts, 1), + PREF_FIELD(flood_retry_max_path, 1), + PREF_FIELD(flood_retry_group_max_path, 1), + PREF_FIELD(flood_retry_advert_enabled, 1), + PREF_FIELD(one_key_dm_enabled, 1), + PREF_FIELD(bluetooth_enabled, 1), + PREF_FIELD(gps_sync_interval_hours, 2), + PREF_FIELD(usb_debug_enabled, 1), +}; +#undef PREF_FIELD +#pragma GCC diagnostic pop +static constexpr size_t FIELD_COUNT = sizeof(PREF_FIELDS) / sizeof(PREF_FIELDS[0]); +constexpr size_t wireSize(size_t count) { + return count == 0 ? 0 : PREF_FIELDS[count - 1].length + wireSize(count - 1); +} +static_assert(wireSize(MANDATORY_FIELDS) == 84, "published mandatory image size"); +static_assert(wireSize(FIELD_COUNT) == 235, "published STM32 preference image size"); + +static const void* prefValue(const PrefField& field, + const CompanionNodePrefs& values, + const double& lat, const double& lon, + const uint8_t pad[4]) { + if (field.offset == PrefsLat) return ⪫ + if (field.offset == PrefsLon) return &lon; + if (field.offset == PrefsPad) return pad; + return reinterpret_cast(&values) + field.offset; +} + +} // namespace companion_prefs +} // namespace mesh +#endif diff --git a/test/fixtures/companion_prefs_storage/legacy_loader.inc b/test/fixtures/companion_prefs_storage/legacy_loader.inc new file mode 100644 index 000000000..1f5ef925c --- /dev/null +++ b/test/fixtures/companion_prefs_storage/legacy_loader.inc @@ -0,0 +1,246 @@ +// Frozen sequential preference serializer from 54e5b0b99. +// Independent of the new storage table; keep its historical field order. +bool DataStore::loadPrefsLegacy(const char *filename, + CompanionNodePrefs& _prefs, double& node_lat, + double& node_lon) { + File file = openRead(_fs, filename); + if (file) { + CompanionNodePrefs loaded_prefs = _prefs; + // Files written before this tail existed always started Bluetooth at boot. + // Do not inherit a runtime off value when loading one of those images. + loaded_prefs.bluetooth_enabled = 1; + loaded_prefs.gps_sync_interval_hours = 0; + // Older images have no independent debug preference. Never inherit a + // runtime-on value when loading one of those images. + loaded_prefs.usb_debug_enabled = 0; + double loaded_lat = node_lat; + double loaded_lon = node_lon; + // The original image ended after ble_pin at byte 84. Later releases only + // appended fields, sometimes as an indivisible group. Accept every format + // actually emitted by those releases, but reject a truncated field/group + // or an unknown tail before any value reaches the live preferences. + static const uint32_t MIN_PREFS_SIZE = 84; + static const uint16_t KNOWN_PREFS_SIZES[] = { + 84, 85, 90, 91, 92, 93, 140, 141, 142, 143, 144, 155, + 156, 157, 158, 159, + 159 + sizeof(loaded_prefs.bluetooth_name), + 161 + sizeof(loaded_prefs.bluetooth_name), + 168 + sizeof(loaded_prefs.bluetooth_name), + 168 + sizeof(loaded_prefs.bluetooth_name) + + sizeof(loaded_prefs.bluetooth_mac_mode) + + sizeof(loaded_prefs.bluetooth_mac), + 168 + sizeof(loaded_prefs.bluetooth_name) + + sizeof(loaded_prefs.bluetooth_mac_mode) + + sizeof(loaded_prefs.bluetooth_mac) + + sizeof(loaded_prefs.bluetooth_stealth_peer_type) + + sizeof(loaded_prefs.bluetooth_stealth_peer), + 168 + sizeof(loaded_prefs.bluetooth_name) + + sizeof(loaded_prefs.bluetooth_mac_mode) + + sizeof(loaded_prefs.bluetooth_mac) + + sizeof(loaded_prefs.bluetooth_stealth_peer_type) + + sizeof(loaded_prefs.bluetooth_stealth_peer) + + sizeof(loaded_prefs.bluetooth_stealth_mode), + 226, // prior 215-byte image plus radio timing, interference, AGC and timezone +#ifdef TBEAM_1W + 233, // fan mode and low/high thermistor thresholds +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 323, // Pico W additionally persists its SSID/password (ESP32 uses NVS) +#endif + 230, // Companion flood retry controls, appended after prior fields +#ifdef TBEAM_1W + 237, +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 327, +#endif + 231, // One-key DM consent policy, appended after flood retry controls +#ifdef TBEAM_1W + 238, +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 328, +#endif +#ifdef TBEAM_1W + 239, // Bluetooth enable preference, appended after one-key DM policy +#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 329, +#else + 232, +#endif +#ifdef TBEAM_1W + 241, // GPS time-sync cadence in hours, appended after Bluetooth enable +#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 331, +#else + 234, +#endif +#ifdef TBEAM_1W + 242, // Independent USB debug preference, after the published GPS tail +#elif defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + 332, +#else + 235, +#endif + }; + const uint32_t prefs_size = file.size(); + bool known_size = false; + for (size_t i = 0; + i < sizeof(KNOWN_PREFS_SIZES) / sizeof(KNOWN_PREFS_SIZES[0]); i++) { + if (prefs_size == KNOWN_PREFS_SIZES[i]) { + known_size = true; + break; + } + } + if (prefs_size < MIN_PREFS_SIZE || !known_size) { + file.close(); + return false; + } + bool success = true; + auto readField = [&file, &success](void* dest, size_t size) -> bool { + if (!success + || file.read(static_cast(dest), size) != size) { + success = false; + return false; + } + return true; + }; + auto readOptionalField = [&file, &readField](void* dest, + size_t size) -> bool { + return file.available() == 0 || readField(dest, size); + }; + uint8_t pad[8]; + + readField(&loaded_prefs.airtime_factor, sizeof(float)); // 0 + readField(loaded_prefs.node_name, sizeof(loaded_prefs.node_name)); // 4 + readField(pad, 4); // 36 + readField(&loaded_lat, sizeof(loaded_lat)); // 40 + readField(&loaded_lon, sizeof(loaded_lon)); // 48 + readField(&loaded_prefs.freq, sizeof(loaded_prefs.freq)); // 56 + readField(&loaded_prefs.sf, sizeof(loaded_prefs.sf)); // 60 + readField(&loaded_prefs.cr, sizeof(loaded_prefs.cr)); // 61 + readField(&loaded_prefs.client_repeat, sizeof(loaded_prefs.client_repeat)); // 62 + readField(&loaded_prefs.manual_add_contacts, sizeof(loaded_prefs.manual_add_contacts)); // 63 + readField(&loaded_prefs.bw, sizeof(loaded_prefs.bw)); // 64 + readField(&loaded_prefs.tx_power_dbm, sizeof(loaded_prefs.tx_power_dbm)); // 68 + readField(&loaded_prefs.telemetry_mode_base, sizeof(loaded_prefs.telemetry_mode_base)); // 69 + readField(&loaded_prefs.telemetry_mode_loc, sizeof(loaded_prefs.telemetry_mode_loc)); // 70 + readField(&loaded_prefs.telemetry_mode_env, sizeof(loaded_prefs.telemetry_mode_env)); // 71 + readField(&loaded_prefs.rx_delay_base, sizeof(loaded_prefs.rx_delay_base)); // 72 + readField(&loaded_prefs.advert_loc_policy, sizeof(loaded_prefs.advert_loc_policy)); // 76 + readField(&loaded_prefs.multi_acks, sizeof(loaded_prefs.multi_acks)); // 77 + readField(&loaded_prefs.path_hash_mode, sizeof(loaded_prefs.path_hash_mode)); // 78 + readField(pad, 1); // 79 + readField(&loaded_prefs.ble_pin, sizeof(loaded_prefs.ble_pin)); // 80 + readOptionalField(&loaded_prefs.buzzer_quiet, + sizeof(loaded_prefs.buzzer_quiet)); // 84 + readOptionalField(&loaded_prefs.gps_enabled, + sizeof(loaded_prefs.gps_enabled)); // 85 + readOptionalField(&loaded_prefs.gps_interval, + sizeof(loaded_prefs.gps_interval)); // 86 + readOptionalField(&loaded_prefs.autoadd_config, + sizeof(loaded_prefs.autoadd_config)); // 90 + readOptionalField(&loaded_prefs.autoadd_max_hops, + sizeof(loaded_prefs.autoadd_max_hops)); // 91 + readOptionalField(&loaded_prefs.rx_boosted_gain, + sizeof(loaded_prefs.rx_boosted_gain)); // 92 + readOptionalField(loaded_prefs.default_scope_name, + sizeof(loaded_prefs.default_scope_name)); // 93 + readOptionalField(loaded_prefs.default_scope_key, + sizeof(loaded_prefs.default_scope_key)); // 124 + readOptionalField(&loaded_prefs.radio_fem_rxgain, + sizeof(loaded_prefs.radio_fem_rxgain)); // 140 + readOptionalField(&loaded_prefs.radio_fem_rxgain_override, + sizeof(loaded_prefs.radio_fem_rxgain_override)); // 141 + readOptionalField(&loaded_prefs.vibe_quiet, + sizeof(loaded_prefs.vibe_quiet)); // 142 + readOptionalField(&loaded_prefs.radio_fem_txgain, + sizeof(loaded_prefs.radio_fem_txgain)); // 143 + readOptionalField(&loaded_prefs.rx_powersaving_enabled, + sizeof(loaded_prefs.rx_powersaving_enabled)); // 144 + readOptionalField(&loaded_prefs.rx_ps_rx_us, + sizeof(loaded_prefs.rx_ps_rx_us)); // 145 + readOptionalField(&loaded_prefs.rx_ps_sleep_us, + sizeof(loaded_prefs.rx_ps_sleep_us)); // 149 + readOptionalField(&loaded_prefs.rx_ps_level, + sizeof(loaded_prefs.rx_ps_level)); // 153 + readOptionalField(&loaded_prefs.rx_ps_preamble, + sizeof(loaded_prefs.rx_ps_preamble)); // 154 + readOptionalField(&loaded_prefs.powersaving_enabled, + sizeof(loaded_prefs.powersaving_enabled)); // 155 + readOptionalField(&loaded_prefs.wifi_enabled, + sizeof(loaded_prefs.wifi_enabled)); // 156 + readOptionalField(&loaded_prefs.powersaving_policy_version, + sizeof(loaded_prefs.powersaving_policy_version)); // 157 + readOptionalField(&loaded_prefs.usb_logging_enabled, + sizeof(loaded_prefs.usb_logging_enabled)); // 158 + readOptionalField(loaded_prefs.bluetooth_name, + sizeof(loaded_prefs.bluetooth_name)); // 159 + readOptionalField(&loaded_prefs.display_rotation_degrees, + sizeof(loaded_prefs.display_rotation_degrees)); // 191 + readOptionalField(&loaded_prefs.cad_enabled, + sizeof(loaded_prefs.cad_enabled)); // 193 + readOptionalField(&loaded_prefs.cad_scan_timeout_ms, + sizeof(loaded_prefs.cad_scan_timeout_ms)); // 194 + readOptionalField(&loaded_prefs.cad_retry_delay_ms, + sizeof(loaded_prefs.cad_retry_delay_ms)); // 196 + readOptionalField(&loaded_prefs.cad_max_duration_ms, + sizeof(loaded_prefs.cad_max_duration_ms)); // 198 + readOptionalField(&loaded_prefs.bluetooth_mac_mode, + sizeof(loaded_prefs.bluetooth_mac_mode)); // 200 + readOptionalField(loaded_prefs.bluetooth_mac, + sizeof(loaded_prefs.bluetooth_mac)); // 201 + readOptionalField(&loaded_prefs.bluetooth_stealth_peer_type, + sizeof(loaded_prefs.bluetooth_stealth_peer_type)); // 207 + readOptionalField(loaded_prefs.bluetooth_stealth_peer, + sizeof(loaded_prefs.bluetooth_stealth_peer)); // 208 + readOptionalField(&loaded_prefs.bluetooth_stealth_mode, + sizeof(loaded_prefs.bluetooth_stealth_mode)); // 214 + readOptionalField(&loaded_prefs.tx_delay_factor, sizeof(loaded_prefs.tx_delay_factor)); + readOptionalField(&loaded_prefs.direct_tx_delay_factor, sizeof(loaded_prefs.direct_tx_delay_factor)); + readOptionalField(&loaded_prefs.interference_threshold, sizeof(loaded_prefs.interference_threshold)); + readOptionalField(&loaded_prefs.agc_reset_interval, sizeof(loaded_prefs.agc_reset_interval)); + readOptionalField(&loaded_prefs.tz_offset, sizeof(loaded_prefs.tz_offset)); +#ifdef TBEAM_1W + readOptionalField(loaded_prefs.fan_mode, sizeof(loaded_prefs.fan_mode)); + readOptionalField(&loaded_prefs.fan_lo, sizeof(loaded_prefs.fan_lo)); + readOptionalField(&loaded_prefs.fan_hi, sizeof(loaded_prefs.fan_hi)); + loaded_prefs.fan_mode[sizeof(loaded_prefs.fan_mode) - 1] = 0; +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + readOptionalField(loaded_prefs.wifi_ssid, sizeof(loaded_prefs.wifi_ssid)); + readOptionalField(loaded_prefs.wifi_pwd, sizeof(loaded_prefs.wifi_pwd)); + loaded_prefs.wifi_ssid[sizeof(loaded_prefs.wifi_ssid) - 1] = 0; + loaded_prefs.wifi_pwd[sizeof(loaded_prefs.wifi_pwd) - 1] = 0; +#endif + readOptionalField(&loaded_prefs.flood_retry_attempts, + sizeof(loaded_prefs.flood_retry_attempts)); + readOptionalField(&loaded_prefs.flood_retry_max_path, + sizeof(loaded_prefs.flood_retry_max_path)); + readOptionalField(&loaded_prefs.flood_retry_group_max_path, + sizeof(loaded_prefs.flood_retry_group_max_path)); + readOptionalField(&loaded_prefs.flood_retry_advert_enabled, + sizeof(loaded_prefs.flood_retry_advert_enabled)); + readOptionalField(&loaded_prefs.one_key_dm_enabled, + sizeof(loaded_prefs.one_key_dm_enabled)); + readOptionalField(&loaded_prefs.bluetooth_enabled, + sizeof(loaded_prefs.bluetooth_enabled)); + readOptionalField(&loaded_prefs.gps_sync_interval_hours, + sizeof(loaded_prefs.gps_sync_interval_hours)); + readOptionalField(&loaded_prefs.usb_debug_enabled, + sizeof(loaded_prefs.usb_debug_enabled)); + + // Any bytes left over form only part of a historically appended field. + // Preserve the file and defaults rather than treating that tail as EOF. + success = success && file.available() == 0; + success = success && loaded_prefs.bluetooth_enabled <= 1; + file.close(); + if (!success) return false; + loaded_prefs.usb_debug_enabled = loaded_prefs.usb_debug_enabled == 1 ? 1 : 0; + _prefs = loaded_prefs; + node_lat = loaded_lat; + node_lon = loaded_lon; + return true; + } + return false; +} diff --git a/test/fixtures/companion_prefs_storage/legacy_writer.inc b/test/fixtures/companion_prefs_storage/legacy_writer.inc new file mode 100644 index 000000000..c30cf87be --- /dev/null +++ b/test/fixtures/companion_prefs_storage/legacy_writer.inc @@ -0,0 +1,151 @@ +// Frozen sequential preference serializer from 54e5b0b99. +// Independent of the new storage table; keep its historical field order. +bool DataStore::savePrefsLegacy(const CompanionNodePrefs& _prefs, double node_lat, double node_lon) { + if (_prefs_load_incomplete) return false; +#if defined(ESP32_PLATFORM) + if (!promoteCompanionRecoveryFile(_fs, "/new_prefs", _prefs_recovery_source)) return false; +#endif +#if defined(NRF52_PLATFORM) + if (_primary_storage_unavailable) return false; +#endif +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + mesh::AtomicFileWriter file(_fs, "/new_prefs"); +#elif defined(ESP32_PLATFORM) + mesh::ContactFileTransaction file(_fs, "/new_prefs", companionPathPresence); +#elif defined(RP2040_PLATFORM) + mesh::ContactFileTransaction file(_fs, "/new_prefs"); +#else + File file = openWrite(_fs, "/new_prefs"); +#endif + if (file) { + const auto writeField = [&file](const void* data, size_t size) -> bool { + return file.write(static_cast(data), size) == size; + }; + uint8_t pad[8]; + memset(pad, 0, sizeof(pad)); + + bool success = writeField((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 + success = success && writeField((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 + success = success && writeField(pad, 4); // 36 + success = success && writeField((uint8_t *)&node_lat, sizeof(node_lat)); // 40 + success = success && writeField((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + success = success && writeField((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 + success = success && writeField((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 + success = success && writeField((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 + success = success && writeField((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 + success = success && writeField((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 + success = success && writeField((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 + success = success && writeField((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + success = success && writeField((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 + success = success && writeField((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 + success = success && writeField((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 + success = success && writeField((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + success = success && writeField((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 + success = success && writeField((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 + success = success && writeField((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 + success = success && writeField(pad, 1); // 79 + success = success && writeField((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 + success = success && writeField((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 + success = success && writeField((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 + success = success && writeField((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 + success = success && writeField((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 + success = success && writeField((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 + success = success && writeField((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 + success = success && writeField((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 + success = success && writeField((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + success = success && writeField((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)); // 122 + success = success && writeField((uint8_t *)&_prefs.radio_fem_rxgain_override, + sizeof(_prefs.radio_fem_rxgain_override)); // 123 + success = success && writeField((uint8_t *)&_prefs.vibe_quiet, + sizeof(_prefs.vibe_quiet)); // 124 + success = success && writeField((uint8_t *)&_prefs.radio_fem_txgain, + sizeof(_prefs.radio_fem_txgain)); // 125 + success = success && writeField((uint8_t *)&_prefs.rx_powersaving_enabled, + sizeof(_prefs.rx_powersaving_enabled)); // 126 + success = success && writeField((uint8_t *)&_prefs.rx_ps_rx_us, + sizeof(_prefs.rx_ps_rx_us)); // 127 + success = success && writeField((uint8_t *)&_prefs.rx_ps_sleep_us, + sizeof(_prefs.rx_ps_sleep_us)); // 131 + success = success && writeField((uint8_t *)&_prefs.rx_ps_level, + sizeof(_prefs.rx_ps_level)); // 135 + success = success && writeField((uint8_t *)&_prefs.rx_ps_preamble, + sizeof(_prefs.rx_ps_preamble)); // 136 + success = success && writeField((uint8_t *)&_prefs.powersaving_enabled, + sizeof(_prefs.powersaving_enabled)); // 137 + success = success && writeField((uint8_t *)&_prefs.wifi_enabled, + sizeof(_prefs.wifi_enabled)); // 138 + success = success && writeField((uint8_t *)&_prefs.powersaving_policy_version, + sizeof(_prefs.powersaving_policy_version)); // 139 + success = success && writeField((uint8_t *)&_prefs.usb_logging_enabled, + sizeof(_prefs.usb_logging_enabled)); // 140 + success = success && writeField((uint8_t *)_prefs.bluetooth_name, + sizeof(_prefs.bluetooth_name)); // 141 + success = success && writeField( + (uint8_t *)&_prefs.display_rotation_degrees, + sizeof(_prefs.display_rotation_degrees)); + success = success && writeField((uint8_t *)&_prefs.cad_enabled, + sizeof(_prefs.cad_enabled)); + success = success && writeField((uint8_t *)&_prefs.cad_scan_timeout_ms, + sizeof(_prefs.cad_scan_timeout_ms)); + success = success && writeField((uint8_t *)&_prefs.cad_retry_delay_ms, + sizeof(_prefs.cad_retry_delay_ms)); + success = success && writeField((uint8_t *)&_prefs.cad_max_duration_ms, + sizeof(_prefs.cad_max_duration_ms)); + success = success && writeField((uint8_t *)&_prefs.bluetooth_mac_mode, + sizeof(_prefs.bluetooth_mac_mode)); + success = success && writeField((uint8_t *)_prefs.bluetooth_mac, + sizeof(_prefs.bluetooth_mac)); + success = success && writeField( + (uint8_t *)&_prefs.bluetooth_stealth_peer_type, + sizeof(_prefs.bluetooth_stealth_peer_type)); + success = success && writeField( + (uint8_t *)_prefs.bluetooth_stealth_peer, + sizeof(_prefs.bluetooth_stealth_peer)); + success = success && writeField( + (uint8_t *)&_prefs.bluetooth_stealth_mode, + sizeof(_prefs.bluetooth_stealth_mode)); + + success = success && writeField((uint8_t *)&_prefs.tx_delay_factor, + sizeof(_prefs.tx_delay_factor)); + success = success && writeField((uint8_t *)&_prefs.direct_tx_delay_factor, + sizeof(_prefs.direct_tx_delay_factor)); + success = success && writeField((uint8_t *)&_prefs.interference_threshold, + sizeof(_prefs.interference_threshold)); + success = success && writeField((uint8_t *)&_prefs.agc_reset_interval, + sizeof(_prefs.agc_reset_interval)); + success = success && writeField((uint8_t *)&_prefs.tz_offset, + sizeof(_prefs.tz_offset)); +#ifdef TBEAM_1W + success = success && writeField((uint8_t *)_prefs.fan_mode, + sizeof(_prefs.fan_mode)); + success = success && writeField((uint8_t *)&_prefs.fan_lo, + sizeof(_prefs.fan_lo)); + success = success && writeField((uint8_t *)&_prefs.fan_hi, + sizeof(_prefs.fan_hi)); +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + success = success && writeField((uint8_t *)_prefs.wifi_ssid, + sizeof(_prefs.wifi_ssid)); + success = success && writeField((uint8_t *)_prefs.wifi_pwd, + sizeof(_prefs.wifi_pwd)); +#endif + + success = success && file.write((uint8_t *)&_prefs.flood_retry_attempts, 1) == 1; + success = success && file.write((uint8_t *)&_prefs.flood_retry_max_path, 1) == 1; + success = success && file.write((uint8_t *)&_prefs.flood_retry_group_max_path, 1) == 1; + success = success && file.write((uint8_t *)&_prefs.flood_retry_advert_enabled, 1) == 1; + success = success && file.write((uint8_t *)&_prefs.one_key_dm_enabled, 1) == 1; + success = success && file.write((uint8_t *)&_prefs.bluetooth_enabled, 1) == 1; + success = success && file.write((uint8_t *)&_prefs.gps_sync_interval_hours, 2) == 2; + success = success && file.write((uint8_t *)&_prefs.usb_debug_enabled, 1) == 1; + +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) + success = file.commit(success); + if (!success) MESH_DEBUG_PRINTLN("DataStore: atomic preferences write failed"); +#else + file.close(); +#endif + return success; + } + return false; +} diff --git a/test/test_companion_preferences_transaction.py b/test/test_companion_preferences_transaction.py index 3c1f0379f..662db367c 100644 --- a/test/test_companion_preferences_transaction.py +++ b/test/test_companion_preferences_transaction.py @@ -22,6 +22,7 @@ HARNESS = r''' #include #endif #include "examples/companion_radio/NodePrefs.h" +#include "examples/companion_radio/PrefsStorageLayout.h" #define MESH_DEBUG_PRINTLN(...) ((void)0) struct DataStore { MemoryFS fs; diff --git a/test/test_companion_prefs_tail.py b/test/test_companion_prefs_tail.py index 68f57cd81..1fcee61c0 100644 --- a/test/test_companion_prefs_tail.py +++ b/test/test_companion_prefs_tail.py @@ -132,18 +132,9 @@ class CompanionPrefsTailTests(unittest.TestCase): def test_full_image_parity_and_atomic_tail_faults(self): source = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text() save = method(source, 'bool DataStore::savePrefs(') - tail_start = save.index(' // Pack only the explicit persisted tail') - tail_end = save.index('\n#if defined(NRF52_PLATFORM)', tail_start) - legacy_save = save[:tail_start] + LEGACY_TAIL + save[tail_end:] - legacy_save = legacy_save.replace('DataStore::savePrefs(', 'DataStore::savePrefsLegacy(', 1) - # The golden tail serializer must still issue the original scalar - # writes, including on STM32 where the production writer now stages - # an image. Its prefix emitter forwards directly to the same file. - emitter_start = legacy_save.index('#if defined(STM32_PLATFORM)\n // Keep the explicit wire field list') - emitter_end = legacy_save.index('\n uint8_t pad[8];', emitter_start) - legacy_save = legacy_save[:emitter_start] + ''' const auto writeField = [&file](const void* data, size_t size) -> bool { - return file.write(static_cast(data), size) == size; - };''' + legacy_save[emitter_end:] + # Keep the complete original scalar writer independent of any future + # production field-table changes, including every native tail byte. + legacy_save = (ROOT / 'test/fixtures/companion_prefs_storage/legacy_writer.inc').read_text() methods = esp_recovery_helpers(source) + '\n'.join(method(source, signature) for signature in ( 'bool DataStore::loadPrefs(', 'bool DataStore::loadPrefsInt(')) methods += '\n' + save + '\n' + legacy_save diff --git a/test/test_companion_prefs_values.py b/test/test_companion_prefs_values.py index b3258505a..3aa67a737 100644 --- a/test/test_companion_prefs_values.py +++ b/test/test_companion_prefs_values.py @@ -114,11 +114,35 @@ int main() { auto actual_adapters = adapterBytes(actual); auto legacy_adapters = adapterBytes(legacy); double lat = 1, lon = 2, legacy_lat = 1, legacy_lon = 2; + READ_CALLS.clear(); AVAILABLE_CALLS = 0; const bool expected = legacy_store.loadPrefsLegacy( "/new_prefs", legacy, legacy_lat, legacy_lon); + const auto expected_reads = READ_CALLS; + const unsigned expected_available = AVAILABLE_CALLS; + READ_CALLS.clear(); AVAILABLE_CALLS = 0; const bool got = actual_store.loadPrefsInt( "/new_prefs", actual, lat, lon); assert(got == expected); + assert(READ_CALLS == expected_reads); + assert(AVAILABLE_CALLS == expected_available); + if (length == 84 && quota == 84) { + // The original mandatory prefix has 21 reads, ending at ble_pin. + // This order is an independent wire contract, not the new table. + const std::vector mandatory = { + 4, 32, 4, 8, 8, 4, 1, 1, 1, 1, 4, 1, 1, 1, 1, + 4, 1, 1, 1, 1, 4}; + assert(expected && READ_CALLS == mandatory); + unsigned optional_fields = 45; +#ifdef TBEAM_1W + optional_fields += 3; +#endif +#if defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE) + optional_fields += 2; +#endif + // Every absent optional field observes EOF, then the completed + // image receives the existing final trailing-byte check. + assert(AVAILABLE_CALLS == optional_fields + 1); + } equalValues(actual, legacy); assert(!memcmp(&lat, &legacy_lat, sizeof(lat))); assert(!memcmp(&lon, &legacy_lon, sizeof(lon))); @@ -145,15 +169,9 @@ class CompanionPrefsValuesTests(unittest.TestCase): def test_value_only_copy_and_full_loader_match_legacy(self): source = (ROOT / 'examples/companion_radio/DataStore.cpp').read_text() loader = method(source, 'bool DataStore::loadPrefsInt(') - legacy = loader.replace('DataStore::loadPrefsInt(', 'DataStore::loadPrefsLegacy(', 1) - legacy = legacy.replace(''' CompanionNodePrefs loaded_prefs; - if (!loaded_prefs.copyPersistedValuesFrom(_prefs)) { - file.close(); - return false; - }''', ' CompanionNodePrefs loaded_prefs = _prefs;') - legacy = legacy.replace( - ' if (!_prefs.copyPersistedValuesFrom(loaded_prefs)) return false;', - ' _prefs = loaded_prefs;') + # A frozen sequential reader, not a clone of the current field table, + # is the authority for all old/torn images and read failure traces. + legacy = (ROOT / 'test/fixtures/companion_prefs_storage/legacy_loader.inc').read_text() self.assertNotIn('copyPersistedValuesFrom', legacy) methods = esp_recovery_helpers(source) + '\n'.join((loader, legacy, method(source, 'bool DataStore::savePrefs('))) @@ -211,6 +229,19 @@ inline char* utoa(unsigned int value,char* output,int base){ transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text() (work / 'ContactFileTransaction.h').write_text(transaction.replace( '#include "IdentityStore.h"', '#include ')) + # Instrument only the existing filesystem boundary. Both actual + # readers and the real transactional writer remain unchanged. + fs = (ROOT / 'test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h').read_text() + fs = fs.replace('class MemoryFS;', + 'static std::vector READ_CALLS;\n' + 'static unsigned AVAILABLE_CALLS = 0;\nclass MemoryFS;') + fs = fs.replace('size_t available() const { return size() - cursor_; }', + 'size_t available() const { ++AVAILABLE_CALLS;return size() - cursor_; }') + fs = fs.replace('inline int File::read(uint8_t* data, size_t size) {', + 'inline int File::read(uint8_t* data, size_t size) {\n' + ' READ_CALLS.push_back(size);') + (work / 'helpers').mkdir() + (work / 'helpers/IdentityStore.h').write_text(fs) cpp = work / 'test.cpp' cpp.write_text(code + tests) for platform in ('STM32_PLATFORM', 'NRF52_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM',