Skip to content

Commit 45d89b7

Browse files
committed
post rebase fixes
1 parent a0d21be commit 45d89b7

3 files changed

Lines changed: 123 additions & 8 deletions

File tree

docs/admin-config-save-gating.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,25 @@ Run against a real node through the
285285
(`MESHTASTIC_FIRMWARE_ROOT` → this checkout), with the serial log open. A nRF52840 SX126x
286286
board (e.g. WisMesh Tag) is the reference target; the crash was reproduced there.
287287
288-
**Transport: serial is sufficient - the on-device menus are not needed, and neither is BLE.**
289-
These operations are all client-protocol admin messages (`ToRadio`), carried identically over
290-
serial (`SerialConsole`/`StreamAPI`) or BLE; the on-device button/screen menus are a separate
291-
code path this work did not touch. Crucially, on nRF52 the BLE `onWrite` callback only
292-
_queues_ the packet - `handleToRadio` → `saveChanges` → `reloadConfig` (the reconfigure) runs
293-
on the **main FreeRTOS task** ([`NimbleBluetooth.cpp:135`](../src/nimble/NimbleBluetooth.cpp#L135)),
288+
The scenarios below cover the **AdminModule / client-protocol path only**. The on-device menu
289+
path was also migrated by this PR (see "On-device menus" above) and is a genuinely separate
290+
code path - it needs its own validation pass, driven by the buttons and screen on the board,
291+
and none of the transport reasoning in this section extends to it.
292+
293+
**Transport for the admin path: serial is sufficient, and BLE is not needed.** These
294+
operations are all client-protocol admin messages (`ToRadio`), carried identically over serial
295+
(`SerialConsole`/`StreamAPI`) or BLE. Crucially, on nRF52 the BLE `onWrite` callback only
296+
_queues_ the packet - `handleToRadio` → `saveChanges` → `applyConfigChange` (the reconfigure)
297+
runs on the **main FreeRTOS task** ([`NimbleBluetooth.cpp:135`](../src/nimble/NimbleBluetooth.cpp#L135)),
294298
exactly where `SerialConsole` (an `OSThread`) services the serial stream. So the code and
295299
thread under test are the same whichever transport you use, and the original crash was
296300
serial-proven. Drive the admin messages over USB serial (e.g. the `meshtastic --port` CLI);
297301
use BLE only if you specifically want to reproduce the exact user-facing conditions.
298302
303+
The menu path reaches the same `MeshService::applyConfigChange` on the same main task, so it
304+
inherits the same reasoning about _what_ runs - but only a menu-driven pass exercises the
305+
menu call sites' own flag choices, which is what this PR changed there.
306+
299307
### 1. Radio-reload / crash validation (regression guard for the favorite-node fix)
300308
301309
Confirms the non-LoRa config saves no longer run the live radio reconfigure - the main-task

test/support/AdminModuleTestShim.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ class AdminModuleTestShim : public AdminModule
2020
// Peek at the reply a handler queued, before drainReply() releases it.
2121
meshtastic_MeshPacket *reply() { return myReply; }
2222

23-
// With an "open edit transaction" saveChanges() is a pure no-op: no reloadConfig/saveToDisk/reboot.
24-
// Stamps the clock like begin_edit_settings, so a slow suite can't age the transaction into expiry.
23+
// With an "open edit transaction" saveChanges() has no outward effect - no
24+
// applyConfigChange/saveToDisk/reboot - it only records what the eventual commit should do.
25+
// Mirrors begin_edit_settings exactly: stamps the clock, so a slow suite can't age the
26+
// transaction into expiry, and clears the deferred decisions so they start from the same
27+
// baseline a real transaction gets.
2528
void deferSaves()
2629
{
2730
hasOpenEditTransaction = true;
2831
editTransactionActivityMs = millis();
32+
deferredShouldReboot = false;
33+
deferredRadioAffected = false;
2934
}
3035
int savedSegments() const { return lastSaveWhatForTest; }
3136

test/test_admin_radio/test_main.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2622,6 +2622,104 @@ static void test_commitWithoutBegin_persistsButDoesNotReloadOrReboot()
26222622
TEST_ASSERT_EQUAL_UINT32(0, rebootAtMsec);
26232623
}
26242624

2625+
// The abandonment path retires a transaction *outside* commit_edit_settings, so it decides both
2626+
// axes on its own. It must reach the same answers the commit would have, not fall back to the
2627+
// old implicit "reload the radio" - an abandoned display-only batch has no more business
2628+
// reprogramming the SX126x than a committed one does.
2629+
static void test_abandonedTransaction_liveOnlyBatch_expiresWithoutReloadOrReboot()
2630+
{
2631+
rebootAtMsec = 0;
2632+
ConfigChangedCounter counter;
2633+
counter.observe(&service->configChanged);
2634+
2635+
sendBeginEdit();
2636+
meshtastic_Config c = meshtastic_Config_init_zero;
2637+
c.which_payload_variant = meshtastic_Config_position_tag;
2638+
c.payload_variant.position = config.position;
2639+
c.payload_variant.position.position_broadcast_secs = config.position.position_broadcast_secs + 60;
2640+
sendSetConfig(c);
2641+
2642+
const int before = mockMeshService->reloadCalls;
2643+
testAdmin->ageEditTransaction();
2644+
sendGetDeviceMetadata(); // any later admin message retires the stale transaction
2645+
2646+
TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
2647+
TEST_ASSERT_EQUAL_INT(before + 1, mockMeshService->reloadCalls); // the deferred write still lands
2648+
TEST_ASSERT_EQUAL_INT(0, counter.count);
2649+
TEST_ASSERT_EQUAL_UINT32(0, rebootAtMsec);
2650+
}
2651+
2652+
// The guard the other way: abandonment must not lose a real LoRa change's reconfigure either.
2653+
static void test_abandonedTransaction_loraSet_stillReloadsRadioOnExpiry()
2654+
{
2655+
usePresetLongFast();
2656+
ConfigChangedCounter counter;
2657+
counter.observe(&service->configChanged);
2658+
2659+
sendBeginEdit();
2660+
meshtastic_Config c = meshtastic_Config_init_zero;
2661+
c.which_payload_variant = meshtastic_Config_lora_tag;
2662+
c.payload_variant.lora = config.lora;
2663+
c.payload_variant.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
2664+
sendSetConfig(c);
2665+
2666+
TEST_ASSERT_EQUAL_INT(0, counter.count); // deferred, as for any open transaction
2667+
2668+
testAdmin->ageEditTransaction();
2669+
sendGetDeviceMetadata();
2670+
2671+
TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
2672+
TEST_ASSERT_EQUAL_INT(1, counter.count);
2673+
}
2674+
2675+
// Reboot is the one axis abandonment deliberately drops: the settings are already live in RAM and
2676+
// the client that asked for the restart is, by definition, gone.
2677+
static void test_abandonedTransaction_rebootingFieldDoesNotRebootOnExpiry()
2678+
{
2679+
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; // known start state
2680+
rebootAtMsec = 0;
2681+
2682+
sendBeginEdit();
2683+
meshtastic_Config c = meshtastic_Config_init_zero;
2684+
c.which_payload_variant = meshtastic_Config_position_tag;
2685+
c.payload_variant.position = config.position;
2686+
c.payload_variant.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
2687+
sendSetConfig(c);
2688+
2689+
testAdmin->ageEditTransaction();
2690+
sendGetDeviceMetadata();
2691+
2692+
TEST_ASSERT_FALSE(testAdmin->editTransactionOpen());
2693+
TEST_ASSERT_EQUAL_UINT32(0, rebootAtMsec);
2694+
}
2695+
2696+
// Expiry must consume-and-clear the accumulated decisions, not just read them. A begin would reset
2697+
// them anyway, so the leak only shows through the one path that consumes them without a begin: a
2698+
// stray commit. Left uncleared, it inherits the abandoned LoRa transaction's answer and reloads.
2699+
static void test_abandonedTransaction_flagsDoNotLeakIntoAStrayCommit()
2700+
{
2701+
usePresetLongFast();
2702+
2703+
sendBeginEdit();
2704+
meshtastic_Config c = meshtastic_Config_init_zero;
2705+
c.which_payload_variant = meshtastic_Config_lora_tag;
2706+
c.payload_variant.lora = config.lora;
2707+
c.payload_variant.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
2708+
sendSetConfig(c);
2709+
testAdmin->ageEditTransaction();
2710+
sendGetDeviceMetadata(); // abandoned, not committed - this is where the flags must be cleared
2711+
2712+
// Start counting after the expiry, so only the stray commit's reload would register.
2713+
rebootAtMsec = 0;
2714+
ConfigChangedCounter counter;
2715+
counter.observe(&service->configChanged);
2716+
2717+
sendCommitEdit(); // no matching begin
2718+
2719+
TEST_ASSERT_EQUAL_INT(0, counter.count);
2720+
TEST_ASSERT_EQUAL_UINT32(0, rebootAtMsec);
2721+
}
2722+
26252723
// -----------------------------------------------------------------------
26262724
// Test runner
26272725
// -----------------------------------------------------------------------
@@ -2818,6 +2916,10 @@ void setup()
28182916
RUN_TEST(test_transaction_liveOnlyBatch_neitherRebootsNorReloads);
28192917
RUN_TEST(test_transaction_flagsDoNotLeakIntoTheNextTransaction);
28202918
RUN_TEST(test_commitWithoutBegin_persistsButDoesNotReloadOrReboot);
2919+
RUN_TEST(test_abandonedTransaction_liveOnlyBatch_expiresWithoutReloadOrReboot);
2920+
RUN_TEST(test_abandonedTransaction_loraSet_stillReloadsRadioOnExpiry);
2921+
RUN_TEST(test_abandonedTransaction_rebootingFieldDoesNotRebootOnExpiry);
2922+
RUN_TEST(test_abandonedTransaction_flagsDoNotLeakIntoAStrayCommit);
28212923

28222924
exit(UNITY_END());
28232925
}

0 commit comments

Comments
 (0)