Skip to content

Commit 65f92fc

Browse files
committed
feat: isolate event radio profiles
1 parent 0199a1f commit 65f92fc

3 files changed

Lines changed: 171 additions & 5 deletions

File tree

src/mesh/NodeDB.cpp

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,19 @@ void NodeDB::loadFromDisk()
21662166
migrationSavePending = false;
21672167
configDecodeFailed = false;
21682168

2169+
#if USERPREFS_EVENT_MODE
2170+
// The event config is intentionally isolated from the ordinary config.
2171+
// We only seed it when no event file exists; a corrupt event file must not
2172+
// silently fall back to and overwrite the user's normal radio profile.
2173+
bool eventConfigMissing = false;
2174+
#ifdef FSCom
2175+
spiLock->lock();
2176+
eventConfigMissing = !FSCom.exists(configFileName);
2177+
spiLock->unlock();
2178+
#endif
2179+
bool initializedEventConfig = false;
2180+
#endif
2181+
21692182
meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero;
21702183

21712184
#ifdef ARCH_ESP32
@@ -2354,6 +2367,31 @@ void NodeDB::loadFromDisk()
23542367

23552368
state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg,
23562369
&config);
2370+
#if USERPREFS_EVENT_MODE
2371+
if (eventConfigMissing && state != LoadFileResult::LOAD_SUCCESS) {
2372+
const LoadFileResult eventConfigState = state;
2373+
const LoadFileResult standardConfigState =
2374+
loadProto(standardConfigFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig),
2375+
&meshtastic_LocalConfig_msg, &config);
2376+
if (standardConfigState == LoadFileResult::LOAD_SUCCESS) {
2377+
// Preserve the user's identity and non-radio preferences, then
2378+
// replace only LoRa with this event build's compiled defaults.
2379+
const meshtastic_LocalConfig standardConfig = config;
2380+
installDefaultConfig(true);
2381+
const meshtastic_Config_LoRaConfig eventLora = config.lora;
2382+
config = eventConfigFromStandard(standardConfig, eventLora);
2383+
state = LoadFileResult::LOAD_SUCCESS;
2384+
initializedEventConfig = true;
2385+
LOG_INFO("Initialized event config without modifying %s", standardConfigFileName);
2386+
} else {
2387+
// loadProto() clears config before decoding. Restore the event
2388+
// outcome so the normal default/degraded path handles that safely.
2389+
// A corrupt normal config is especially important: preserving its
2390+
// decode failure prevents a replacement identity from being made.
2391+
state = standardConfigState == LoadFileResult::DECODE_FAILED ? LoadFileResult::DECODE_FAILED : eventConfigState;
2392+
}
2393+
}
2394+
#endif
23572395
if (state == LoadFileResult::DECODE_FAILED) {
23582396
// Config file present but undecodable this boot (corruption / torn write / transient decrypt fail).
23592397
// loadProto() already zeroed `config`, so the keypair is gone from RAM; minting a new one would change
@@ -2415,6 +2453,15 @@ void NodeDB::loadFromDisk()
24152453
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
24162454
#endif
24172455

2456+
#if USERPREFS_EVENT_MODE
2457+
if (initializedEventConfig) {
2458+
// This is the first durable event-profile write. A failed write is
2459+
// safe: normal files remain untouched and the next event boot retries.
2460+
if (!saveToDisk(SEGMENT_CONFIG))
2461+
LOG_ERROR("Unable to persist initial event config");
2462+
}
2463+
#endif
2464+
24182465
if (backupSecurity.private_key.size > 0) {
24192466
LOG_DEBUG("Restoring backup of security config");
24202467
config.security = backupSecurity;
@@ -2546,6 +2593,23 @@ void NodeDB::loadFromDisk()
25462593
EncryptedStorage::migrateFile(fn);
25472594
}
25482595
}
2596+
2597+
// Both radio profiles can contain channel PSKs. The inactive profile
2598+
// is not covered by saveToDisk(), so migrate it explicitly when it
2599+
// exists before treating the encrypted-storage pass as complete.
2600+
#ifdef FSCom
2601+
const char *radioProfileFiles[] = {standardConfigFileName, standardChannelFileName, standardBackupFileName,
2602+
eventConfigFileName, eventChannelFileName, eventBackupFileName};
2603+
for (const char *fn : radioProfileFiles) {
2604+
spiLock->lock();
2605+
const bool exists = FSCom.exists(fn);
2606+
spiLock->unlock();
2607+
if (exists && !EncryptedStorage::isEncrypted(fn)) {
2608+
LOG_INFO("Migrating inactive radio profile %s to encrypted storage", fn);
2609+
EncryptedStorage::migrateFile(fn);
2610+
}
2611+
}
2612+
#endif
25492613
}
25502614
#endif
25512615

