Skip to content

feat(firmware): add verified event OTA installer - #2223

Draft
RCGV1 wants to merge 9 commits into
meshtastic:mainfrom
RCGV1:codex/event-firmware-ota
Draft

feat(firmware): add verified event OTA installer#2223
RCGV1 wants to merge 9 commits into
meshtastic:mainfrom
RCGV1:codex/event-firmware-ota

Conversation

@RCGV1

@RCGV1 RCGV1 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Status

Warning

Currently untested with real event firmware on physical hardware. This is a draft for contract, security, and UX review. Production in-app event installation remains disabled until Meshtastic provisions the API release signing key and the app embeds its corresponding trusted public key.

Stacked on and dependent on #2222. Until that PR lands, GitHub will also show its firmware-edition branding commits in this draft.

Cross-repository dependencies:

What changed?

  • Adds an event-firmware installer under Firmware Updates, including a return-to-standard-firmware path.
  • Defines a signed schema-v1 release contract covering exact pioEnv, hardware model, architecture, source-version floor, artifact version/format, byte count, SHA-256, and architecture-specific OTA compatibility.
  • Shows the exact signed artifact version before installation.
  • Requires immutable approved artifact URLs and rejects redirects, oversized downloads, size mismatches, checksum mismatches, expired contracts, unknown signing keys, and invalid signatures.
  • Re-checks the connected node and recomputes exact-target selection after download, immediately before handing the verified file to OTA.
  • Hands verified ESP32 binaries and nRF OTA ZIPs to the app’s existing firmware installers.
  • Falls back to flasher.meshtastic.org whenever the app cannot prove an exact compatible in-app path.
  • Includes a simulator-only signed fixture and mock progress flow. No simulator trust key or fixture is compiled into production behavior.

Why did it change?

Event metadata is display-oriented and cannot safely authorize firmware installation. A separate signed contract is needed so the app can select one immutable artifact for the exact connected target and verify it before entering the sensitive OTA flow. The contract also carries the standard-firmware return artifact so leaving an event edition is an explicit supported transition.

How is this tested?

  • Focused iOS Simulator run: 43 tests in 5 suites covering contract verification, exact-target selection, artifact download verification, installer handoff policy, and availability behavior.
  • Simulator mock exercises the signed-contract and verified-download handoff without contacting a radio.
  • SwiftLint, XcodeGen drift, and docs coverage CI passed.
  • Not yet tested: a real event firmware install or return-to-standard install on an ESP32 or nRF device through this UI.
  • Not yet configured: production signing key or trusted public key. The API PR fails closed until those are deliberately provisioned.

Screenshots/Videos (when applicable)

Simulator-only UX is available in this branch. Real-device screenshots and recordings should be added after the server contract and hardware test matrix are approved.

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. Documentation remains pending while the contract is under review.
  • I have tested the change to ensure that it works as intended on physical event-firmware hardware.

Copilot AI review requested due to automatic review settings July 29, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Event firmware handling now uses resilient manifest merging, HTTPS and image validation, policy-based notifications, event-specific presentation, and signed OTA contracts with verified artifact downloads. The UI exposes event branding and installation flows while preserving device targeting and cache behavior.

Changes

Event firmware metadata and presentation

