Skip to content

feat: import whole device configuration (stacked on #1940)#2

Closed
bruschill wants to merge 3 commits into
feat/export-device-configfrom
feat/import-device-config
Closed

feat: import whole device configuration (stacked on #1940)#2
bruschill wants to merge 3 commits into
feat/export-device-configfrom
feat/import-device-config

Conversation

@bruschill

@bruschill bruschill commented Jul 3, 2026

Copy link
Copy Markdown
Owner

What changed?

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.

Stacked on meshtastic#1940. This PR targets feat/export-device-config so the diff is import-only. Retarget to main once the export PR merges. Import reuses the export branch's DeviceProfile format and MeshtasticChannelURL (from meshtastic#1983).

  • Meshtastic/Import/DeviceProfileImport.swift — pure, device-free core. parseDeviceProfile defensively decodes untrusted bytes (size cap before decode). DeviceProfileImportPlan(profile:currentUser:) is the has*-gated inverse of exportDeviceProfile(): it emits an ordered [ImportItem] plan and is the single source of coverage truth so export/import can't silently drift. Items are grouped into 8 coarse toggle sections and carry isSensitive/causesReboot flags.
  • Meshtastic/Import/DeviceProfileImporter.swift — a ProfileApplyGateway seam plus the apply engine (sequential, isConnected pre-checks, abort-on-first-failure). The production gateway forwards each item to the existing save*Config / saveUser / saveChannelSet / saveRtttlConfig / saveCannedMessageModuleMessages / setFixedPosition calls, with fromUser == toUser == the connected node (self-admin, no session passkey).
  • Meshtastic/Views/Settings/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.
  • Meshtastic/Views/Settings/Tools.swift — the 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 live GPS fix).

Design decisions worth a look:

  • Apply order is fixed and reboot-safe. The plan emits items in a strict order (owner → radio/device → modules → personalization → fixed position → network → bluetooth → security → Channels & LoRa last). The UI filters by section but never reorders, so the reboot-prone LoRa/channel step is always last and can't drop later sends. Network/Bluetooth are placed late because they can drop the transport the import runs over.
  • Security & Identity defaults OFF. Importing a foreign private/admin key rewrites this node's cryptographic identity and can break existing DMs / lock local admin, so it's a deliberate opt-in. Every other present section defaults ON, preserving a one-tap "restore my device" flow.
  • LoRa/channel dedupe. Export writes the same LoRaConfig into both config.lora and the channel URL's ChannelSet. 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).
  • Owner merge preserves identity. The owner step starts from the live node.user.toProto() and overrides only long/short name, so id/hwModel/publicKey/role are never wiped by setOwner.

Why did it change?

The Android app and meshtastic CLI can import a device's full configuration from a .cfg; iOS/macOS could export (meshtastic#1940) but not import. This closes the loop so a backup can be restored — or a config cloned onto a second node — across the Meshtastic ecosystem.

How is this tested?

  • New DeviceProfileImportTests (Swift Testing) — 22 tests, all passing: defensive parse + export↔import round-trip, coverage parity with export (all 8 device + 15 module configs; meshBeacon/remoteHardware intentionally 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, and the engine's abort/skip/reboot semantics via a mock gateway.
  • Full app build succeeds for the iOS 18 simulator (Debug).
  • The plan was reviewed field-by-field against exportDeviceProfile() to keep import/export symmetric.

Screenshots/Videos (when applicable)

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated accordingly. Added a Tools section to docs/user/settings.md (NFC, Export & Import Configuration) and regenerated the bundled HTML.
  • I have tested the change to ensure that it works as intended.

Notes for reviewers

  • Sensitive material is written to the device in plaintext. The .cfg carries the private key, admin keys, channel PSKs, and Wi-Fi/MQTT passwords, and import applies them (strictly more dangerous than export, which only reads). Handling: Security & Identity defaults OFF, a live banner itemizes exactly what a given selection overwrites, and a confirmation dialog gates apply. No secret is logged.
  • Export ↔ import remain two hand-maintained mirrors. DeviceProfileImportPlan is the inverse of exportDeviceProfile(); a round-trip test locks the format, but a shared single source of truth between the two is the deeper follow-up (also noted on feat: export whole device configuration (#1924) meshtastic/Meshtastic-Apple#1940).

🤖 Generated with Claude Code

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.
bruschill added 2 commits July 3, 2026 11:40
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.
@bruschill

Copy link
Copy Markdown
Owner Author

No longer needed — the export and import work is combined in the upstream PR meshtastic#2023.

@bruschill bruschill closed this Jul 3, 2026
garthvh pushed a commit that referenced this pull request Jul 15, 2026
…shtastic#2060) (meshtastic#2082)

* feat(event-firmware): add off-device event-firmware metadata pipeline (meshtastic#2060)

Aligns Meshtastic-Apple with the cross-platform event-firmware spec
(design#120 / Meshtastic-Android #6162/#6163). A device only reports which
event edition it runs via MyNodeInfo.firmwareEdition; the display data now
lives off-device so a new event ships without an App Store release.

Pipeline + cache (gap #1, unblocks the rest):
- New EventFirmwareEntity @model caches the v2 payload (name, welcome message,
  dates, IANA time zone, location, accent color, icon, links, theme, firmware
  build) with derived helpers (hex-color parsing, links JSON round-trip,
  timezone-aware hasEnded lifecycle). Registered in MeshtasticSchemaV1.
- MeshtasticAPI fetches https://api.meshtastic.org/resource/eventFirmware:
  bundled event_firmware.json seed at launch (offline-first) + a background
  live refresh with NO short local timeout (the endpoint is 20-60s; a short
  deadline would pin users to the seed). An empty/failed response is a no-op
  that leaves the cache intact; a successful payload upserts + prunes orphans
  on the main context.
- FirmwareEditions gains editionKey <-> init?(editionKey:) mapping as the
  stable join key; lastEventFirmwareAPIUpdate tracks refreshes.

Consumption:
- Connect screen: EventFirmwareBadge replaces the hardcoded orange label with
  the fetched displayName tinted by accentColor (falling back to the enum name
  and .accentColor), plus the welcome message (gap #3, minimal).
- Post-event nudge (gap #2): a persistent banner appears when the connected
  device runs an event edition whose eventEnd (in its IANA zone) has passed,
  linking to the firmware-update flow; clears on return to vanilla. A missing/
  unparseable eventEnd never counts as ended.

Tests: 16 passing (edition-key mapping, hex byte-order, links round-trip,
hasEnded past/future/missing/unparseable + end-of-day zone boundary).
Docs: swiftdata.md documents the new runtime-cache entity (+ regenerated
bundled docs).

Deferred follow-ups (tracked by meshtastic#2060): ambient theme accent wash + opt-out
toggle, event info sheet (links/location/dates), theme.fonts, and the
firmware{}.zipUrl "flash the event firmware" flow.

* feat(event-firmware): event info sheet, ambient theme, fonts & firmware compare (meshtastic#2060)

Completes the remaining event-firmware gaps on top of the metadata pipeline,
so meshtastic#2060 is fully covered rather than pipeline-only.

Event info surface (gap #3):
- New EventFirmwareInfoView presented by tapping the (now tappable, icon-
  bearing) event badge: welcome message, location, dates, theme name/tagline,
  palette swatches, links, and the event firmware build (version + comparison
  to the connected device + a NavigationLink into the Firmware update flow +
  release notes disclosure).

Ambient theme + opt-out (gap #3):
- Subtle accent wash over the Connect grouped background (not a full recolor),
  gated on the connected event edition and the new UserDefaults.useEventTheme
  opt-in (default on). The "Use Event Theme" toggle lives in the info sheet and
  is shared via @AppStorage; opting out keeps the branding visible so it can be
  re-enabled.
- Event icon (iconUrl) via AsyncImage with an accent-tinted sparkles fallback.

v2 field consumption (gap #4):
- theme.fonts: EventFirmwareFontResolver maps Google font *family names* to
  SwiftUI fonts only when the family is registered on-device, else falls back to
  the system font (families aren't bundled today, so this resolves to system
  until a font provider ships — the resolver is future-proof).
- firmware{}: version comparison (dot-boundary prefix match so a truncated
  device version lines up with the full event build without a bare-substring
  false positive), release notes, and the update entry point. paletteColors and
  a locale-aware formattedDateRange (DateIntervalFormatter) helpers added.

Tests: 27 passing (adds version-comparison incl. the dot-boundary regression,
font-resolver availability/fallback, palette filtering, date range).
Docs: docs/user/firmware.md gains an "Event Firmware" section (+ regenerated
bundled docs).

* fix(event-firmware): address CodeRabbit review — reactive cache + no bundle prune

Two Major "quick win" findings on meshtastic#2082:

- Bundled reseed no longer purges live-cached editions. importEventEditions
  gains a pruneMissing flag: the authoritative live-API import prunes retired
  editions (true), but the bundled seed is a floor and only upserts/seeds
  (false). Previously the bundle reseed on every launch deleted any edition
  cached from a prior successful API refresh but absent from the bundled JSON,
  so a newly-announced event cached while online vanished on the next offline
  relaunch — the exact poor-connectivity-at-the-venue case the cache exists for.

- Connect's event branding is now reactive. Replaced the one-shot @State
  eventFirmware fetch (resolved only on .onAppear / firmwareEdition change) with
  a @query over EventFirmwareEntity plus a computed lookup. If the edition was
  already known before the async cache load finished, the old code returned nil
  and never re-fetched, so branding fell back to the enum name and the
  post-event nudge could silently never fire. @query re-renders as rows are
  populated/updated by the background refresh.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant