Files
trail-mate/docs/specification/SETTINGS_ARCHITECTURE_REFACTOR_SPEC.md

612 lines
30 KiB
Markdown

# Settings Architecture Refactor Specification
Status date: 2026-07-09
This document defines the next phase of retrofit specifications for Trail Mate settings. It is not a minor UI
tidying up, but a re-straightening of "what the configuration is, which protocol it belongs to, how to display it, how to persist it as an SD-first working document, and how it
applies it to the runtime".
This specification will be documented first and the runtime code will not be changed. The actual implementation must be carried out in stages according to this article, and GitNexus impact analysis must be performed according to the warehouse rules before modifying any
functions, classes or methods.
## User Goal
The target user scenario is that Trail Mate can work without a mobile phone:
- After selecting Meshtastic, MeshCore or Reticulum, the settings only show projects that are truly relevant to that protocol.
- Meshtastic's MQTT only serves Meshtastic; MeshCore's MQTT only serves MeshCore; the two are not interoperable.
- MQTT only supports light-burden mode: no TLS is implemented, and no additional MQTT payload encryption layer is implemented.
- MQTT enablement is an explicit configuration; the runtime must stop MQTT when Wi-Fi is turned off, and MQTT
configuration must not be automatically changed to enabled when Wi-Fi is on.
- If a protocol is configured with MQTT and the runtime is using the Wi-Fi MQTT transport, the protocol corresponding
BLE phone dependency should be suppressed or turned off to avoid users mistakenly thinking that the phone must still be connected.
- All user configuration must be persistent in the SD-first working document and mirrored to NVS.
- SD configuration cannot sacrifice ESP memory safety for "human readability"; the former whole-package JSON/cJSON schema is replaced by bounded TMS streaming.
## Distinctions
These concepts must be separated in code and UI, and cannot continue to be mixed in the two big baskets of `Chat` / `Network`.
| Concept | Meaning | Must not be confused with |
| --- | --- | --- |
| Protocol | Protocol semantics and node identity system of Meshtastic, MeshCore, Reticulum | transport, radio preset, UI page |
| Radio profile | LoRa frequency, bandwidth, spreading factor, coding rate, tx power, region/preset | channel name, PSK, broadcast/private |
| Channel / group | Group, slot, topic or destination configuration within the protocol | Air interface parameters |
| Transport | Bearing methods such as LoRa, BLE phone link, Wi-Fi MQTT, Reticulum TCP/UDP | protocol itself |
| Conversation | Broadcast session, private chat session, contact context in the UI | Meshtastic channel slot or MeshCore channel slot |
| Device settings | Screen, language, GPS, map, Wi-Fi, owner name, privacy and other cross-protocol settings | Profile of the current active protocol |
| Persistence | SD working document/NVS/Preferences/IDF store placement fact | UI widget state |
| Apply runtime | Apply configuration changes to radio, MQTT, BLE, GPS, privacy policy | Save configuration |
### Channel, Radio, Broadcast, Private
The air interface parameters determine "who can hear whom at the RF layer": frequency, bandwidth, spreading factor, coding rate, tx power, region or
preset must be compatible so that two devices can send and receive LoRa frames to each other.
Channel/group determines "which protocol group or key domain it belongs to after receiving the frame": Meshtastic uses channel
slot/name/key/hash, MeshCore uses channel slot/name/key/public-channel fallback, and Reticulum
uses destination, announce, interface and identity. They are not the same kind of objects, and you cannot make a generalized
`channel` and then have three protocols apply it.
Broadcast and private chat are addressing semantics: broadcast means sent to all visible nodes in the current protocol/channel/group; private chat means
sent to a node id, destination or peer, and may involve ack, route, retry, session status. Broadcast
or private chat does not change the air interface parameters, nor automatically create a channel.
MQTT is a transport, not a fourth protocol, nor a cross-protocol bridge. Meshtastic
packets from Meshtastic MQTT should go through the Meshtastic receiving path; MeshCore packets from MeshCore MQTT should go through the MeshCore receiving
path. There are no interoperability requirements between the two.
## Current Code Inventory
This section records the current implementation facts as a baseline before transformation.
| Area | Current owner | Observed shape |
| --- | --- | --- |
| Global config aggregate | `modules/core_sys/include/app/app_config.h` | `AppConfig` also carries device, chat, GPS, map, privacy, Meshtastic, MeshCore, Reticulum, MQTT, legacy channel fields |
| Protocol config object | `modules/core_chat/include/chat/domain/chat_types.h` | `chat::MeshConfig` is reused by three protocols, and contains radio, Meshtastic channel, MeshCore channel, MQTT, Reticulum group/interface fields |
| Shared LVGL settings | `modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp` | `kChatItems` / `kNetworkItems` mixed protocol items, hidden through `pref_key` string and `should_show_item` |
| Shared settings state | `modules/ui_shared/include/ui/screens/settings/settings_state.h` | A large UI state that also caches chat, network, MT MQTT, MC MQTT, Reticulum, device fields |
| Mono settings | `modules/ui_mono/src/runtime.cpp` | Keeps a lot of MT/MC settings processing code, but currently radio item list Only a few entrances are exposed, and capabilities are inconsistent with UI display |
| GTK settings | `apps/linux_uconsole_gtk/src/platform/gtk/gtk_uconsole_settings_logic.cpp` | There is a prototype of stack/page switching by protocol, which can be used as a reference for the "protocol page" idea, but the GTK widget logic should not be copied directly |
| Arduino ESP persistence | `platform/esp/arduino_common/src/app_config_store.cpp` | Use Preferences to save by field, a large number of keys already exist, but field coverage relies on handwriting load/save to keep synchronized |
| IDF persistence | `apps/esp32_lvgl/src/esp32_lvgl_idf_app_facade_runtime.cpp` | Put the entire `AppConfig` is packaged into raw blob, and the version is determined by `sizeof(AppConfig)`. Once the structure becomes old, the configuration will be rejected |
| SD working configuration | `platform/esp/arduino_common/src/app_config_sd_tms_runtime.cpp` | `/trailmate/config.tms` is a bounded, line-streamed, SD-first configuration authority; it mirrors every supported settings owner to NVS. |
| Store API | `modules/core_sys/include/platform/ui/settings_store.h` | Also provides `get_blob(std::vector<uint8_t>&)` and `get_blob_into(...)`; new ESP paths should preferentially use the bounded buffer version |
| Apply facade | `modules/core_sys/include/app/app_facades.h` | UI can be modified directly with `getConfig()`, and then call `saveConfig()`, `applyMeshConfig()`, `applyUserInfo()` and other apply methods |
## Problems
### 1. Settings taxonomy is wrong
`Chat` and `Network` are not product concepts now, but historical containers. The result is:
- Meshtastic MQTT, MeshCore MQTT, and Reticulum Wi-Fi interface may all appear in the same type of page.
- The name `chat_psk` cannot express whether it is a Meshtastic channel key or a MeshCore channel key.
- After the user selects a protocol, they will still see the remnants of another protocol, or they must be hidden by string blacklist.
- When adding channel management, "Meshtastic channel slot" and "MeshCore channel slot" cannot be expressed naturally.
### 2. Config ownership is too broad
`AppConfig` and `MeshConfig` are currently runtime large objects. They can serve as transitional compatibility layers, but cannot continue to be
Long-term settings schema. Reason:
- The field ownership is unclear, causing UI, Preferences, the SD working projection, and protocol apply to each record a copy of the facts.
- ESP stack hygiene has listed `AppConfig` and `MeshConfig` as dangerous automatic local types.
- When adding a channel list or more protocol profiles, if you continue to stuff these two structs, the memory risk will continue to amplify.
### 3. Persistence is not schema-driven
Arduino Preferences is currently saved by field, which is more stable than raw blob, but each field requires hand-written load/save, default value, and
migration logic. When adding a settings field, it is easy to miss the SD working projection or a certain UI.
IDF raw blob uses `sizeof(AppConfig)` as the compatibility condition, which is very fragile to subsequent split structures. Any `AppConfig`
layout changes may cause the old configuration to fail to load.
### 4. Historical SD JSON backup was too heavy
The advantage of the current JSON solution is that it is readable, but the cost is high on ESP:
- JSON restore needed to read the entire file into memory.
- cJSON parse will construct the entire tree.
- JSON backup print generated the complete string.
- A combination of `std::string`, `std::vector<uint8_t>`, whole-document parse/print exists in the old code.
This conflicts with the goal of a complete SD working configuration. The current
TMS path is line-streamed and bounded, so it does not construct a DOM or a
whole-document allocation.
### 5. Apply semantics are scattered
The UI directly changes `getConfig()`, and then manually calls different apply methods according to fields. This is difficult to guarantee:
- When modifying the protocol, the BLE/MQTT/LoRa/runtime status will switch according to the same set of rules.
- Turning off Wi-Fi only stops the MQTT runtime and does not quietly change the user configuration.
- When MQTT is successfully uplinked, the UI no longer only waits for LoRa to be successful before sending.
- Nodes received from MQTT and nodes received from LoRa go into the same contact/nearby/chat projection.
These problems have been exposed in MQTT debugging. Settings refactor needs to use the runtime impact as part of the field metadata
rather than scattered in the callback.
## Target Architecture
### Layer Shape
The target structure is as follows:
```text
Settings UI
-> SettingsDescriptor tables
-> SettingsEditSession / field-level draft
-> SettingsTransaction
-> SettingsValidator + normalizer
-> SettingsPersistence
-> RuntimeApplyDispatcher
-> Protocol/runtime adapters
```
`AppConfig` continues to exist in the first stage, but should be downgraded to the compatibility backing store and no longer as settings
The single source of truth for schema.
### Domain Buckets
The long-term structure should divide the configuration into these owners:
| Owner | Examples |
| --- | --- |
| `DeviceSettings` | owner long/short name、locale、screen、time、battery/display policy |
| `ConnectivitySettings` | Wi-Fi credentials、Wi-Fi enable policy、network limits |
| `GpsMapSettings` | GPS power/publish policy、map tile/cache/source、tracker defaults |
| `PrivacySettings` | ignored nodes、contact alert policy、location visibility |
| `MeshtasticProfile` | radio preset、region、hops、node info、channels、Meshtastic MQTT |
| `MeshCoreProfile` | radio profile、channel slot/name/key、public-channel fallback、MeshCore MQTT |
| `ReticulumProfile` | identity、LoRa interface、Wi-Fi interface、LXMF/announce groups |
| `ChatPresentationSettings` | active conversation defaults、notification/presentation preferences |
| `SdWorkingConfiguration` | schema version, SD/NVS authority, sensitive plaintext warning, and migration policy |
These owners can be mapped to the existing `AppConfig` fields first, but the schema naming must be designed according to the owner first to avoid
continuing to stuff the three protocols back into `Chat` / `Network` in the future.
### Settings Descriptor
Each displayable/persistent field must have a static descriptor. The descriptor should be a small `constexpr`
table entry to avoid dynamic allocation and heavy callbacks.
It is recommended that the descriptor at least contain:
| Metadata | Purpose |
| --- | --- |
| stable field id | compile-time enum, no arbitrary string is used for business judgment |
| owner/profile | device, connectivity, mt, mc, reticulum, etc. |
| UI section | determine which protocol page or device page to display |
| type | bool、u8、i32、enum、bounded string、hex blob、secret |
| bounds | Maximum string length, value range, maximum blob length |
| protocol mask | Visibility of MT/MC/Reticulum/global |
| capability mask | Whether the board supports Wi-Fi, BLE, GPS, LoRa, SD |
| runtime impact | none、save-only、apply-mesh、apply-user、apply-gps、restart-mqtt、restart-ble |
| storage key | NVS key、SD key、legacy key |
| default provider | Default value according to protocol/region/board type |
| migration rule | How to write when restoring from old key, old blob, old JSON |
| sensitive flag | PSK, MQTT password, Wi-Fi password, etc. |
The UI layer only consumes descriptor and current protocol/capability, and can no longer write `if (pref_key == "...")` as the main
visibility rule.
### Protocol-Specific UI
The top level of Settings is recommended to be split into:
- Device
- Connectivity
- Protocol
- Channels
- MQTT
- GPS & Map
- Privacy
- Configuration lifecycle and reset
- Diagnostics
The contents of `Protocol`, `Channels`, and `MQTT` are determined by the active protocol.
The Meshtastic page should display:
- Meshtastic radio preset/region/modem preset/hops/tx power.
- Meshtastic channel slots. The first stage can continue with primary/secondary, and the schema must reserve a slot list.
- Meshtastic MQTT:enabled、preset、host、port、username、password、root topic、uplink/downlink.
- BLE phone link policy: BLE can be turned off or downgraded when the Meshtastic MQTT runtime is available.
The MeshCore page should display:
- MeshCore radio profile/region/channel slot/tx power.
- MeshCore channel name/key/public channel fallback.
- MeshCore MQTT:enabled、preset、host、port、username、password、root topic、uplink/downlink.
- MeshCore contact/nearby projection node that must handle MQTT receive the same as LoRa receive.
The Reticulum page should show:
- Reticulum identity/status.
- LoRa interface parameters.
- Wi-Fi interface parameters.
- LXMF/announce groups.
- MQTT is not shown because the current target does not contain Reticulum MQTT.
### MQTT Policy
MQTT is a protocol-scoped transport:
```text
MeshtasticProfile.mqtt -> Meshtastic MQTT runtime only
MeshCoreProfile.mqtt -> MeshCore MQTT runtime only
```
Running state eligibility:
```text
configured = profile.mqtt.enabled && host not empty && port > 0
eligible = configured && wifi_runtime.connected && protocol == active_protocol
```
Constraints:
- MQTT runtime must be stopped when `wifi_runtime.connected == false`.
- When Wi-Fi becomes connected, only an enabled and fully configured MQTT runtime can become eligible;
`profile.mqtt.enabled` cannot be automatically changed from false to true.
- Plain MQTT only: `tls=false` is the only supported form; the UI does not provide a TLS switch, and the code does not introduce a TLS client.
- MQTT username/password is supported as it is not TLS; but must be handled as secret field.
- MQTT receive must enter the same protocol as LoRa receive projection: chat message, delivery status,
contacts/nearby、node info、position、notification.
- MQTT uplink success cannot be overwritten as failed by LoRa TX failure; delivery outcome should distinguish between transports.
### Default Presets
The default MQTT preset cannot use a personal broker as the default value. The default table should be part of the protocol owner:
| Protocol | Default preset intent |
| --- | --- |
| Meshtastic | mainstream Meshtastic public MQTT preset, plaintext transport, default root/topic/channel matching current community convention |
| MeshCore | mainstream MeshCore public/community preset if available; otherwise disabled with empty custom host until user selects preset |
The implementation does not hard-code a personal domain name as the default. Personal brokers can exist in custom presets or user configurations.
## SD Working Configuration Format
### Decision
The working configuration does not use JSON.
Using line-oriented typed key-value format, the goal is:
- Manual inspection.
- Streamable.
- Only one bounded line buffer is needed at a time.
- No cJSON tree required.
- No need to read the entire file into `std::string`.
- No need for `std::vector<uint8_t>` to accept whole blob.
Authoritative file names:
```text
/trailmate/config.tms
/trailmate/config.tms.new
/trailmate/config.tms.bak
/trailmate/config.tms.txn
```
`.bak` is the immediately preceding committed generation and `.txn` is a tiny digest for
recovering an interrupted replacement. Neither is a second working authority: while a
valid `config.tms` exists it is the only selected document.
### Format Sketch
```text
TMSET7
schema.version=u16:7
document.kind=enum:working
device.owner.long=str:Trail Mate
device.owner.short=str:TM
protocol.active=enum:meshtastic
mt.radio.region=enum:CN
mt.radio.modem_preset=enum:LONG_FAST
mt.channel.0.name=str:LongFast
mt.channel.0.psk=hex:01020304...
mt.mqtt.enabled=bool:1
mt.mqtt.host=str:mqtt.meshtastic.org
mt.mqtt.port=u16:1883
mt.mqtt.root=str:msh/CN
mt.mqtt.username=str:meshdev
mt.mqtt.password=secret:large4cats
mc.channel.slot=u8:0
mc.channel.name=str:public
mc.channel.key=hex:
mc.mqtt.enabled=bool:0
wifi.enabled=bool:1
wifi.profile_count=u8:2
wifi.profile.0.ssid=str:Office
wifi.profile.0.password=str:secret
rt.net.version=u16:1
rt.net.interface_count=u8:1
rt.net.interface.0.id=str:tcp
rt.net.interface.0.type=u8:1
```
Rules:
- First line is magic: `TMSET7`.
- Max content line length is 383 bytes; max document size is 32 KiB. Any longer line is rejected before apply.
- Key is ASCII stable storage key.
- Type prefix is mandatory.
- Strings are bounded by descriptor metadata.
- Hex blob max length is bounded by descriptor metadata before decoding.
- Unknown keys are rejected. A hand-edited working document must not silently retain an
old value because of a misspelled key.
- Known key with invalid value is rejected and reported, not partially applied.
- Validation completes before independent settings owners are written. The bounded
extension staging state and temporary `AppConfig` prefer PSRAM; neither is a task-stack
object or a whole-file buffer.
- Every supported setting write commits the SD working document synchronously in the
current call. A multi-key owner coalesces until its scope exits; there is no pending
loop service, retry timer, or NVS metadata authority.
- `TMSET7` is the only newly-written schema. It contains the Reticulum
network/LXMF `rt.net.*` projection as well as the core Reticulum group
destinations; a separate editable Reticulum JSON file is migration input,
not a second working authority.
### Why Not Binary TLV First
Binary TLV is smaller and faster, but it is harder to inspect and repair on SD. The line KV format is the
better first target because it keeps manual recovery possible without the cJSON memory cost. A future binary
TLV export can be added for factory/provisioning use, but it should not replace the editable user working format.
### Sensitive Fields
To make SD the complete working authority, Wi-Fi passwords, MQTT passwords, channel
PSKs, and cellular credentials are emitted into `config.tms`. The UI and logs must
continue to treat them as sensitive.
The document is plaintext. Sensitivity controls rendering and log redaction; it does
not encrypt the SD card.
## Persistence Model
Each field must declare its persistence location through the same descriptor.
### Arduino Preferences
The existing Preferences key can be retained, but the schema must become an override list:
- Press descriptor to read the key during load, and apply default/migration.
- Press descriptor to write the key when saving.
- Use bounded buffer for blob/secret.
- Do a migration for the old key and do not write compatibility logic in the UI callback.
### IDF Store
raw `sizeof(AppConfig)` blob can only be used as legacy input. The new path must be a versioned field store:
- Migrate to schema field store when reading old raw blobs.
- New saves no longer depend on `sizeof(AppConfig)`.
- If compact snapshot is retained for startup speed, there must also be independent schema version and field-level fallback.
### SD Working Configuration
`/trailmate/config.tms` is the startup authority when complete and valid. It
is parsed and validated in a streaming pass before any supported independent
settings owner is modified; the second step applies it and mirrors values to
NVS as a compatibility cache. Only a missing card or missing file permits the
NVS fallback. A present invalid file is retained for repair and must not be
overridden by NVS. The replacement transaction is SD-local:
```text
durable setting change
-> stream canonical config.tms.new
-> parse and canonicalize .new
-> write tiny .txn digest
-> move primary to .bak and promote .new
-> if boot finds no primary, validate and recover .new or .bak
```
## Memory Budget Rules
Actual implementation must comply with:
- Do not create `AppConfig`, `chat::MeshConfig`, protobuf frame, large byte array on ESP task stack.
- Settings UI edit session does not copy the entire `AppConfig`; only saves the field-level dirty value or active editor
buffer.
- SD working-configuration import does not read the complete file, does not construct the tree, and does not use `cJSON_ParseWithLength`.
- Do not introduce `std::deque` to ESP BLE/Meshtastic bridge headers.
- New schema table uses static/constexpr storage.
- The new channel list uses a fixed upper limit and explicit drop/error policy, and cannot grow without bounds.
- Large string formatting uses caller-provided buffer or small scratch owner, without placing temporary large objects on the callback stack.
## Packaged Delivery Plan
This refactor is delivered as one cohesive feature package, not as user-visible partial phases.
The steps below are an internal construction sequence only. The final deliverable must include
schema, protocol-aware UI, persistence, SD-first configuration, runtime apply behavior, tests and
verification together.
No intermediate state should be considered complete if it leaves settings half migrated, exposes
new protocol pages without matching persistence, or creates a second SD configuration authority.
### Slice 0: Specification and Audit
Deliverables:
- This document.
- Current settings/persistence/apply code listing.
- Confirm SD-first TMS authority and JSON retirement direction.
No runtime behavior change.
### Slice 1: Descriptor Read Model
Introduce descriptor tables and read accessors without changing existing UI behavior.
Deliverables:
- `SettingsFieldId` enum.
- protocol/global owner metadata.
- field descriptors for all currently visible settings.
- tests that every field has default, storage key, owner, visibility, runtime impact.
Compatibility:
- Existing `AppConfig` remains backing store.
- Existing LVGL settings can still use old state while descriptors are validated in tests.
### Slice 2: Transaction and Apply Dispatcher
Move settings mutation through a small transaction boundary.
Deliverables:
- field-level set/get APIs.
- validator/normalizer.
- runtime impact diff.
- dispatcher that calls `applyMeshConfig()`、`applyUserInfo()`、`applyPositionConfig()`、
MQTT restart/stop and BLE policy in one place.
Compatibility:
- Existing UI callbacks can be converted incrementally field by field.
### Slice 3: Protocol-Aware UI Sections
Replace `kChatItems` / `kNetworkItems` as primary organization.
Deliverables:
- Device/Connectivity/Protocol/Channels/MQTT/GPS & Map/Privacy/Maintenance sections.
- Active protocol filter from descriptor metadata.
- Board capability filter from descriptor metadata.
- No business visibility based on `pref_key` string comparisons.
Acceptance:
- Selecting Meshtastic shows Meshtastic channel/MQTT/radio settings only.
- Selecting MeshCore shows MeshCore channel/MQTT/radio settings only.
- Selecting Reticulum shows Reticulum interface/group settings and hides MQTT.
### Slice 4: Bounded SD Working Configuration
Replace the former JSON backup writer/reader with the startup-authoritative `.tms` working document.
Deliverables:
- streaming writer.
- streaming parser.
- fixed max line length.
- CRC.
- descriptor-backed working-TMS coverage.
- legacy schema v2/v3 import that emits a complete strict schema-v4 document on the
next working-configuration save.
Acceptance:
- Full working configuration synchronization succeeds without whole-file allocation.
- Unknown or misspelled keys are rejected safely before any configuration is applied.
- Sensitive fields import correctly and logs are redacted.
### Slice 5: Channel Management
Introduce protocol-specific channel/group management.
Deliverables:
- Meshtastic channel slot model.
- MeshCore channel slot model.
- Reticulum group/destination model remains separate.
- Create/join/share flow for supported protocols.
- QR/import/export payload generation on demand, using bounded scratch storage.
Acceptance:
- Creating a Meshtastic channel does not mutate MeshCore fields.
- Creating a MeshCore channel does not mutate Meshtastic fields.
- Broadcast/private conversation selection references protocol-specific channel identity explicitly.
### Slice 6: Retire Raw Struct Persistence
After migrations are covered by tests and field store is proven:
- Stop writing raw `AppConfig` blobs.
- Keep one-way read migration for a bounded release window.
- Remove legacy keys only after working-configuration migration tests prove no supported user path is lost.
### Package Acceptance
The package is not done until all of these are true:
- Protocol selection changes visible settings, stored settings and runtime apply behavior together.
- Meshtastic, MeshCore and Reticulum each have their own settings surface; hidden fields are hidden by
descriptor/capability metadata, not by ad hoc string checks.
- Meshtastic MQTT and MeshCore MQTT can be configured independently and are persisted in the working configuration.
- Wi-Fi off stops MQTT runtime; Wi-Fi on does not auto-enable MQTT config.
- The SD working configuration writes and reads `.tms`, and all supported settings covered by descriptors round trip.
- Legacy Preferences/IDF/raw config paths migrate into the new schema without losing existing user settings.
- Settings UI does not create new large ESP stack objects or whole-config drafts.
- Tests and stack hygiene checks pass for the touched areas.
## Verification Requirements
Before implementation PR/commit:
- Run GitNexus impact analysis before each edited symbol, and warn before HIGH/CRITICAL edits.
- Run unit tests for descriptor coverage, migration, transaction diff and the TMS parser.
- Run `python3 scripts/check_esp_stack_hygiene.py` when touching settings save/load, ESP BLE,
Meshtastic bridge, or app config code.
- For PlatformIO build/upload/monitor, use background process + log polling as required by repo rules.
- Run `detect_changes()` before commit to verify affected symbols and flows.
Suggested tests:
| Test | Purpose |
| --- | --- |
| descriptor coverage snapshot | every field has owner, protocol visibility, storage key, default, impact |
| protocol visibility matrix | MT/MC/Reticulum show different settings |
| legacy Preferences migration | existing NVS keys map to schema fields |
| IDF raw blob migration | old raw config can migrate once |
| `.tms` round trip | export -> boot import produces equivalent configuration |
| `.tms` malformed input | long line, bad type, bad hex, unknown key, bad CRC handled safely |
| MQTT policy matrix | Wi-Fi off stops runtime; Wi-Fi on does not enable config; protocol switch stops old runtime |
| contact projection parity | MQTT receive and LoRa receive update contacts/nearby through same app event path |
## Explicit Non-Goals
- Does not support Meshtastic and MeshCore MQTT interoperability.
- Do not add TLS for MQTT.
- Do not disguise Reticulum as MQTT/channel page.
- Don't force a generic `Channel` type onto three protocols.
- Don't use whole-document JSON in new SD configuration paths.
- Discontinue using raw `sizeof(AppConfig)` as the new persistence format.
- Do not continue to expand the `pref_key` string blacklist for fast UI hiding.
- Don't stuff a lot of channel or QR/share payloads into `AppConfig`.
## Open Decisions
| Decision | Recommendation |
| --- | --- |
| `.tms` max line length | Fixed at 383 content bytes; max document size is 32 KiB |
| Legacy schema migration | Accept TMSET2 through TMSET6 as one-time migration inputs; TMSET6 is accepted only as its complete pre-release dialect, and all new writes emit canonical strict TMSET7 including Reticulum network/LXMF and BLE records |
| MeshCore public MQTT preset | Verify upstream/community default before hardcoding; otherwise default disabled with preset picker |
| Meshtastic channel slot count | Implement current primary/secondary first, schema list-ready |
| IDF protocol support | Current IDF runtime appears Meshtastic-only; full protocol UI must either expose capability limits or implement MC/RT there first |
| Sensitive working-file UX | Keep full capability, with explicit plaintext warning/redaction rather than silently omitting secrets |
## Implementation Guardrail
Even though this is a single packaged feature, implementation should still proceed in a safe internal
order. The first code change after this spec should not rewrite all settings UI at once. The safest opening
move is:
1. Add schema field IDs and descriptor coverage tests.
2. Map descriptors to existing `AppConfig` read paths.
3. Add protocol visibility tests for MT/MC/Reticulum.
4. Only then start moving UI sections and persistence writers.
This keeps the refactor observable during development while still packaging the final user-facing result as
one complete settings architecture change.