Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Meshtastic.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@
DD000011UTILENTST0000001 /* UtilityAndEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000011UTILENTST0000000 /* UtilityAndEnumTests.swift */; };
DD000012COTEXTTST0000001 /* CoTExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000012COTEXTTST0000000 /* CoTExtensionTests.swift */; };
DD000013FWAPITST00000001 /* FirmwareAndAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000013FWAPITST00000000 /* FirmwareAndAPITests.swift */; };
DD000031FWUPNPOL0000001 /* FirmwareUpdateNotificationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000031FWUPNPOL0000000 /* FirmwareUpdateNotificationPolicy.swift */; };
DD000032FWUPNTFRSRC001 /* FirmwareUpdateNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000032FWUPNTFRSRC000 /* FirmwareUpdateNotifier.swift */; };
DD000032FWUPNTFR0000001 /* FirmwareUpdateNotifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000032FWUPNTFR0000000 /* FirmwareUpdateNotifierTests.swift */; };
DD000031FWUPNTST0000001 /* FirmwareUpdateNotificationPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000031FWUPNTST0000000 /* FirmwareUpdateNotificationPolicyTests.swift */; };
DD000014RTRDLTST00000001 /* RouterDeepLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000014RTRDLTST00000000 /* RouterDeepLinkTests.swift */; };
DD000016NAVSTATST00000001 /* NavigationStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000016NAVSTATST00000000 /* NavigationStateTests.swift */; };
DD000017MTRVZTST00000001 /* MetricsVisualizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000017MTRVZTST00000000 /* MetricsVisualizationTests.swift */; };
Expand Down Expand Up @@ -889,6 +893,10 @@
DD000011UTILENTST0000000 /* UtilityAndEnumTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityAndEnumTests.swift; sourceTree = "<group>"; };
DD000012COTEXTTST0000000 /* CoTExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoTExtensionTests.swift; sourceTree = "<group>"; };
DD000013FWAPITST00000000 /* FirmwareAndAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareAndAPITests.swift; sourceTree = "<group>"; };
DD000031FWUPNPOL0000000 /* FirmwareUpdateNotificationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareUpdateNotificationPolicy.swift; sourceTree = "<group>"; };
DD000032FWUPNTFRSRC000 /* FirmwareUpdateNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareUpdateNotifier.swift; sourceTree = "<group>"; };
DD000032FWUPNTFR0000000 /* FirmwareUpdateNotifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareUpdateNotifierTests.swift; sourceTree = "<group>"; };
DD000031FWUPNTST0000000 /* FirmwareUpdateNotificationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareUpdateNotificationPolicyTests.swift; sourceTree = "<group>"; };
DD000014RTRDLTST00000000 /* RouterDeepLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterDeepLinkTests.swift; sourceTree = "<group>"; };
DD000016NAVSTATST00000000 /* NavigationStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStateTests.swift; sourceTree = "<group>"; };
DD000017MTRVZTST00000000 /* MetricsVisualizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsVisualizationTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1326,6 +1334,8 @@
children = (
23C2BE292EEAF96A00F6A997 /* FirmwareViewModel.swift */,
2315D1A42EED94E800E0FAE7 /* FirmwareFile.swift */,
DD000031FWUPNPOL0000000 /* FirmwareUpdateNotificationPolicy.swift */,
DD000032FWUPNTFRSRC000 /* FirmwareUpdateNotifier.swift */,
);
path = Firmware;
sourceTree = "<group>";
Expand Down Expand Up @@ -1626,6 +1636,8 @@
DD000011UTILENTST0000000 /* UtilityAndEnumTests.swift */,
DD000012COTEXTTST0000000 /* CoTExtensionTests.swift */,
DD000013FWAPITST00000000 /* FirmwareAndAPITests.swift */,
DD000031FWUPNTST0000000 /* FirmwareUpdateNotificationPolicyTests.swift */,
DD000032FWUPNTFR0000000 /* FirmwareUpdateNotifierTests.swift */,
DD000014RTRDLTST00000000 /* RouterDeepLinkTests.swift */,
DD000016NAVSTATST00000000 /* NavigationStateTests.swift */,
DD000017MTRVZTST00000000 /* MetricsVisualizationTests.swift */,
Expand Down Expand Up @@ -2763,6 +2775,8 @@
DD000011UTILENTST0000001 /* UtilityAndEnumTests.swift in Sources */,
DD000012COTEXTTST0000001 /* CoTExtensionTests.swift in Sources */,
DD000013FWAPITST00000001 /* FirmwareAndAPITests.swift in Sources */,
DD000031FWUPNTST0000001 /* FirmwareUpdateNotificationPolicyTests.swift in Sources */,
DD000032FWUPNTFR0000001 /* FirmwareUpdateNotifierTests.swift in Sources */,
DD000014RTRDLTST00000001 /* RouterDeepLinkTests.swift in Sources */,
DD000016NAVSTATST00000001 /* NavigationStateTests.swift in Sources */,
DD000017MTRVZTST00000001 /* MetricsVisualizationTests.swift in Sources */,
Expand Down Expand Up @@ -2904,6 +2918,8 @@
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
230A98422EF86AA9004D87F1 /* ESP32BLEOTAViewModel.swift in Sources */,
23C2BE2A2EEAF96A00F6A997 /* FirmwareViewModel.swift in Sources */,
DD000031FWUPNPOL0000001 /* FirmwareUpdateNotificationPolicy.swift in Sources */,
DD000032FWUPNTFRSRC001 /* FirmwareUpdateNotifier.swift in Sources */,
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ extension AccessoryManager {
if let activeDevice = self.activeConnection?.device, activeDevice.isManualConnection {
ManualConnectionList.shared.insert(device: activeDevice)
}

// Best-effort: the notifier bounds stale API refresh and cannot roll back a completed connect.
await FirmwareUpdateNotifier.notifyIfNeeded(accessoryManager: self)
}

