Include and qualify Meshtext in RAK4631 Full Companion

Port Meshtext settings to verified atomic nRF52 storage and use the existing radio RNG and hardware entropy. Keep the ExtraFS boundary, GPS, contacts, channel count and 8 KiB stack unchanged; omit unused library data to fit the image. Require linked codec and Companion transport evidence and build the real RAK Full profile in CI. Add configuration and storage-failure regression coverage.
This commit is contained in:
mikecarper committed 2026-10-12 22:39:00 -07:00
1 parent 7e5eabe81c
commit 65820d0dd2
13 files changed
+515 -9

No files matched your search

+51 -1
View File
@@ -371,7 +371,7 @@ static void cleanupAtomicTempFiles(FILESYSTEM* fs) {
"/_main.id.tmp", "/new_prefs.tmp", "/channels2.tmp",
"/contacts3.tmp", "/contacts4.mig.tmp", "/adv_blobs.tmp",
"/.extrafs.mig.tmp", "/extrafs.badpages.tmp",
"/extrafs.badpages.bak.tmp"};
"/extrafs.badpages.bak.tmp", "/mesh_mt.tmp"};
for (size_t i = 0; i < sizeof(fixed_temp_paths) / sizeof(fixed_temp_paths[0]); i++) {
if (fs->exists(fixed_temp_paths[i])) fs->remove(fixed_temp_paths[i]);
}
@@ -573,6 +573,56 @@ void DataStore::useVolatilePrimaryFS(FILESYSTEM& fs) {
_contact_load_incomplete = false;
_prefs_load_incomplete = false;
}
bool DataStore::loadMeshtextSettings(uint8_t image[40], size_t& size) {
size = 0;
if (image == nullptr || _fs == nullptr || _primary_storage_unavailable
|| _volatile_primary_fs) return false;
bool present = false;
uint32_t stored_size = 0;
if (!contactPathPresence(_fs, "/mesh_mt", present, &stored_size)) return false;
if (!present) return true;
// This backend has never stored the legacy ESP32-only NVS formats. Do not
// accept a truncated image, directory or uncommitted .tmp as fresh defaults.
if (stored_size != 40) return false;
for (uint8_t attempt = 0; attempt < 3; ++attempt) {
File file = openRead(_fs, "/mesh_mt");
if (!file) continue;
uint8_t loaded[40];
const bool ok = !file.isDirectory() && file.size() == sizeof(loaded)
&& file.read(loaded, sizeof(loaded)) == sizeof(loaded);
file.close();
if (ok) {
memcpy(image, loaded, sizeof(loaded));
size = sizeof(loaded);
return true;
}
}
return false;
}
bool DataStore::saveMeshtextSettings(const uint8_t image[40]) {
if (image == nullptr || _fs == nullptr || _primary_storage_unavailable
|| _volatile_primary_fs) return false;
bool present = false;
if (!contactPathPresence(_fs, "/mesh_mt", present)) return false;
if (!contactPathPresence(_fs, "/mesh_mt.tmp", present)) return false;
if (present && !_fs->remove("/mesh_mt.tmp")) return false;
mesh::AtomicFileWriter file(_fs, "/mesh_mt");
return file && file.write(image, 40) == 40 && file.commit();
}
bool DataStore::eraseMeshtextSettings() {
if (_fs == nullptr || _primary_storage_unavailable || _volatile_primary_fs) return false;
bool present = false;
// A staging file must never become a recovery source after explicit erase.
for (const char* path : {"/mesh_mt.tmp", "/mesh_mt"}) {
if (!contactPathPresence(_fs, path, present)) return false;
if (present && !_fs->remove(path)) return false;
if (!contactPathPresence(_fs, path, present) || present) return false;
}
return true;
}
#endif
void DataStore::disableSecondaryFS(bool authority_unknown) {
+5
View File
@@ -188,6 +188,11 @@ public:
#if defined(NRF52_PLATFORM)
void useVolatilePrimaryFS(FILESYSTEM& fs);
bool isVolatilePrimaryFS() const { return _volatile_primary_fs; }
// Meshtext settings belong to the primary store, alongside the identity.
// An absent record is size zero; unreadable or volatile storage is failure.
bool loadMeshtextSettings(uint8_t image[40], size_t& size);
bool saveMeshtextSettings(const uint8_t image[40]);
bool eraseMeshtextSettings();
#endif
void markPrimaryFSUnavailable();
void disableSecondaryFS(bool authority_unknown = true);
+34 -3
View File
@@ -1,8 +1,12 @@
#include "MyMesh.h"
#if MESH_ENABLE_MESHTEXT_CHAT
#if defined(NRF52_PLATFORM)
#include <helpers/NRF52Crypto.h>
#else
#include <Preferences.h>
#include <esp_system.h>
#endif
#include <helpers/CLICommandUtils.h>
#include <helpers/UTF8Helpers.h>
@@ -98,15 +102,31 @@ void MyMesh::beginMeshtext() {
refreshMeshtextIdentity();
// Independent random boot seed, then monotonic IDs. Never reuse a packet ID
// within this boot (including failed admission) with the same CTR sender.
#if defined(NRF52_PLATFORM)
getRNG()->random(reinterpret_cast<uint8_t*>(&_mt_packet_id), sizeof(_mt_packet_id));
#if defined(USE_CC310_HW_CRYPTO)
mesh::mixCC310Random(reinterpret_cast<uint8_t*>(&_mt_packet_id), sizeof(_mt_packet_id));
#endif
#else
_mt_packet_id = esp_random();
#endif
if (!_mt_packet_id) _mt_packet_id = 1;
uint8_t image[SettingsSize] = {}, expected[SettingsSize] = {};
#if defined(NRF52_PLATFORM)
size_t size = 0;
if (!_store->loadMeshtextSettings(image, size)) { _mt_settings_valid = false; return; }
#else
Preferences prefs;
if (!prefs.begin("mesh-mt", false)) { _mt_settings_valid = false; return; }
const size_t size = prefs.getBytesLength("settings");
#endif
if (size) {
uint8_t image[SettingsSize] = {}, expected[SettingsSize] = {};
#if defined(NRF52_PLATFORM)
_mt_settings_valid = size == SettingsSize;
#else
_mt_settings_valid = (size == SettingsSize || size == LegacySettingsSize)
&& prefs.getBytes("settings", image, size) == size;
#endif
if (_mt_settings_valid) {
const auto region = static_cast<mesh::mt::Region>(image[4]);
const bool legacy = size == LegacySettingsSize;
@@ -138,7 +158,9 @@ void MyMesh::beginMeshtext() {
}
}
}
#if !defined(NRF52_PLATFORM)
prefs.end();
#endif
refreshMeshtextProfile();
}
@@ -149,8 +171,12 @@ bool MyMesh::saveMeshtextSettings(mesh::mt::Region region, uint32_t slot, uint8_
if (node_override == UINT32_MAX) node_override = _mt_node_override;
if (region > mesh::mt::Region::ANZ || slot > 1024 || hops > 7 || !validShortName(short_name)
|| !validNodeOverride(node_override) || hasForeignRadioFrame()) return false;
uint8_t image[SettingsSize], verify[SettingsSize];
uint8_t image[SettingsSize];
makeSettings(image, region, slot, hops, short_name, node_override);
#if defined(NRF52_PLATFORM)
const bool ok = _store->saveMeshtextSettings(image);
#else
uint8_t verify[SettingsSize];
Preferences prefs;
if (!prefs.begin("mesh-mt", false)) return false;
// NVS commits this one blob atomically; never split region and slot into
@@ -159,8 +185,9 @@ bool MyMesh::saveMeshtextSettings(mesh::mt::Region region, uint32_t slot, uint8_
&& prefs.getBytes("settings", verify, sizeof(verify)) == sizeof(verify)
&& memcmp(image, verify, sizeof(image)) == 0;
prefs.end();
#endif
if (!ok) {
// A failed read-back cannot prove whether NVS committed. Suspend the
// A failed read-back cannot prove whether storage committed. Suspend the
// backend rather than silently use one network now and another at boot.
_mt_settings_valid = false;
_mt_profile_dirty = true;
@@ -188,6 +215,9 @@ bool MyMesh::eraseMeshtextSettings() {
_mt_seen.clear();
_radio_profiles.clearAutoSecondary();
_radio_profiles.clearSecondaryProtocol();
#if defined(NRF52_PLATFORM)
const bool ok = _store->eraseMeshtextSettings();
#else
Preferences prefs;
if (!prefs.begin("mesh-mt", false)) {
_mt_status = "Meshtext settings erase failed";
@@ -195,6 +225,7 @@ bool MyMesh::eraseMeshtextSettings() {
}
const bool ok = !prefs.getBytesLength("settings") || prefs.remove("settings");
prefs.end();
#endif
if (ok) {
_mt_region = mesh::mt::Region::Unset; _mt_slot = 0; _mt_hop_limit = 7;
_mt_short_name[0] = 0; _mt_node_override = 0; _mt_node = _mt_default_node;