Layer / File(s) Summary
Manifest decoding and cache merge
Meshtastic/API/MeshtasticAPI.swift, Meshtastic/Model/EventFirmwareEntity.swift, MeshtasticTests/EventFirmwareMetadataTests.swift
Manifest entries decode independently, refresh attempts are throttled, bundled data does not overwrite live cache data, and partial live responses preserve existing fields and editions.
Validation and event branding
Meshtastic/Model/EventFirmwareEntity.swift, Meshtastic/Model/EventFirmwarePresentation.swift, Meshtastic/Views/Connect/*, Meshtastic/Views/Helpers/MeshtasticLogo.swift
HTTPS URLs, colors, dates, palettes, contrast, and images are validated; event artwork, tinting, and event information presentation are wired into the app.
Notification policy
Meshtastic/Model/EventFirmwareNotificationPolicy.swift, Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift, Meshtastic/Views/Onboarding/DeviceOnboarding.swift
Notification defaults and user overrides are represented as policy-driven settings and persisted through new defaults.
Signed OTA installation
Meshtastic/Model/EventFirmwareOTA*.swift, Meshtastic/Model/EventFirmwareArtifactDownloader.swift, Meshtastic/Views/Settings/Firmware/*
Signed contracts are verified, compatible artifacts are selected and downloaded with size/checksum validation, and installation actions are restricted to the expected active device.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: garthvh, bruschill

Sequence Diagram(s)

sequenceDiagram
    participant FirmwareScreen
    participant AvailabilityResolver
    participant ContractVerifier
    participant ArtifactDownloader
    participant Installer

    FirmwareScreen->>AvailabilityResolver: Resolve event OTA availability
    AvailabilityResolver->>ContractVerifier: Verify signed OTA contract
    ContractVerifier-->>AvailabilityResolver: Return verified contract
    AvailabilityResolver-->>FirmwareScreen: Return compatible artifact
    FirmwareScreen->>ArtifactDownloader: Prepare artifact
    ArtifactDownloader-->>FirmwareScreen: Return verified local file
    FirmwareScreen->>Installer: Start installation for expected device
Loading

Poem

A rabbit guards the firmware cache,
With HTTPS links tucked safe and snug.
Signed OTA hops through moonlit paths,
While event colors brighten each bug.
“Install,” I whisper, “but check the node—
Then nibble tests and share the code!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately reflects the main change: a verified event OTA installer.
Description check ✅ Passed The description includes the required What/Why/How sections, screenshots, and checklist, and is specific about the OTA installer.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI review requested due to automatic review settings July 29, 2026 04:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@RCGV1

RCGV1 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI review requested due to automatic review settings July 29, 2026 04:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Meshtastic/Views/Settings/Firmware/Firmware.swift (1)

411-421: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Pass expectedNodeNum to local-file firmware install sheets.

Firmware.swift passes node.num for row installs, but the Catalyst file picker still opens NRFDFUSheet and ESP32OTAIntroSheet without the target node. When expectedNodeNum is nil, these sheets fall back to treating any connected device as valid, while row installs require the correct node.

🤖 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/Firmware/Firmware.swift` around lines 411 - 421,
Update the local-file firmware sheet switch in the showInstallationSheet flow to
pass the selected target node’s number as expectedNodeNum to NRFDFUSheet and
ESP32OTAIntroSheet, matching the row-install paths; leave UF2MassStorageView
unchanged unless it already supports the same parameter.
🧹 Nitpick comments (7)
Meshtastic/Views/Connect/EventFirmwareInfoView.swift (1)

278-288: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid byte-at-a-time accumulation for the icon download.

for try await byte in bytes runs one async-sequence iteration per byte — up to ~2M iterations for a max-size icon. Since the cap is already enforced, URLSession.data(for:) plus a length check is both simpler and far cheaper; if you want streaming, iterate bytes.chunks style buffers instead.

♻️ Simpler bounded fetch
-			let (bytes, response) = try await URLSession.shared.bytes(for: request)
+			let (data, response) = try await URLSession.shared.data(for: request)
 			guard let response = response as? HTTPURLResponse,
 				  (200..<300).contains(response.statusCode),
 				  response.expectedContentLength <= Int64(EventFirmwareImageValidator.maximumEncodedBytes),
-				  response.mimeType == "image/png" || response.mimeType == "image/jpeg" else {
+				  response.mimeType == "image/png" || response.mimeType == "image/jpeg",
+				  data.count <= EventFirmwareImageValidator.maximumEncodedBytes else {
 				return nil
 			}
-			var data = Data()
-			if response.expectedContentLength > 0 {
-				data.reserveCapacity(Int(response.expectedContentLength))
-			}
-			for try await byte in bytes {
-				guard data.count < EventFirmwareImageValidator.maximumEncodedBytes else {
-					return nil
-				}
-				data.append(byte)
-			}
 			return EventFirmwareImageValidator.image(from: data)

Note this buffers the full body before the cap check; keep the streaming form if you must abort mid-transfer, but buffer in chunks rather than per byte.

🤖 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/Connect/EventFirmwareInfoView.swift` around lines 278 - 288,
Update the icon download logic around the byte iteration to avoid per-byte async
accumulation. Prefer a bounded URLSession data fetch followed by a maximum-size
check before calling EventFirmwareImageValidator.image(from:), or, if early
streaming cancellation is required, consume buffered chunks while preserving the
existing cap enforcement and validation behavior.
Meshtastic/Model/EventFirmwareOTASelector.swift (1)

1-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing file-level header/MARK comment on new Model files. Both new files start directly with their import statements with no // MARK: FileName or copyright header, per repo convention.

  • Meshtastic/Model/EventFirmwareOTASelector.swift#L1-L2: add a // MARK: EventFirmwareOTASelector.swift (or copyright) header before import Foundation.
  • Meshtastic/Model/EventFirmwareOTAService.swift#L1-L2: add a // MARK: EventFirmwareOTAService.swift (or copyright) header before import CryptoKit.

As per coding guidelines, "Add // MARK: FileName or a file-level copyright comment at the top of files."

🤖 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/Model/EventFirmwareOTASelector.swift` around lines 1 - 2, Add a
file-level MARK or copyright header before the imports in both
EventFirmwareOTASelector.swift (lines 1-2) and EventFirmwareOTAService.swift
(lines 1-2), using each file’s name for the MARK comment.

Source: Coding guidelines

Meshtastic/Model/EventFirmwareArtifactDownloader.swift (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial

Missing file-level header comment.

This new file has no // MARK: FileName or copyright header at the top, unlike EventFirmwareNotificationPolicy.swift in the same PR. As per coding guidelines, "Add // MARK: FileName or a file-level copyright comment at the top of files."

🤖 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/Model/EventFirmwareArtifactDownloader.swift` around lines 1 - 3,
Add the project-standard file-level header at the top of
EventFirmwareArtifactDownloader.swift, using either a // MARK: FileName comment
or the same copyright-header format established by
EventFirmwareNotificationPolicy.swift. Keep it before the import statements.

Source: Coding guidelines

Meshtastic/Model/EventFirmwareOTAContract.swift (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial

Missing file-level header comment.

Same as EventFirmwareArtifactDownloader.swift — no // MARK: FileName or copyright header at the top of this new file. As per coding guidelines, "Add // MARK: FileName or a file-level copyright comment at the top of files."

🤖 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/Model/EventFirmwareOTAContract.swift` around lines 1 - 3, Add a
file-level header at the top of EventFirmwareOTAContract.swift, using either the
project’s standard // MARK: FileName format or the established copyright header
pattern from EventFirmwareArtifactDownloader.swift, before the imports.

Source: Coding guidelines

Meshtastic/Views/Settings/Firmware/EventFirmwareInstallerView.swift (2)

302-309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the underlying error before collapsing it to a generic message.

Download/checksum/signature failures are exactly the cases that need diagnostics, and the caught error is discarded. Add a typed Logger call (per the project's Logger extensions) while keeping the user-facing text generic.

🪵 Proposed fix
 			} catch {
+				Logger.services.error("Event firmware artifact preparation failed: \(error.localizedDescription, privacy: .public)")
 				await MainActor.run {
 					preparationState = .failed(
 						"The firmware package could not be downloaded and verified."
 					)

As per coding guidelines, "Use OSLog / Logger for all logging... Prefer the typed loggers defined in Meshtastic/Extensions/Logger.swift."

🤖 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/Firmware/EventFirmwareInstallerView.swift` around
lines 302 - 309, In the catch block of the firmware preparation flow, log the
caught error using the project’s typed Logger extension before updating
preparationState. Keep the existing generic user-facing failure message and
preparationTask cleanup unchanged.

Source: Coding guidelines


1-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a file header and // MARK: - separators.

This file bundles the policy enum, the main view, a DI helper, a format extension, and two simulator-only types with no section markers or top-of-file comment.

As per coding guidelines, "Use // MARK: - comments to separate logical sections within a file" and "Add // MARK: FileName or a file-level copyright comment at the top of files."

🤖 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/Firmware/EventFirmwareInstallerView.swift` around
lines 1 - 27, Add a file-level header comment at the top of
EventFirmwareInstallerView.swift, then organize the policy enum, main view,
dependency-injection helper, format extension, and simulator-only types into
logical sections using // MARK: - separators. Keep the existing declarations and
behavior unchanged.

Source: Coding guidelines

Meshtastic/Views/Settings/Firmware/ESP32 OTA/ESP32OTAIntroSheet.swift (1)

38-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Optional-expectedNodeNum fallback is reimplemented in both OTA sheets. Both views wrap EventFirmwareInstallerPolicy.isExpectedDeviceActive with an identical guard let that degrades to "any device is connected" when expectedNodeNum is nil. Move that fallback into the policy so the weaker semantics are defined once and can be tested.

  • Meshtastic/Views/Settings/Firmware/ESP32 OTA/ESP32OTAIntroSheet.swift#L38-L46: replace the computed property body with a single call to a policy overload accepting Int64?.
  • Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift#L20-L28: apply the same replacement.
🤖 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/Firmware/ESP32` OTA/ESP32OTAIntroSheet.swift around
lines 38 - 46, Move the optional expectedNodeNum fallback into
EventFirmwareInstallerPolicy by adding or using an overload accepting Int64?,
preserving “any device connected” behavior when nil. Replace the computed
property bodies in Meshtastic/Views/Settings/Firmware/ESP32
OTA/ESP32OTAIntroSheet.swift lines 38-46 and
Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift lines 20-28 with a
single policy call, leaving the non-optional policy logic centralized and
reusable.
🤖 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/API/MeshtasticAPI.swift`:
- Around line 1073-1126: Update the manifest-to-entity assignments in the
payload mapping block so every remaining plain string field only overwrites
cached values when its payload value is non-empty. Apply this to displayName,
welcomeMessage, tag, eventStart, eventEnd, timeZone, location, domain, theme
name/tagline, theme fonts, and firmware slug/version/id/title/releaseNotes,
while preserving the existing URL and color validation behavior.

In `@Meshtastic/Assets.xcassets/EventFirmwareHAMVENTION.imageset/Contents.json`:
- Around line 3-7: Remove the "scale" entry from the hamvention.png image record
in the EventFirmwareHAMVENTION asset metadata, leaving the filename and idiom
unchanged so this single-scale imageset matches EventFirmwareDEFCON and
EventFirmwareFAB.

In `@Meshtastic/Views/Settings/Firmware/EventFirmwareInstallerView.swift`:
- Around line 204-219: Update the String-returning helpers actionTitle,
actionIcon, actionFooter, and unavailableMessage so each user-facing literal is
localized with String(localized:), preserving the existing conditional and
switch behavior. Ensure the corresponding Text and Label call sites receive the
localized strings rather than raw untranslated literals.

---

Outside diff comments:
In `@Meshtastic/Views/Settings/Firmware/Firmware.swift`:
- Around line 411-421: Update the local-file firmware sheet switch in the
showInstallationSheet flow to pass the selected target node’s number as
expectedNodeNum to NRFDFUSheet and ESP32OTAIntroSheet, matching the row-install
paths; leave UF2MassStorageView unchanged unless it already supports the same
parameter.

---

Nitpick comments:
In `@Meshtastic/Model/EventFirmwareArtifactDownloader.swift`:
- Around line 1-3: Add the project-standard file-level header at the top of
EventFirmwareArtifactDownloader.swift, using either a // MARK: FileName comment
or the same copyright-header format established by
EventFirmwareNotificationPolicy.swift. Keep it before the import statements.

In `@Meshtastic/Model/EventFirmwareOTAContract.swift`:
- Around line 1-3: Add a file-level header at the top of
EventFirmwareOTAContract.swift, using either the project’s standard // MARK:
FileName format or the established copyright header pattern from
EventFirmwareArtifactDownloader.swift, before the imports.

In `@Meshtastic/Model/EventFirmwareOTASelector.swift`:
- Around line 1-2: Add a file-level MARK or copyright header before the imports
in both EventFirmwareOTASelector.swift (lines 1-2) and
EventFirmwareOTAService.swift (lines 1-2), using each file’s name for the MARK
comment.

In `@Meshtastic/Views/Connect/EventFirmwareInfoView.swift`:
- Around line 278-288: Update the icon download logic around the byte iteration
to avoid per-byte async accumulation. Prefer a bounded URLSession data fetch
followed by a maximum-size check before calling
EventFirmwareImageValidator.image(from:), or, if early streaming cancellation is
required, consume buffered chunks while preserving the existing cap enforcement
and validation behavior.

In `@Meshtastic/Views/Settings/Firmware/ESP32` OTA/ESP32OTAIntroSheet.swift:
- Around line 38-46: Move the optional expectedNodeNum fallback into
EventFirmwareInstallerPolicy by adding or using an overload accepting Int64?,
preserving “any device connected” behavior when nil. Replace the computed
property bodies in Meshtastic/Views/Settings/Firmware/ESP32
OTA/ESP32OTAIntroSheet.swift lines 38-46 and
Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift lines 20-28 with a
single policy call, leaving the non-optional policy logic centralized and
reusable.

In `@Meshtastic/Views/Settings/Firmware/EventFirmwareInstallerView.swift`:
- Around line 302-309: In the catch block of the firmware preparation flow, log
the caught error using the project’s typed Logger extension before updating
preparationState. Keep the existing generic user-facing failure message and
preparationTask cleanup unchanged.
- Around line 1-27: Add a file-level header comment at the top of
EventFirmwareInstallerView.swift, then organize the policy enum, main view,
dependency-injection helper, format extension, and simulator-only types into
logical sections using // MARK: - separators. Keep the existing declarations and
behavior unchanged.
🪄 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: 7a32c34d-3e5c-4dd0-93a2-d1a1c1de68a7

📥 Commits

Reviewing files that changed from the base of the PR and between de26cc4 and 181e38e.

⛔ Files ignored due to path filters (3)
  • Meshtastic/Assets.xcassets/EventFirmwareDEFCON.imageset/defcon34.png is excluded by !**/*.png
  • Meshtastic/Assets.xcassets/EventFirmwareFAB.imageset/fab26.png is excluded by !**/*.png
  • Meshtastic/Assets.xcassets/EventFirmwareHAMVENTION.imageset/hamvention.png is excluded by !**/*.png