// Step 8: Update UI and status to connected
Expand Down
14 changes: 14 additions & 0 deletions Meshtastic/Extensions/UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extension UserDefaults {
case testIntEnum
case lastDeviceAPIUpdate
case lastFirmwareAPIUpdate
case firmwareUpdateNotificationKeys
}

func reset() {
Expand Down Expand Up @@ -194,6 +195,19 @@ extension UserDefaults {

@UserDefault(.testIntEnum, defaultValue: .one)
static var testIntEnum: TestIntEnum

@UserDefault(.firmwareUpdateNotificationKeys, defaultValue: [])
static var firmwareUpdateNotificationKeys: [String]

static var firmwareUpdateNotificationKeySet: Set<String> {
Set(firmwareUpdateNotificationKeys)
}

static func recordFirmwareUpdateNotificationKey(_ key: String) {
var keys = firmwareUpdateNotificationKeySet
keys.insert(key)
firmwareUpdateNotificationKeys = keys.sorted()
}

static var manualConnections: [Device] {
get {
Expand Down
8 changes: 7 additions & 1 deletion Meshtastic/MeshtasticAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,13 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
let deepLink = userInfo["path"] as? String,
let url = URL(string: deepLink) {
Logger.services.info("userNotificationCenter didReceiveResponse handling deeplink: \(targetValue, privacy: .public) \(deepLink, privacy: .public)")
router?.route(url: url)
if url.scheme == "meshtastic" {
router?.route(url: url)
} else if targetValue == FirmwareUpdateNotifier.flasherTarget && url.absoluteString == FirmwareUpdateNotifier.flasherPath {
UIApplication.shared.open(url)
} else {
Logger.services.error("Unsupported notification response URL: \(deepLink, privacy: .public)")
}
} else {
Logger.services.error("Failed to handle notification response: \(userInfo, privacy: .public)")
}
Expand Down
64 changes: 64 additions & 0 deletions Meshtastic/Model/Firmware/FirmwareUpdateNotificationPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// MARK: - FirmwareUpdateNotificationPolicy

import Foundation

enum FirmwareUpdateInstallMethod: Equatable {
case appOTA
case flasher
}

enum FirmwareUpdateNotificationPolicy {
static func installMethod(architecture: String?) -> FirmwareUpdateInstallMethod {
guard let architecture = architecture.flatMap({ Architecture(rawValue: $0) }) else {
return .flasher
}

switch architecture {
case .esp32, .esp32C3, .esp32S3, .esp32C6, .nrf52840:
return .appOTA
case .rp2040:
return .flasher
}
}

static func normalizedVersion(_ version: String) -> String {
let trimmedVersion = version.trimmingCharacters(in: .whitespacesAndNewlines)
let cleanVersion = trimmedVersion.hasPrefix("v") ? String(trimmedVersion.dropFirst()) : trimmedVersion
let parts = cleanVersion.split(separator: ".")
guard parts.count >= 3 else { return cleanVersion }
return parts.prefix(3).joined(separator: ".")
}

static func isUpdateAvailable(current: String, latestStable: String) -> Bool {
let currentVersion = normalizedVersion(current)
let latestStableVersion = normalizedVersion(latestStable)
return currentVersion.compare(latestStableVersion, options: .numeric) == .orderedAscending
}

static func notificationKey(
nodeNum: Int64,
platformioTarget: String,
latestStableVersion: String
) -> String {
"firmware-update-notified:\(nodeNum):\(platformioTarget):\(normalizedVersion(latestStableVersion))"
}

static func shouldNotify(
nodeNum: Int64,
platformioTarget: String,
currentVersion: String,
latestStableVersion: String,
alreadyNotified: Set<String>
) -> Bool {
guard isUpdateAvailable(current: currentVersion, latestStable: latestStableVersion) else {
return false
}

let key = notificationKey(
nodeNum: nodeNum,
platformioTarget: platformioTarget,
latestStableVersion: latestStableVersion
)
return !alreadyNotified.contains(key)
}
}
Loading
Loading