@@ -2680,8 +2744,9 @@ bool NodeDB::disableLockdownToPlaintext()
26802744
// plaintext->encrypted migrate loop above. Order does not matter here;
26812745
// EncryptedStorage::removeLockdownArtifacts() (which deletes the DEK,
26822746
// the commit point) only runs after every file is confirmed plaintext.
2683-
const char *filesToCheck[] = {configFileName, moduleConfigFileName, channelFileName, deviceStateFileName,
2684-
nodeDatabaseFileName};
2747+
const char *filesToCheck[] = {standardConfigFileName, standardChannelFileName, standardBackupFileName,
2748+
eventConfigFileName, eventChannelFileName, eventBackupFileName, moduleConfigFileName,
2749+
deviceStateFileName, nodeDatabaseFileName};
26852750
for (const char *fn : filesToCheck) {
26862751
if (!EncryptedStorage::migrateFileToPlaintext(fn)) {
26872752
LOG_ERROR("NodeDB: failed to revert %s to plaintext; aborting disable (device stays in lockdown)", fn);

src/mesh/NodeDB.h

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,47 @@ extern meshtastic_Position localPosition;
112112
static constexpr const char *deviceStateFileName = "/prefs/device.proto";
113113
static constexpr const char *legacyPrefFileName = "/prefs/db.proto";
114114
static constexpr const char *nodeDatabaseFileName = "/prefs/nodes.proto";
115-
static constexpr const char *configFileName = "/prefs/config.proto";
115+
// Event builds keep their radio profile in separate files. A normal firmware
116+
// image therefore resumes the user's ordinary config and channel table without
117+
// a client-mediated restore after an event OTA.
118+
static constexpr const char *standardConfigFileName = "/prefs/config.proto";
119+
static constexpr const char *standardChannelFileName = "/prefs/channels.proto";
120+
static constexpr const char *eventConfigFileName = "/prefs/event-config.proto";
121+
static constexpr const char *eventChannelFileName = "/prefs/event-channels.proto";
122+
static constexpr const char *standardBackupFileName = "/backups/backup.proto";
123+
static constexpr const char *eventBackupFileName = "/backups/event-backup.proto";
124+
125+
struct RadioProfileStoragePaths {
126+
const char *config;
127+
const char *channels;
128+
const char *backup;
129+
};
130+
131+
constexpr RadioProfileStoragePaths radioProfileStoragePaths(bool eventMode)
132+
{
133+
return eventMode ? RadioProfileStoragePaths{eventConfigFileName, eventChannelFileName, eventBackupFileName}
134+
: RadioProfileStoragePaths{standardConfigFileName, standardChannelFileName, standardBackupFileName};
135+
}
136+
137+
inline meshtastic_LocalConfig eventConfigFromStandard(const meshtastic_LocalConfig &standard,
138+
const meshtastic_Config_LoRaConfig &eventLora)
139+
{
140+
meshtastic_LocalConfig eventConfig = standard;
141+
eventConfig.has_lora = true;
142+
eventConfig.lora = eventLora;
143+
return eventConfig;
144+
}
145+
146+
#if USERPREFS_EVENT_MODE
147+
static constexpr auto radioProfileStorage = radioProfileStoragePaths(true);
148+
#else
149+
static constexpr auto radioProfileStorage = radioProfileStoragePaths(false);
150+
#endif
151+
static constexpr const char *configFileName = radioProfileStorage.config;
152+
static constexpr const char *channelFileName = radioProfileStorage.channels;
153+
static constexpr const char *backupFileName = radioProfileStorage.backup;
116154
static constexpr const char *uiconfigFileName = "/prefs/uiconfig.proto";
117155
static constexpr const char *moduleConfigFileName = "/prefs/module.proto";
118-
static constexpr const char *channelFileName = "/prefs/channels.proto";
119-
static constexpr const char *backupFileName = "/backups/backup.proto";
120156

121157
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
122158
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include "MeshTypes.h" // Include BEFORE TestUtil.h
2+
#include "TestUtil.h"
3+
#include "mesh/NodeDB.h"
4+
#include <unity.h>
5+
6+
void test_standard_profile_uses_standard_files()
7+
{
8+
const auto paths = radioProfileStoragePaths(false);
9+
10+
TEST_ASSERT_EQUAL_STRING("/prefs/config.proto", paths.config);
11+
TEST_ASSERT_EQUAL_STRING("/prefs/channels.proto", paths.channels);
12+
TEST_ASSERT_EQUAL_STRING("/backups/backup.proto", paths.backup);
13+
}
14+
15+
void test_event_profile_uses_isolated_files()
16+
{
17+
const auto paths = radioProfileStoragePaths(true);
18+
19+
TEST_ASSERT_EQUAL_STRING("/prefs/event-config.proto", paths.config);
20+
TEST_ASSERT_EQUAL_STRING("/prefs/event-channels.proto", paths.channels);
21+
TEST_ASSERT_EQUAL_STRING("/backups/event-backup.proto", paths.backup);
22+
}
23+
24+
void test_event_config_preserves_identity_but_replaces_lora()
25+
{
26+
auto standard = meshtastic_LocalConfig_init_zero;
27+
standard.has_security = true;
28+
standard.security.private_key.size = 1;
29+
standard.security.private_key.bytes[0] = 0xA5;
30+
standard.has_device = true;
31+
standard.device.node_info_broadcast_secs = 600;
32+
standard.has_lora = true;
33+
standard.lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
34+
standard.lora.channel_num = 20;
35+
36+
auto eventLora = standard.lora;
37+
eventLora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
38+
eventLora.channel_num = 3;
39+
40+
const auto eventConfig = eventConfigFromStandard(standard, eventLora);
41+
42+
TEST_ASSERT_EQUAL_UINT8(1, eventConfig.security.private_key.size);
43+
TEST_ASSERT_EQUAL_HEX8(0xA5, eventConfig.security.private_key.bytes[0]);
44+
TEST_ASSERT_EQUAL_UINT32(600, eventConfig.device.node_info_broadcast_secs);
45+
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(eventLora.region), static_cast<uint32_t>(eventConfig.lora.region));
46+
TEST_ASSERT_EQUAL_UINT32(3, eventConfig.lora.channel_num);
47+
}
48+
49+
void setUp(void) {}
50+
51+
void tearDown(void) {}
52+
53+
extern "C" {
54+
void setup()
55+
{
56+
initializeTestEnvironment();
57+
UNITY_BEGIN();
58+
RUN_TEST(test_standard_profile_uses_standard_files);
59+
RUN_TEST(test_event_profile_uses_isolated_files);
60+
RUN_TEST(test_event_config_preserves_identity_but_replaces_lora);
61+
exit(UNITY_END());
62+
}
63+
64+
void loop() {}
65+
}

0 commit comments

Comments
 (0)