📒 Files selected for processing (81)
  • Meshtastic/API/MeshtasticAPI.swift
  • Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift
  • Meshtastic/Assets.xcassets/EventFirmwareDEFCON.imageset/Contents.json
  • Meshtastic/Assets.xcassets/EventFirmwareFAB.imageset/Contents.json
  • Meshtastic/Assets.xcassets/EventFirmwareHAMVENTION.imageset/Contents.json
  • Meshtastic/Enums/FirmwareEditionEnum.swift
  • Meshtastic/Extensions/UserDefaults.swift
  • Meshtastic/MeshtasticApp.swift
  • Meshtastic/Model/EventFirmwareArtifactDownloader.swift
  • Meshtastic/Model/EventFirmwareEntity.swift
  • Meshtastic/Model/EventFirmwareNotificationPolicy.swift
  • Meshtastic/Model/EventFirmwareOTAContract.swift
  • Meshtastic/Model/EventFirmwareOTASelector.swift
  • Meshtastic/Model/EventFirmwareOTAService.swift
  • Meshtastic/Model/EventFirmwarePresentation.swift
  • Meshtastic/Persistence/MarketingCapture.swift
  • Meshtastic/Persistence/Persistence.swift
  • Meshtastic/Persistence/UpdateSwiftData.swift
  • Meshtastic/Resources/docs/developer/architecture.html
  • Meshtastic/Resources/docs/developer/swiftdata.html
  • Meshtastic/Resources/docs/index.json
  • Meshtastic/Resources/docs/markdown/developer/architecture.md
  • Meshtastic/Resources/docs/markdown/developer/swiftdata.md
  • Meshtastic/Resources/docs/markdown/user/firmware.md
  • Meshtastic/Resources/docs/user/firmware.html
  • Meshtastic/Resources/event_firmware.json
  • Meshtastic/Views/Connect/Connect.swift
  • Meshtastic/Views/Connect/EventFirmwareInfoView.swift
  • Meshtastic/Views/ContentView.swift
  • Meshtastic/Views/Helpers/Compact Widgets/ParticulateMatterCompactWidget.swift
  • Meshtastic/Views/Helpers/Compact Widgets/RadiationCompactWidget.swift
  • Meshtastic/Views/Helpers/MeshtasticLogo.swift
  • Meshtastic/Views/Nodes/Helpers/Map/MapSettingsForm.swift
  • Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift
  • Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift
  • Meshtastic/Views/Onboarding/DeviceOnboarding.swift
  • Meshtastic/Views/Provisioning/WifiProvisioningView.swift
  • Meshtastic/Views/Settings/AppData.swift
  • Meshtastic/Views/Settings/AppSettings.swift
  • Meshtastic/Views/Settings/Channels/ChannelForm.swift
  • Meshtastic/Views/Settings/Config/BluetoothConfig.swift
  • Meshtastic/Views/Settings/Config/DeviceConfig.swift
  • Meshtastic/Views/Settings/Config/DisplayConfig.swift
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift
  • Meshtastic/Views/Settings/Config/Module/AudioConfig.swift
  • Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift
  • Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift
  • Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift
  • Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift
  • Meshtastic/Views/Settings/Config/Module/MeshBeaconConfig.swift
  • Meshtastic/Views/Settings/Config/Module/NeighborInfoConfig.swift
  • Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift
  • Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift
  • Meshtastic/Views/Settings/Config/Module/SerialConfig.swift
  • Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift
  • Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift
  • Meshtastic/Views/Settings/Config/Module/TrafficManagementConfig.swift
  • Meshtastic/Views/Settings/Config/NetworkConfig.swift
  • Meshtastic/Views/Settings/Config/PositionConfig.swift
  • Meshtastic/Views/Settings/Config/PowerConfig.swift
  • Meshtastic/Views/Settings/Config/SecurityConfig.swift
  • Meshtastic/Views/Settings/Discovery/DiscoveryScanView.swift
  • Meshtastic/Views/Settings/Firmware/ESP32 OTA/ESP32OTAIntroSheet.swift
  • Meshtastic/Views/Settings/Firmware/EventFirmwareInstallerView.swift
  • Meshtastic/Views/Settings/Firmware/Firmware.swift
  • Meshtastic/Views/Settings/Firmware/NRF DFU/NRFDFUSheet.swift
  • Meshtastic/Views/Settings/Routes.swift
  • Meshtastic/Views/Settings/ShareChannels.swift
  • Meshtastic/Views/Settings/TAKServerConfig.swift
  • Meshtastic/Views/Settings/UserConfig.swift
  • MeshtasticTests/EventFirmwareArtifactDownloaderTests.swift
  • MeshtasticTests/EventFirmwareInstallerViewTests.swift
  • MeshtasticTests/EventFirmwareMetadataTests.swift
  • MeshtasticTests/EventFirmwareNotificationTests.swift
  • MeshtasticTests/EventFirmwareOTAContractTests.swift
  • MeshtasticTests/EventFirmwareOTASelectorTests.swift
  • MeshtasticTests/EventFirmwareOTAServiceTests.swift
  • docs/developer/architecture.md
  • docs/developer/swiftdata.md
  • docs/user/firmware.md
