|
1 | 1 | # AdminModule config-save side effects: radio-reload & reboot gating |
2 | 2 |
|
3 | | -**Status:** Implemented |
| 3 | +**Status:** Implementation complete; hardware validation outstanding |
4 | 4 | **Date:** 2026-07-23 |
5 | 5 | **Area:** `src/modules/AdminModule.cpp` (`handleSetConfig`, `saveChanges`), `src/mesh/MeshService.cpp` (`reloadConfig`), `src/mesh/NodeDB.h` (`SEGMENT_*`) |
6 | | -**Commits:** `babeef08d` (radio-reload), `65c27f813` + `273fa8d0a` (segment docs / TODO markers), `71d711867` (reboot Tier 1), `cc9abdbbc` (reboot Tier 2) |
| 6 | +**Reference:** meshtastic/firmware#11181 (commit SHAs omitted - they do not survive rebase or squash merge) |
7 | 7 |
|
8 | 8 | Saving a config over the phone/BLE used to do more than persist bytes: it could re-init the |
9 | 9 | LoRa radio **and** reboot the device, on config that touched neither. This document records |
@@ -161,21 +161,52 @@ is worth. |
161 | 161 | Separating the few that plausibly don't need one (canned messages, ambient lighting, |
162 | 162 | status message) needs a per-module audit - its own effort, not part of this work. |
163 | 163 |
|
164 | | -5. **On-device menu `reloadConfig` sites** - 27 `reloadConfig(SEGMENT_CONFIG)` / |
165 | | - `reloadConfig(changes)` calls in `src/graphics/draw/MenuHandler.cpp` and |
166 | | - `src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp` still reload on any Config |
167 | | - save (they pass only `saveWhat`; the default `radioAffected=true` preserves this). |
168 | | - **Why untouched:** this work was scoped to the AdminModule (client admin-message) crash class. |
169 | | - The menu paths are a mix of genuinely-radio changes (region/preset) and incidental ones |
170 | | - (role, display units), run on the UI thread. Each is marked with a |
171 | | - `// TODO(radioAffected)` note (`audit` vs `radio-affecting`) for a future per-site pass. |
| 164 | +5. ~~**On-device menu `reloadConfig` sites**~~ - **now done, see "On-device menus" below.** |
| 165 | + These were initially left alone as out of scope, then migrated in the same PR. |
172 | 166 |
|
173 | 167 | For contrast, `set_channel` ([`:1445`](../src/modules/AdminModule.cpp#L1445)) and |
174 | 168 | `restore_preferences` ([`:1911`](../src/modules/AdminModule.cpp#L1911)) also still reload - |
175 | 169 | but that is _correct_, not conservative: both genuinely change LoRa/channel state. |
176 | 170 |
|
177 | 171 | --- |
178 | 172 |
|
| 173 | +## On-device menus (`applyConfigChange`) |
| 174 | + |
| 175 | +Both UIs go through one entry point on `MeshService`, so a menu action states the same two |
| 176 | +decisions AdminModule does rather than inheriting defaults: |
| 177 | + |
| 178 | +```cpp |
| 179 | +void applyConfigChange(int saveWhat, uint8_t flags, int32_t rebootSeconds = DEFAULT_REBOOT_SECONDS); |
| 180 | + |
| 181 | +enum ConfigApplyFlags : uint8_t { |
| 182 | + CONFIG_APPLY_NONE = 0, // persist only |
| 183 | + CONFIG_APPLY_RADIO = 1 << 0, // region/preset/freq/channel/PSK - re-init the LoRa chip |
| 184 | + CONFIG_APPLY_REBOOT = 1 << 1, // field only takes effect after a restart |
| 185 | +}; |
| 186 | +``` |
| 187 | +
|
| 188 | +`flags` has no default: the point is that every call site says what it wants. A flags enum |
| 189 | +rather than two bools because `reboot` and `radioAffected` are both bools, sat in different |
| 190 | +positions across the helpers this replaced, and transposing them compiled silently. InkHUD's |
| 191 | +`applyConfigReload(changes, reboot)` took `reboot` exactly where `reloadConfig` and |
| 192 | +`saveChanges` take `radioAffected`. |
| 193 | +
|
| 194 | +Notes for anyone extending this: |
| 195 | +
|
| 196 | +- `reloadConfig(X, /*radioAffected=*/false)` is **exactly equivalent** to `saveToDisk(X)` - the |
| 197 | + save is unconditional, outside the guard. So never pair them: that writes the file twice. |
| 198 | +- **AdminModule keeps using `saveChanges`**, not `applyConfigChange` directly. It owns the |
| 199 | + edit-transaction deferral, without which a multi-field remote set writes flash per field. |
| 200 | +- Reboots go through `requestReboot()` (`src/main.h`). It carries no UI: BaseUI already renders |
| 201 | + the notice at draw time from `rebootAtMsec`, while InkHUD's e-ink only draws when pushed and so |
| 202 | + raises `notifyApplyingChanges()` explicitly. |
| 203 | +- `rebootSeconds` exists for one caller - InkHUD's wifi-recovery path uses a shorter delay than |
| 204 | + `DEFAULT_REBOOT_SECONDS`. |
| 205 | +- UI-only state stays out of this: BaseUI `uiconfig` fields use `saveUIConfig()`, and InkHUD has |
| 206 | + its own non-protobuf `Persistence::Settings` store. Neither is in the `/prefs` protobuf tree. |
| 207 | +
|
| 208 | +--- |
| 209 | +
|
179 | 210 | ## Verification |
180 | 211 |
|
181 | 212 | Native coverage lives in `test/test_admin_radio/test_main.cpp`: |
@@ -258,9 +289,8 @@ a cheap confidence test but not a gate. |
258 | 289 |
|
259 | 290 | - To stop a config field from reloading the radio on an **AdminModule/client config save**: it |
260 | 291 | already doesn't, unless it's `lora`. Do **not** widen `radioAffected` to non-LoRa config - |
261 | | - only `RadioInterface::reconfigure()` (which reads `config.lora`) consumes it. This does not |
262 | | - apply to the on-device menu `reloadConfig` sites (item 5 above), which still reload on any |
263 | | - Config save regardless of field - that is a separate, untouched code path. |
| 292 | + only `RadioInterface::reconfigure()` (which reads `config.lora`) consumes it. The on-device |
| 293 | + menus are now gated the same way, via `applyConfigChange` (see below). |
264 | 294 | - To move a field off the reboot path: confirm it is consumed live (read from `config` at use |
265 | 295 | time, no cached/driver state), add it to the live set in the relevant `handleSetConfig` |
266 | 296 | case, and add a native case asserting no reboot on its change. For anything driver- or |
|
0 commit comments