feat: export and import whole device configuration (#1924)#2023
feat: export and import whole device configuration (#1924)#2023bruschill wants to merge 19 commits into
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).
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.
# Conflicts: # Meshtastic.xcodeproj/project.pbxproj
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.
…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.
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds connected-node device profile export/import using protobuf ChangesDevice Profile Export/Import
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant Tools
participant ImportDeviceProfileView
participant DeviceProfileImporter
participant AccessoryManager
User->>Tools: Select .cfg file
Tools->>ImportDeviceProfileView: Present parsed import plan
User->>ImportDeviceProfileView: Select sections and confirm
ImportDeviceProfileView->>DeviceProfileImporter: Apply selected items
DeviceProfileImporter->>AccessoryManager: Send configuration updates
AccessoryManager-->>DeviceProfileImporter: Return success or failure
DeviceProfileImporter-->>ImportDeviceProfileView: Report applied, skipped, and failed items
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Add a Tools section to docs/user/settings.md covering the NFC contact tag, Export Configuration, and the new Import Configuration flow (per-section review, Security defaults off, reboot/secret warnings). Regenerate the bundled HTML via scripts/build-docs.sh.
|
Android will import a partial config - if a cfg doesn't have the longname populated for example, it will leave it as is on the node. I haven't read your code yet but I want to ask that you also support a partial config import. I see a future where events and stuff can just pass an official config file that sets everything except names and keys and stuff instead of needing to do event-specific firmware and stuff. My next PR will be for CLI to export a partial config to create these |
93290f5 to
6052768
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
docs/developer/deep-links.md (1)
58-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing hyperlink wrapper for consistency.
Every other static (parameterless) Settings route in this table is a clickable link, but this new row is plain code text.
📝 Proposed fix
-| `meshtastic:///settings/localMeshDiscovery/history` | Local Mesh Discovery (history view) | +| [`meshtastic:///settings/localMeshDiscovery/history`](meshtastic:///settings/localMeshDiscovery/history) | Local Mesh Discovery (history view) |🤖 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 `@docs/developer/deep-links.md` at line 58, The new Settings deep-link entry is listed as plain code text instead of a clickable link, unlike the other static routes in this table. Update the deep-links markdown entry for the local mesh discovery history route so it uses the same hyperlink wrapper style as the other parameterless Settings routes, keeping the table formatting consistent.Meshtastic/Views/Settings/Tools.swift (1)
128-137: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer
Task {@mainactorin ... }overDispatchQueue.main.async.This defers presenting the file exporter to the next runloop tick, but uses a GCD callback instead of Swift concurrency.
♻️ Proposed refactor
Button("Export Configuration") { // Re-resolve the connected node at confirm time — the entity is fetched fresh rather than // captured at button-tap, so it can't be a stale/faulted object if the device disconnects // while the dialog is open. Defer to the next runloop so presenting the file exporter isn't // swallowed by the confirmation dialog's dismissal animation. - DispatchQueue.main.async { + Task { `@MainActor` in guard let node = connectedNode else { return } exportConfiguration(for: node) } }As per coding guidelines, "Prefer
async/awaitover callback-based APIs for new code" and "UseTaskfor fire-and-forget async work."🤖 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/Views/Settings/Tools.swift` around lines 128 - 137, The Export Configuration action currently defers work with DispatchQueue.main.async, but this should use Swift concurrency instead. Update the Button action in Tools.swift to wrap the re-resolution of connectedNode and the exportConfiguration(for:) call inside a fire-and-forget Task { `@MainActor` in ... } so the exporter is still presented on the next runloop tick without using GCD.Source: Coding guidelines
🤖 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 `@docs/user/settings.md`:
- Line 170: The Warning callout title in the settings docs is too long and
should be shortened to match the style of nearby callout titles. Update the
warning text in the settings documentation so the title is concise, and ensure
the closing bold marker is immediately adjacent to the last word; use the
existing callout examples like the Warning and Tip titles in this file as the
style reference.
In `@Meshtastic/Export/DeviceProfileExport.swift`:
- Around line 447-448: The export logic in DeviceProfileExport’s channel
settings uses direct UInt32 casts for ChannelEntity.id and
ChannelEntity.positionPrecision, which can trap on negative persisted values.
Update the assignments in the export flow to use truncating conversion for both
fields, keeping the behavior in the DeviceProfileExport code path safe even when
malformed data is encountered.
---
Nitpick comments:
In `@docs/developer/deep-links.md`:
- Line 58: The new Settings deep-link entry is listed as plain code text instead
of a clickable link, unlike the other static routes in this table. Update the
deep-links markdown entry for the local mesh discovery history route so it uses
the same hyperlink wrapper style as the other parameterless Settings routes,
keeping the table formatting consistent.
In `@Meshtastic/Views/Settings/Tools.swift`:
- Around line 128-137: The Export Configuration action currently defers work
with DispatchQueue.main.async, but this should use Swift concurrency instead.
Update the Button action in Tools.swift to wrap the re-resolution of
connectedNode and the exportConfiguration(for:) call inside a fire-and-forget
Task { `@MainActor` in ... } so the exporter is still presented on the next
runloop tick without using GCD.
🪄 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: a50acc4c-65fe-4bdc-8155-145be725217b
📒 Files selected for processing (19)
Meshtastic.xcodeproj/project.pbxprojMeshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swiftMeshtastic/Export/DeviceProfileDocument.swiftMeshtastic/Export/DeviceProfileExport.swiftMeshtastic/Extensions/Date.swiftMeshtastic/Import/DeviceProfileImport.swiftMeshtastic/Import/DeviceProfileImporter.swiftMeshtastic/Persistence/UpdateSwiftData.swiftMeshtastic/Resources/docs/developer/deep-links.htmlMeshtastic/Resources/docs/index.jsonMeshtastic/Resources/docs/markdown/developer/deep-links.mdMeshtastic/Resources/docs/markdown/user/settings.mdMeshtastic/Resources/docs/user/settings.htmlMeshtastic/Views/Settings/ImportDeviceProfileView.swiftMeshtastic/Views/Settings/Tools.swiftMeshtasticTests/DeviceProfileExportTests.swiftMeshtasticTests/DeviceProfileImportTests.swiftdocs/developer/deep-links.mddocs/user/settings.md
Partial-import safety (from an adversarial safety review — device state was already coherent/recoverable in every case; these address UI liveness and honest reporting): - Make the apply cancellable: a Cancel button on the applying screen, a Task.isCancelled check per item in the engine, and a CancellationError catch. A liveness backstop (canForceDismiss) lets the user out even if a single send stalls on the transport, so the modal can never trap them. - DeviceProfileImportResult gains `wasCancelled`; isCompleteSuccess accounts for it. Cancellation records current + remaining items as skipped, not failed. - Honest result copy: report "Sent N settings" (sends are fire-and-forget, not device-acked), always show a reconnect-to-verify hint, and warn that a failed channel replace may have partially applied. Human-readable labels (ImportItemKind.displayName) replace raw case names in progress/results. Localization: - ImportItemKind.displayName and ImportSection.title were plain Strings shown via Text(variable) and never localized. Route them through `.localized` with casing that matches existing config-screen keys so the 25 shared labels reuse their translations. - Localize the assembled confirmation/sensitive-material messages via `.localized` fragments + String(format:). - Register all new import (and previously-unlocalized export) UI strings in Localizable.xcstrings as source-language entries so translators / the translate-locale.sh pass can fill the other 17 locales. Tests: add cancellation coverage (CancellationError path + pre-cancelled task) and a displayName label check. 25 import tests pass; app builds for the iOS 18 simulator.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
MeshtasticTests/DeviceProfileImportTests.swift (2)
453-497: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGuarded assertion asserts nothing when
wasCancelledis false.Lines 494-496 wrap the key assertion in
if result.wasCancelled, so if the pre-cancelled task instead completes fully (i.e.wasCancelledis false), the test passes without verifying the invariant it's meant to check (that a coherent count of applied+skipped items exists). Consider asserting unconditionally, e.g. computeapplied.count + skipped.countand compare againstplan.items.countregardless of which path was taken, or split into two explicit branches with an assertion in each.🤖 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 `@MeshtasticTests/DeviceProfileImportTests.swift` around lines 453 - 497, The pre-cancelled task test in engineHonorsPreCancelledTask has a guarded assertion that can be skipped when result.wasCancelled is false, so the invariant is not always checked. Update the test to assert the applied/skipped item count against plan.items.count unconditionally, or split the cancelled and non-cancelled paths so each branch contains a real assertion; use the existing DeviceProfileImporter.apply, result.wasCancelled, result.applied, result.skipped, and plan.items symbols to keep the check tied to the importer behavior.
280-297: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing assertion for
containsSensitiveafter deselecting sensitive sections.The comment claims deselecting sensitive/reboot sections "clears both aggregates," but only
willReboot(in: safe)is asserted;containsSensitive(in: safe)is never checked, leaving that half of the claim unverified.🧪 Proposed fix
let safe: Set<ImportSection> = [.owner, .radioAndDevice, .modules, .personalization, .fixedPosition] `#expect`(!plan.willReboot(in: safe)) + `#expect`(!plan.containsSensitive(in: safe))🤖 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 `@MeshtasticTests/DeviceProfileImportTests.swift` around lines 280 - 297, The DeviceProfileImportPlan flags test verifies reboot clearing but does not assert the sensitive aggregate after deselecting sections. Update the flags() test in DeviceProfileImportTests to also check containsSensitive(in:) against the safe ImportSection set, alongside the existing willReboot(in:) assertion, so the “clears both aggregates” claim is fully covered. Use the existing plan.containsSensitive and plan.willReboot calls in the flags() test to locate the change.
🤖 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.
Nitpick comments:
In `@MeshtasticTests/DeviceProfileImportTests.swift`:
- Around line 453-497: The pre-cancelled task test in
engineHonorsPreCancelledTask has a guarded assertion that can be skipped when
result.wasCancelled is false, so the invariant is not always checked. Update the
test to assert the applied/skipped item count against plan.items.count
unconditionally, or split the cancelled and non-cancelled paths so each branch
contains a real assertion; use the existing DeviceProfileImporter.apply,
result.wasCancelled, result.applied, result.skipped, and plan.items symbols to
keep the check tied to the importer behavior.
- Around line 280-297: The DeviceProfileImportPlan flags test verifies reboot
clearing but does not assert the sensitive aggregate after deselecting sections.
Update the flags() test in DeviceProfileImportTests to also check
containsSensitive(in:) against the safe ImportSection set, alongside the
existing willReboot(in:) assertion, so the “clears both aggregates” claim is
fully covered. Use the existing plan.containsSensitive and plan.willReboot calls
in the flags() test to locate the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 88b0edfd-e65a-4892-af59-6e4cc60ce1a9
📒 Files selected for processing (5)
Localizable.xcstringsMeshtastic/Import/DeviceProfileImport.swiftMeshtastic/Import/DeviceProfileImporter.swiftMeshtastic/Views/Settings/ImportDeviceProfileView.swiftMeshtasticTests/DeviceProfileImportTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
- Meshtastic/Import/DeviceProfileImporter.swift
- Meshtastic/Import/DeviceProfileImport.swift
- Meshtastic/Views/Settings/ImportDeviceProfileView.swift
…config # Conflicts: # Localizable.xcstrings
|
@coderabbitai review |
✅ Action performedReview finished.
|
- DeviceProfileExport: use UInt32(truncatingIfNeeded:) for ChannelEntity.id and positionPrecision (both Int32) so malformed negative persisted values can't trap the export. - docs/user/settings.md: shorten the import Warning callout title per the callout-title guideline; regenerate bundled HTML. - DeviceProfileImportTests: assert containsSensitive is false for a genuinely non-sensitive selection (the old set wrongly included Modules, which carries the MQTT password); make the pre-cancelled-task test assert coherent invariants unconditionally instead of only when wasCancelled.
…file omits them Addresses maintainer feedback on meshtastic#2023 (tylerpieper): support importing a partial/"official" event config that sets everything except names and keys, leaving the node's own values in place. Block-level and owner-name partial import already worked (the plan is has*-gated per config block, and ownerUser merges only the long/short name present). This adds the missing "keys" half: a present SecurityConfig block previously replaced the node's identity keypair wholesale, so an event config that only set flags or admin keys would wipe the node's public/private key. DeviceProfileImportPlan.securityConfig(from:base:) now merges the profile's security block onto the node's current one, preserving the existing private/ public/admin keys whenever the profile leaves them empty. An empty key is never a valid identity, so empty-as-absent is safe: full backups (keys present) still restore the keypair; partial configs keep the node's identity. The review-sheet summary reflects when identity is preserved. Adds 6 tests covering preserve-when-absent, replace-when-present, per-field independence, the no-base passthrough, and the emitted item's payload/summary. All 31 DeviceProfileImportTests pass.
…config # Conflicts: # Localizable.xcstrings # Meshtastic.xcodeproj/project.pbxproj # Meshtastic/Resources/docs/index.json
…config # Conflicts: # Meshtastic/Resources/docs/index.json
What changed?
Adds Export and Import of a node's whole configuration to Settings → Tools, using the binary
DeviceProfile(.cfg) format shared with the Android app and themeshtasticCLI.Export
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, with an Android-style default filename and a shared.meshtasticDeviceProfileUTType.Tools.swift— the export button +fileExporter, gated on a connected node, behind a confirmation dialog that warns about the sensitive material in the file.UpdateSwiftData.swift— persists MQTTmapReportingShouldReportLocationin the config-update branch (it was only set on insert) so the exported value isn't a stale default.Import
DeviceProfileImport.swift— pure, device-free core.parseDeviceProfiledefensively decodes untrusted bytes (size cap before decode, capped read at the call site).DeviceProfileImportPlan(profile:currentUser:)is thehas*-gated inverse ofexportDeviceProfile()— the single source of coverage truth so export/import can't silently drift. It emits an ordered[ImportItem]plan grouped into 8 coarse toggle sections, each item carryingisSensitive/causesRebootflags.DeviceProfileImporter.swift— aProfileApplyGatewayseam plus the apply engine (sequential,isConnectedpre-checks, abort-on-first-failure). The production gateway forwards each item to the existingsave*Config/saveUser/saveChannelSet/saveRtttlConfig/saveCannedMessageModuleMessages/setFixedPositioncalls, withfromUser == toUser ==the connected node (self-admin, no session passkey).ImportDeviceProfileView.swift— review sheet: per-section toggles, live sensitive-material + reboot banners, a confirmation gate, per-item progress, and an applied/failed/skipped result summary.Tools.swift— the Import section +.fileImporter([.meshtasticDeviceProfile])with a capped read, parse, and the review sheet.AccessoryManager+ToRadio.swift— newsetFixedPosition(_:fromUser:toUser:)overload to restore explicit coordinates from a profile (the existing overload only uses the phone's live GPS fix).Import design decisions worth a look:
LoRaConfiginto bothconfig.loraand the channel URL'sChannelSet. The plan emits exactly one terminal item — the channel URL when it's a valid replace-mode set with a LoRa config, otherwise the standalone LoRa config (add-mode or LoRa-less URLs fall back so the region is still restored).node.user.toProto()and overrides only long/short name, soid/hwModel/publicKey/roleare never wiped bysetOwner.Task.isCancelled/CancellationErrorhandling, and a liveness backstop so a single stalled send can never trap the sheet); the result reports what was sent (admin sends are fire-and-forget, not device-acked), always nudges the user to reconnect and verify, and warns when a destructive channel replace may have partially applied.ImportItemKind.displayNamegives human-readable labels in progress/results instead of raw case names.Localization
ImportItemKind.displayNameandImportSection.titlewere plainStrings shown viaText(variable), which SwiftUI does not localize — routed them through.localizedwith casing that matches existing config-screen strings, so 25 shared labels reuse their translations. The assembled confirmation/sensitive-material messages are localized via.localizedfragments +String(format:).Localizable.xcstringsas source-language entries (English default = key), so translators /scripts/translate-locale.shcan fill the other 17 locales.Docs
docs/user/settings.md(NFC contact tag, Export, and Import) — the Tools screen was previously undocumented — and regenerates the bundled in-app HTML.Why did it change?
The Android app and
meshtasticCLI can export and import a device's full configuration; iOS/macOS could do neither. The.cfgbinaryDeviceProfileformat matches Android, so a profile can be backed up, restored, or cloned onto another node across the Meshtastic ecosystem. Shipping export and import together closes the loop in one change.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.DeviceProfileImportTests(Swift Testing) — 25 tests covering defensive parse + export↔import round-trip, coverage parity with export (all 8 device + 15 module configs;meshBeacon/remoteHardwareintentionally absent), apply ordering, LoRa/channel dedupe including the add-mode and LoRa-less fallbacks, fixed-position gating, owner-identity preservation at the plan level, sensitivity/reboot flags, the engine's abort/skip/reboot semantics via a mock gateway, and cancellation handling (current + remaining items skipped, not failed).exportDeviceProfile()to keep the two directions symmetric, and hardened against partial-failure scenarios via an adversarial safety review.Screenshots/Videos (when applicable)
Checklist
docs/user/ordocs/developer/, and updated accordingly. Added a Tools section todocs/user/settings.md(NFC, Export & Import Configuration) and regenerated the bundled HTML.Notes for reviewers
.cfgbehavior (the format is meant to fully restore a device). No secret is logged.DeviceProfileImportPlanis the inverse ofexportDeviceProfile(), and each export converter is the inverse of anupsert*ConfigPacketmapping; a round-trip test locks the format, but a shared single source of truth between the directions would be the deeper fix for a future pass.Summary by CodeRabbit
.cfgfiles compatible with Android and the Meshtastic CLI.