💤 Files with no reviewable changes (26)
  • Meshtastic/Views/Helpers/Compact Widgets/ParticulateMatterCompactWidget.swift
  • Meshtastic/Views/Settings/Config/Module/NeighborInfoConfig.swift
  • Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift
  • Meshtastic/Views/Settings/AppData.swift
  • Meshtastic/Views/Settings/Config/Module/AudioConfig.swift
  • Meshtastic/Views/Settings/ShareChannels.swift
  • Meshtastic/Views/Settings/Config/Module/MeshBeaconConfig.swift
  • Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift
  • Meshtastic/Views/Helpers/Compact Widgets/RadiationCompactWidget.swift
  • Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift
  • Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift
  • Meshtastic/Views/Settings/Config/SecurityConfig.swift
  • Meshtastic/Views/Settings/Config/DisplayConfig.swift
  • Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift
  • Meshtastic/Views/Provisioning/WifiProvisioningView.swift
  • Meshtastic/Views/Settings/Config/Module/SerialConfig.swift
  • Meshtastic/Views/Settings/Config/Module/TrafficManagementConfig.swift
  • Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift
  • Meshtastic/Views/Settings/Discovery/DiscoveryScanView.swift
  • Meshtastic/Views/Settings/Config/NetworkConfig.swift
  • Meshtastic/Views/Settings/Config/LoRaConfig.swift
  • Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift
  • Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift
  • Meshtastic/Views/Settings/Config/PositionConfig.swift
  • Meshtastic/Views/Settings/AppSettings.swift
  • Meshtastic/Views/Settings/Config/PowerConfig.swift

