feat: export whole device configuration (#1924)#1940
Conversation
Adds a "Export Configuration" action to Settings → Tools that saves the connected node's full configuration to a binary DeviceProfile (.cfg) file, matching the format the Android app and meshtastic CLI use so it can be backed up or imported elsewhere. - DeviceProfileExport.swift: entity → protobuf converters for all 8 device configs and 15 module configs (the inverse of the proto → entity mapping in UpdateSwiftData.swift), plus NodeInfoEntity.exportDeviceProfile() which assembles a DeviceProfile (long/short name, LocalConfig, LocalModuleConfig, channel-set URL, ringtone, canned messages, and the fixed position when set). - DeviceProfileDocument.swift: FileDocument wrapping the serialized profile. - Tools.swift: export button + fileExporter; disabled with guidance when no node is connected. - Tests covering the converters and DeviceProfile assembly incl. binary round-trip. Fixes found in a code review of this feature: - Export StoreForward isServer (was dropped) and the TAK, traffic-management, and status-message module configs (they ARE part of LocalModuleConfig). - Preserve admin-key slot alignment instead of compacting empty slots, which corrupted keys on round-trip when an earlier slot was empty. - Persist MQTT mapReportingShouldReportLocation in the upsert update branch (pre-existing bug that made the export read a stale default). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the 5 routable settings cases missing from the table (localMeshDiscovery + its history subpath, trafficManagement, takConfig, backupManagement, deviceLinks) and correct the tak label to "TAK Server" to distinguish it from the new takConfig TAK module config route. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # Meshtastic.xcodeproj/project.pbxproj
RCGV1
left a comment
There was a problem hiding this comment.
Compatibility/hardening review focused on Android .cfg parity:
-
[P2] Add a confirmation/warning before export because this file can include security/admin material. The export path serializes the full
DeviceProfile, includingconfig.security, admin keys/private key when present, and channel PSKs viachannelURL. Right nowTools.swiftstarts export directly from the button. Please add a warning/confirmation or selection step that makes it clear this backup can grant mesh/admin access if shared. -
[P2] Make the filename/type match Android more closely. Android currently exports names like
Meshtastic_${shortName}_${yyyyMMdd}_nodeConfig.cfg; this PR uses the long name, no explicit.cfgsuffix in the default filename, and a dynamicUTType(filenameExtension: "cfg"). Please use a sanitized Android-style filename with an explicit.cfgextension, and consider defining a stable exported UTType instead of recreating it inline. -
[P3] Surface file-export write failures to the user. The
.fileExportercompletion logs failures, but the visible alert only covers serialization/preparation failures before the exporter opens. If iOS fails to write/save the document, the user should see an alert instead of only a log entry.
The raw file format itself looks compatible with Android: both sides are using raw serialized DeviceProfile protobuf bytes, not JSON or a wrapper container.
|
@bruschill, I think this is a great feature. Can you address the issues I raised above |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds device configuration export to ChangesDevice Profile Export
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Meshtastic/Export/DeviceProfileExport.swift`:
- Around line 213-223: The store-forward export is reading a stale server flag
because the existing-entity path in upsertStoreForwardModuleConfigPacket updates
only some fields and never refreshes StoreForwardConfigEntity.isRouter, so
protoConfig keeps exporting the old value as isServer. Update the upsert logic
to assign the latest server-mode value to isRouter whenever an existing
StoreForwardConfigEntity is reused, so the protoConfig getter exports the
current state.
- Around line 140-145: The exported admin-key array is preserving stale trailing
slots, so retired `adminKey2`/`adminKey3` values can be written back into
backups. Update the export path in `DeviceProfileExport` to explicitly clear
removed admin-key slots before building `config.adminKey`, and ensure
`upsertSecurityConfigPacket`/the backing model does not leave old values behind
when the protobuf now contains fewer keys. Keep the positional mapping logic,
but only export slots that are still present in the current security config and
avoid carrying forward stale SwiftData values.
In `@Meshtastic/Views/Settings/Tools.swift`:
- Around line 98-104: The `.fileExporter` completion handler in the export flow
only logs `.failure` from the write step, so users don’t get visible feedback
when the save itself fails. Update the export handling around `Tools.swift`’s
`.fileExporter` result switch to surface the error in-app the same way prepare
failures are handled, and keep the existing `Logger.services` entry for
diagnostics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f92db928-0dfd-4019-af58-7b7c8b66dafd
📒 Files selected for processing (7)
Meshtastic.xcodeproj/project.pbxprojMeshtastic/Export/DeviceProfileDocument.swiftMeshtastic/Export/DeviceProfileExport.swiftMeshtastic/Persistence/UpdateSwiftData.swiftMeshtastic/Views/Settings/Tools.swiftMeshtasticTests/DeviceProfileExportTests.swiftdocs/developer/deep-links.md
| // The proto `adminKey` is a positional repeated field (index 0/1/2 map back to | ||
| // adminKey/adminKey2/adminKey3 on import), so preserve slots — nil/empty middle slots stay | ||
| // as empty entries and only trailing empties are trimmed. | ||
| var adminKeys = [adminKey, adminKey2, adminKey3].map { $0 ?? Data() } | ||
| while let last = adminKeys.last, last.isEmpty { adminKeys.removeLast() } | ||
| config.adminKey = adminKeys |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Clear removed admin-key slots before exporting.
upsertSecurityConfigPacket only overwrites the slots present in the incoming protobuf, so a node that shrinks from 3 admin keys to 1 or 2 keeps stale adminKey2/adminKey3 values in SwiftData. This exporter then preserves those non-empty trailing slots and writes retired admin keys back into the .cfg backup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Meshtastic/Export/DeviceProfileExport.swift` around lines 140 - 145, The
exported admin-key array is preserving stale trailing slots, so retired
`adminKey2`/`adminKey3` values can be written back into backups. Update the
export path in `DeviceProfileExport` to explicitly clear removed admin-key slots
before building `config.adminKey`, and ensure `upsertSecurityConfigPacket`/the
backing model does not leave old values behind when the protobuf now contains
fewer keys. Keep the positional mapping logic, but only export slots that are
still present in the current security config and avoid carrying forward stale
SwiftData values.
| extension StoreForwardConfigEntity { | ||
| var protoConfig: ModuleConfig.StoreForwardConfig { | ||
| var config = ModuleConfig.StoreForwardConfig() | ||
| config.enabled = enabled | ||
| config.isServer = isRouter | ||
| config.heartbeat = heartbeat | ||
| config.records = UInt32(truncatingIfNeeded: records) | ||
| config.historyReturnMax = UInt32(truncatingIfNeeded: historyReturnMax) | ||
| config.historyReturnWindow = UInt32(truncatingIfNeeded: historyReturnWindow) | ||
| return config | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Refresh the store-forward server flag on updates.
This exports isRouter as isServer, but the existing-entity branch in upsertStoreForwardModuleConfigPacket never updates isRouter. After the first sync, toggling server mode on the device will still export the old value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Meshtastic/Export/DeviceProfileExport.swift` around lines 213 - 223, The
store-forward export is reading a stale server flag because the existing-entity
path in upsertStoreForwardModuleConfigPacket updates only some fields and never
refreshes StoreForwardConfigEntity.isRouter, so protoConfig keeps exporting the
old value as isServer. Update the upsert logic to assign the latest server-mode
value to isRouter whenever an existing StoreForwardConfigEntity is reused, so
the protoConfig getter exports the current state.
CodeRabbit feedback: - Clear removed positional admin-key slots on security-config sync so a node shrinking from 3 admin keys to 1/2 no longer keeps (and exports) retired keys. Still gated on a non-empty reported array so a partial/empty packet can't wipe stored keys. - Refresh StoreForwardConfigEntity.isRouter on config update so server-mode changes are reflected in the export (the update branch never refreshed it). - Surface .fileExporter write failures in-app (not just a log), ignoring user cancellation. Also (code-review): drop the removed TrafficManagementConfig bool proto fields from the export — they were replaced upstream by the non-zero-uint32 convention, which was breaking the build; sanitize the export filename against path-illegal characters; update the affected tests. Addresses CodeRabbit + code-review feedback on meshtastic#1924.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Meshtastic/Persistence/UpdateSwiftData.swift`:
- Around line 1085-1088: In UpdateSwiftData.swift, the admin key update logic in
the security config path only handles the non-empty case, so an empty
config.adminKey leaves old values behind. Update the branch around the existing
adminKey assignments so that when config.adminKey is empty it explicitly clears
newSecurityConfig.adminKey, newSecurityConfig.adminKey2, and
newSecurityConfig.adminKey3, while preserving the current slot-by-slot
assignment when keys are present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 72aa73fd-c21b-4ba1-a0e5-b50de7a95a2c
📒 Files selected for processing (4)
Meshtastic/Export/DeviceProfileExport.swiftMeshtastic/Persistence/UpdateSwiftData.swiftMeshtastic/Views/Settings/Tools.swiftMeshtasticTests/DeviceProfileExportTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
- Meshtastic/Views/Settings/Tools.swift
- MeshtasticTests/DeviceProfileExportTests.swift
- Meshtastic/Export/DeviceProfileExport.swift
…Type Address the compatibility/hardening review on meshtastic#1940: - Add a confirmation dialog before exporting that warns the .cfg contains sensitive security material (private/admin keys, channel PSKs, Wi-Fi/MQTT passwords) before the file is written. - Match Android's filename: Meshtastic_<shortName>_<yyyyMMdd>_nodeConfig.cfg via a testable DeviceProfileDocument.exportFilename(...) helper and a new Date.exportDateStamp; the .cfg suffix comes from a stable, reused UTType.meshtasticDeviceProfile instead of an inline UTType per call site. - A name made only of path-illegal characters now trims to empty and falls back to "Node" instead of collapsing to a string of dashes. - Re-resolve the connected node at confirm time (not the entity captured at button tap) and defer the file exporter past the dialog dismissal. - Dedup the export DateFormatter boilerplate into cached static formatters. Tests: add DeviceProfileExportFilenameTests (filename format, fallbacks, illegal-char stripping, UTType extension); 22 tests pass.
Address CodeRabbit review: upsertSecurityConfigPacket only rewrote admin keys when the reported array was non-empty, so a node that drops to zero admin keys kept stale adminKey/adminKey2/adminKey3 values in SwiftData — which the device profile export then wrote back into the .cfg backup. Mirror every positional slot unconditionally (an empty array now clears all three) via a single applyAdminKeys(_:to:) helper shared by the insert and existing-entity branches, so the two stay in lockstep and slot 0 reads as the idiomatic `.first`. Security sub-configs arrive as full snapshots, so an empty admin_key reflects the device's true state.
|
Seeing this inspired me to have the hallucination machine implement this for CLI and Web as well. Now we will have one import/export format common across all platforms! |
|
This looks solid — nice symmetry with the Holding off on merging until the import side exists too — export alone isn't very useful without a way to bring a |
The committed docs/developer/deep-links.md was updated but the bundled HTML/markdown under Meshtastic/Resources/docs/ was never regenerated, so the in-app copy was stale. Rebuild via scripts/build-docs.sh.
|
Superseded by #2023, which contains this same export work plus the inverse import feature so the two ship together. Closing in favor of the combined PR. |
…Type Address the compatibility/hardening review on meshtastic#1940: - Add a confirmation dialog before exporting that warns the .cfg contains sensitive security material (private/admin keys, channel PSKs, Wi-Fi/MQTT passwords) before the file is written. - Match Android's filename: Meshtastic_<shortName>_<yyyyMMdd>_nodeConfig.cfg via a testable DeviceProfileDocument.exportFilename(...) helper and a new Date.exportDateStamp; the .cfg suffix comes from a stable, reused UTType.meshtasticDeviceProfile instead of an inline UTType per call site. - A name made only of path-illegal characters now trims to empty and falls back to "Node" instead of collapsing to a string of dashes. - Re-resolve the connected node at confirm time (not the entity captured at button tap) and defer the file exporter past the dialog dismissal. - Dedup the export DateFormatter boilerplate into cached static formatters. Tests: add DeviceProfileExportFilenameTests (filename format, fallbacks, illegal-char stripping, UTType extension); 22 tests pass.
Adds an "Import Device Configuration" action to Settings → Tools, the inverse of the Export Configuration feature (meshtastic#1940). It applies a saved binary DeviceProfile (.cfg) file to the connected node, matching the Android app and meshtastic CLI so a backup can be restored across the ecosystem. - DeviceProfileImport.swift — pure, device-free core: defensive parse (size-cap before decode), and DeviceProfileImportPlan, the has*-gated inverse of exportDeviceProfile(). Emits an ordered [ImportItem] plan (owner, radio/device, modules, personalization, fixed position, network, bluetooth, security, then Channels & LoRa last because it reboots) with per-item isSensitive/causesReboot flags and coarse ImportSection groups. Dedupes the LoRa config that export writes into both config.lora and the channel URL, preferring the URL when it's a valid replace-mode set (else the standalone LoRa config). - DeviceProfileImporter.swift — ProfileApplyGateway seam + apply engine (sequential, isConnected pre-checks, abort-on-first-failure). The production gateway forwards each item to the existing save*Config/saveUser/saveChannelSet/setFixedPosition calls; the standalone-LoRa reboot disconnect is swallowed only when the link actually drops. - ImportDeviceProfileView.swift — review sheet with per-section toggles (Security & Identity defaults OFF), live sensitive-material + reboot banners, a confirmation gate, per-item progress, and an applied/failed/skipped result summary. - Tools.swift — Import section + .fileImporter([.meshtasticDeviceProfile]) with a capped read, parse, and the review sheet. - AccessoryManager+ToRadio.swift — new setFixedPosition(_:fromUser:toUser:) overload to restore explicit coordinates from a profile (the existing overload only uses the phone's GPS). Tests: DeviceProfileImportTests (Swift Testing) — 22 tests covering parse/round-trip, coverage vs. export, apply ordering, LoRa/channel dedupe fallbacks, fixed-position and owner-identity preservation, sensitivity/reboot flags, and the engine's abort/skip/reboot semantics via a mock gateway. All pass; full app build succeeds for the iOS simulator.
What changed?
Adds an Export Configuration action to Settings → Tools that saves the connected node's full configuration to a binary
DeviceProfile(.cfg) file.DeviceProfileExport.swift— entity → protobuf converters for all 8 device configs and 15 module configs (the inverse of the proto → entity mapping inUpdateSwiftData.swift), plusNodeInfoEntity.exportDeviceProfile()which assembles aDeviceProfile: long/short name,LocalConfig,LocalModuleConfig, the channel-set URL, ringtone, canned messages, and the fixed position when one is set.DeviceProfileDocument.swift— aFileDocumentwrapping the serialized profile bytes for.fileExporter.Tools.swift— the export button +fileExporter; shown with guidance/disabled when no node is connected.UpdateSwiftData.swift— persists MQTTmapReportingShouldReportLocationin the config-update branch (it was only set on insert), so the exported value isn't a stale default.Closes #1924.
Why did it change?
The Android app and the
meshtasticCLI can export a device's full configuration; iOS/macOS could not. The.cfgbinaryDeviceProfileformat matches Android, so an exported file can be backed up and re-imported across the Meshtastic ecosystem.How is this tested?
DeviceProfileExportTests(Swift Testing) — 16 tests covering the converters (enum mapping, negated-flag inversion, packed IPv4 bit-pattern, admin-key slot preservation, nested map-report settings,isServer, TAK/traffic/status) andDeviceProfileassembly including a binary serialization round-trip. All pass.upsert*ConfigPacketfunctions inUpdateSwiftData.swift(the import mapping) to keep export/import symmetric.Screenshots/Videos (when applicable)
Checklist
docs/user/ordocs/developer/. A short note in the Tools/settings docs may be warranted; happy to add it here or apply theskip-docs-checklabel per maintainer preference.Notes for reviewers
.cfgbehavior (the format is meant to fully restore a device), but the exported file should be treated as secret. Worth a deliberate decision if iOS wants to diverge (e.g. prompt or redact).protoConfigconverter is the inverse of anupsert*ConfigPacketmapping, and the two can silently drift as config fields are added (the file header comments call this out). A shared single source of truth between import and export would be the deeper fix for a future pass.🤖 Generated with Claude Code
Summary by CodeRabbit
.cfgfile with a safe, Android-style filename and correct export type.