@@ -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,
2749+ moduleConfigFileName, 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);
0 commit comments