Comment on lines +1073 to +1126
if let value = payload.displayName { entity.displayName = value }
if let value = payload.welcomeMessage { entity.welcomeMessage = value }
if let value = payload.tag { entity.tag = value }
if let value = payload.eventStart { entity.eventStart = value }
if let value = payload.eventEnd { entity.eventEnd = value }
if let value = payload.timeZone { entity.timeZone = value }
if let value = payload.location { entity.location = value }
if let value = EventFirmwareURLPolicy.httpsURL(from: payload.iconUrl)?.absoluteString {
entity.iconUrl = value
}
if let value = payload.accentColor,
EventFirmwareEntity.color(fromHex: value) != nil {
entity.accentColor = value
}
if let value = payload.domain { entity.domain = value }
if let links = payload.links {
let safeLinks = links.map {
EventFirmwareEntity.Link(label: $0.label, url: $0.url)
}.filter {
EventFirmwareURLPolicy.httpsURL(from: $0.url) != nil
}
if !safeLinks.isEmpty {
entity.setLinks(safeLinks)
}
}
if let value = payload.theme?.name { entity.themeName = value }
if let value = payload.theme?.tagline { entity.themeTagline = value }
if let value = payload.theme?.colors?.primary,
EventFirmwareEntity.color(fromHex: value) != nil {
entity.themePrimaryColor = value
}
if let value = payload.theme?.colors?.secondary,
EventFirmwareEntity.color(fromHex: value) != nil {
entity.themeSecondaryColor = value
}
if let value = payload.theme?.colors?.accent,
EventFirmwareEntity.color(fromHex: value) != nil {
entity.themeAccentColor = value
}
if let palette = payload.theme?.palette {
let validPalette = palette.filter {
EventFirmwareEntity.color(fromHex: $0) != nil
}
if !validPalette.isEmpty {
entity.themePalette = validPalette
}
}
if let value = payload.theme?.fonts?.heading { entity.themeFontHeading = value }
if let value = payload.theme?.fonts?.body { entity.themeFontBody = value }
if let value = payload.firmware?.slug { entity.firmwareSlug = value }
if let value = payload.firmware?.version { entity.firmwareVersion = value }
if let value = payload.firmware?.id { entity.firmwareId = value }
if let value = payload.firmware?.title { entity.firmwareTitle = value }
if let value = payload.firmware?.releaseNotes { entity.firmwareReleaseNotes = value }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Empty strings from the manifest still overwrite cached copy.

if let value = payload.displayName treats "" as a supplied value, so an edition entry with blank strings blanks out good cached branding. The URL/color fields already validate; the plain string fields don't.

🛠️ Suggested guard
-				if let value = payload.displayName { entity.displayName = value }
-				if let value = payload.welcomeMessage { entity.welcomeMessage = value }
+				if let value = payload.displayName, !value.isEmpty { entity.displayName = value }
+				if let value = payload.welcomeMessage, !value.isEmpty { entity.welcomeMessage = value }

(apply the same non-empty check to the remaining plain string fields)

🤖 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/API/MeshtasticAPI.swift` around lines 1073 - 1126, Update the
manifest-to-entity assignments in the payload mapping block so every remaining
plain string field only overwrites cached values when its payload value is
non-empty. Apply this to displayName, welcomeMessage, tag, eventStart, eventEnd,
timeZone, location, domain, theme name/tagline, theme fonts, and firmware
slug/version/id/title/releaseNotes, while preserving the existing URL and color
validation behavior.

Comment on lines +3 to +7
{
"filename" : "hamvention.png",
"idiom" : "universal",
"scale" : "1x"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Drop the 1x scale to match the other event imagesets.

EventFirmwareDEFCON and EventFirmwareFAB use scale-less (single-scale) entries; pinning this one to 1x makes the artwork upscale on 2x/3x displays.

🎨 Proposed fix
     {
       "filename" : "hamvention.png",
-      "idiom" : "universal",
-      "scale" : "1x"
+      "idiom" : "universal"
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"filename" : "hamvention.png",
"idiom" : "universal",
"scale" : "1x"
}
{
"filename" : "hamvention.png",
"idiom" : "universal"
}
🤖 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/Assets.xcassets/EventFirmwareHAMVENTION.imageset/Contents.json`
around lines 3 - 7, Remove the "scale" entry from the hamvention.png image
record in the EventFirmwareHAMVENTION asset metadata, leaving the filename and
idiom unchanged so this single-scale imageset matches EventFirmwareDEFCON and
EventFirmwareFAB.

Comment on lines +204 to +219
private var actionTitle: String {
installPurpose == .event ? "Install Event Firmware" : "Return to Standard Firmware"
}

private var actionIcon: String {
installPurpose == .event ? "calendar.badge.checkmark" : "arrow.uturn.backward.circle"
}

private var actionFooter: String {
switch availability {
case .available:
return "The download is verified before the existing firmware installer is opened. Keep the app open and your device nearby during installation."
case .unavailable:
return "This app cannot verify a compatible in-app package for this exact device. The web flasher provides the supported installation path."
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

These String-returning helpers bypass localization.

Text(actionFooter), Label(actionTitle, ...) and Label(unavailableMessage(for:), ...) take a String, not a LocalizedStringKey, so these strings ship untranslated while the adjacent inline literals (Lines 111–112) are localized. Wrap each literal in String(localized:) as done elsewhere in the codebase.

🌐 Proposed fix (pattern applies to all three helpers)
 	private var actionTitle: String {
-		installPurpose == .event ? "Install Event Firmware" : "Return to Standard Firmware"
+		installPurpose == .event
+			? String(localized: "Install Event Firmware")
+			: String(localized: "Return to Standard Firmware")
 	}
 		case .contractUnavailable:
-			return "No trusted installation contract is currently published for this event."
+			return String(localized: "No trusted installation contract is currently published for this event.")

Also applies to: 234-253

🤖 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/Firmware/EventFirmwareInstallerView.swift` around
lines 204 - 219, Update the String-returning helpers actionTitle, actionIcon,
actionFooter, and unavailableMessage so each user-facing literal is localized
with String(localized:), preserving the existing conditional and switch
behavior. Ensure the corresponding Text and Label call sites receive the
localized strings rather than raw untranslated literals.

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.

2 participants