From dffd54004540c4e90b6e6624935395a0d43d8693 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 7 Apr 2026 08:58:24 -0500 Subject: [PATCH 1/3] Add TAK Config settings screen --- Localizable.xcstrings | 20 + Meshtastic.xcodeproj/project.pbxproj | 8 +- .../AccessoryManager+ToRadio.swift | 56 ++ Meshtastic/Helpers/MeshPackets.swift | 4 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 519 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 47 ++ Meshtastic/Router/NavigationState.swift | 1 + Meshtastic/Views/Settings/Settings.swift | 16 +- .../Views/Settings/TAKServerConfig.swift | 234 ++++++++ 10 files changed, 895 insertions(+), 12 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 57.xcdatamodel/contents diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a21426e4..dde69cfe 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -26820,6 +26820,10 @@ } } }, + "Identity" : { + "comment" : "A section", + "isCommentAutoGenerated" : true + }, "If DOP is set, use HDOP / VDOP values instead of PDOP" : { "localizations" : { "da" : { @@ -52839,6 +52843,10 @@ "comment" : "A label for the TAK channel index.", "isCommentAutoGenerated" : true }, + "TAK Config" : { + "comment" : "The title of the TAK module configuration screen.", + "isCommentAutoGenerated" : true + }, "TAK Server" : { }, @@ -53079,6 +53087,10 @@ } } }, + "Team" : { + "comment" : "A label for the team picker.", + "isCommentAutoGenerated" : true + }, "Telemetry" : { "localizations" : { "da" : { @@ -54794,6 +54806,9 @@ } } } + }, + "These settings only apply when the device role is TAK or TAK Tracker." : { + }, "These settings will %@" : { "comment" : "A paragraph below the title that explains what the user is about to do.", @@ -54854,6 +54869,10 @@ } } }, + "These values are included in TAK position reports. Leave either setting at Default to let firmware use Cyan and Team Member." : { + "comment" : "A", + "isCommentAutoGenerated" : true + }, "Thirty Minutes" : { "localizations" : { "da" : { @@ -55299,6 +55318,7 @@ } }, "This node does not support any configurable modules." : { + "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4a83769b..5ff2936e 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -101,7 +101,6 @@ 8E587743574CE17703E892C6 /* Certificates in Resources */ = {isa = PBXBuildFile; fileRef = 518D504DED9874EBF9D76578 /* Certificates */; }; 8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; }; 9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; }; - DCC919C6B47C15BB0795456C /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AD8037A0D583C614B0597 /* Tools.swift */; }; A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; }; AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; }; ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; }; @@ -135,6 +134,7 @@ D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; }; D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; }; D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; }; + DCC919C6B47C15BB0795456C /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AD8037A0D583C614B0597 /* Tools.swift */; }; DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; }; DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; DD09240001E7FAD600E70001 /* MapLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09240002E7FAD600E70001 /* MapLegend.swift */; }; @@ -349,11 +349,11 @@ /* Begin PBXFileReference section */ 01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = ""; }; - 1D5AD8037A0D583C614B0597 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = ""; }; 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = ""; }; 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; + 1D5AD8037A0D583C614B0597 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; 230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Discovery.swift"; sourceTree = ""; }; 231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEAuthorizationHelper.swift; sourceTree = ""; }; 231A53772E69ADB900216B99 /* NodeFilterParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeFilterParameters.swift; sourceTree = ""; }; @@ -437,6 +437,7 @@ 8D3F8A402D44C2A6009EAAA4 /* PowerMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMetricsLog.swift; sourceTree = ""; }; 9155703C39B55FC9DDF3E4C1 /* TAKDataPackageGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKDataPackageGenerator.swift; sourceTree = ""; }; AA00010022E2730EC0060000 /* ConnectViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewTests.swift; sourceTree = ""; }; + AA0001022F07A4B000600001 /* MeshtasticDataModelV 57.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 57.xcdatamodel"; sourceTree = ""; }; ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconButton.swift; sourceTree = ""; }; ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPicker.swift; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; @@ -2439,6 +2440,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + AA0001022F07A4B000600001 /* MeshtasticDataModelV 57.xcdatamodel */, DD7D46262F833B520028AC1A /* MeshtasticDataModelV 56.xcdatamodel */, DD04804A2E9295A5005F946C /* MeshtasticDataModelV 55.xcdatamodel */, DDDF34392E2CB8E600356DC3 /* MeshtasticDataModelV 54.xcdatamodel */, @@ -2496,7 +2498,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD7D46262F833B520028AC1A /* MeshtasticDataModelV 56.xcdatamodel */; + currentVersion = AA0001022F07A4B000600001 /* MeshtasticDataModelV 57.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index f34b5ae4..4fa73d2d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -1820,6 +1820,32 @@ extension AccessoryManager { try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) } + public func requestTAKModuleConfig(fromUser: UserEntity, toUser: UserEntity) async throws { + + var adminPacket = AdminMessage() + adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.takConfig + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { + + var adminPacket = AdminMessage() + adminPacket.setModuleConfig.tak = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } + var meshPacket: MeshPacket = MeshPacket() + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. _XCCurrentVersionName - MeshtasticDataModelV 56.xcdatamodel + MeshtasticDataModelV 57.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 57.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 57.xcdatamodel/contents new file mode 100644 index 00000000..cd39db2a --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 57.xcdatamodel/contents @@ -0,0 +1,519 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 32d9bc99..5a141ed3 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1791,4 +1791,51 @@ extension MeshPackets { Logger.data.error("💥 [TelemetryConfigEntity] Fetching node for core data TelemetryConfigEntity failed: \(nsError, privacy: .public)") } } + + func upsertTAKModuleConfigPacket(config: ModuleConfig.TAKConfig, nodeNum: Int64, sessionPasskey: Data? = Data()) async { + let context = self.backgroundContext + await context.perform { + self.upsertTAKModuleConfigPacket(config: config, nodeNum: nodeNum, sessionPasskey: sessionPasskey, context: context) + } + } + + nonisolated func upsertTAKModuleConfigPacket(config: ModuleConfig.TAKConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("TAK module config received: %@".localized, String(nodeNum)) + Logger.data.info("🎯 \(logString, privacy: .public)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + if !fetchedNode.isEmpty { + if fetchedNode[0].takConfig == nil { + let newTAKConfig = TAKConfigEntity(context: context) + newTAKConfig.team = Int32(config.team.rawValue) + newTAKConfig.role = Int32(config.role.rawValue) + fetchedNode[0].takConfig = newTAKConfig + } else { + fetchedNode[0].takConfig?.team = Int32(config.team.rawValue) + fetchedNode[0].takConfig?.role = Int32(config.role.rawValue) + } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("💾 [TAKConfigEntity] Updated TAK Module Config for node: \(nodeNum.toHex(), privacy: .public)") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [TAKConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("💥 [TAKConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save TAK Module Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("💥 [TAKConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } + } } diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index 173a2c4e..c087f478 100644 --- a/Meshtastic/Router/NavigationState.swift +++ b/Meshtastic/Router/NavigationState.swift @@ -53,6 +53,7 @@ enum SettingsNavigationState: String { case appFiles case firmwareUpdates case tak + case takConfig case tools } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index d4fe2712..26a2d247 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -276,14 +276,12 @@ struct Settings: View { } } - // Update this list with the modules that are shown above. If all are not supported - // Then show a message. - if !isAnySupported([.ambientlightingConfig, .cannedmsgConfig, - .detectionsensorConfig, .extnotifConfig, - .mqttConfig, .rangetestConfig, .paxcounterConfig, - .audioConfig, .serialConfig, .storeforwardConfig, - .telemetryConfig]) { - Text("This node does not support any configurable modules.") + NavigationLink(value: SettingsNavigationState.takConfig) { + Label { + Text("TAK") + } icon: { + Image(systemName: "shield.checkered") + } } } header: { Text("Module Configuration") @@ -545,6 +543,8 @@ struct Settings: View { Tools() case .tak: TAKServerConfig() + case .takConfig: + TAKModuleConfig(node: nodes.first(where: { $0.num == selectedNode })) } } .onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index 7e8b6502..3f615f04 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -9,6 +9,7 @@ import SwiftUI import UniformTypeIdentifiers import OSLog import CoreData +import MeshtasticProtobufs enum CertificateImportType { case p12 @@ -563,3 +564,236 @@ struct ZipDocument: FileDocument { FileWrapper(regularFileWithContents: data) } } + +struct TAKModuleConfig: View { + @Environment(\.managedObjectContext) private var context + @EnvironmentObject private var accessoryManager: AccessoryManager + @Environment(\.dismiss) private var goBack + + let node: NodeInfoEntity? + + @State private var hasChanges = false + @State private var team = Team.unspecifedColor.rawValue + @State private var role = MemberRole.unspecifed.rawValue + + private var selectedTeam: Team { + Team(rawValue: team) ?? .unspecifedColor + } + + private var selectedRole: MemberRole { + MemberRole(rawValue: role) ?? .unspecifed + } + + private var deviceRole: DeviceRoles? { + guard let role = node?.deviceConfig?.role else { return nil } + return DeviceRoles(rawValue: Int(role)) + } + + var body: some View { + Form { + ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues) + + if let deviceRole, deviceRole != .tak && deviceRole != .takTracker { + Section { + Text("These settings only apply when the device role is TAK or TAK Tracker.") + .font(.callout) + .foregroundColor(.orange) + } + } + + Section(header: Text("Identity")) { + VStack(alignment: .leading) { + Picker("Team", selection: $team) { + ForEach(Team.allCases, id: \.rawValue) { teamOption in + Text(teamTitle(teamOption)).tag(teamOption.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + Text(teamHelpText(selectedTeam)) + .foregroundColor(.gray) + .font(.callout) + } + + VStack(alignment: .leading) { + Picker("Role", selection: $role) { + ForEach(MemberRole.allCases, id: \.rawValue) { roleOption in + Text(roleTitle(roleOption)).tag(roleOption.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + Text(roleHelpText(selectedRole)) + .foregroundColor(.gray) + .font(.callout) + } + } + + Section { + Text("These values are included in TAK position reports. Leave either setting at Default to let firmware use Cyan and Team Member.") + .foregroundColor(.gray) + .font(.callout) + } + } + .disabled(!accessoryManager.isConnected || node?.takConfig == nil) + .safeAreaInset(edge: .bottom, alignment: .center) { + HStack(spacing: 0) { + SaveConfigButton(node: node, hasChanges: $hasChanges) { + guard let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? -1, context: context), + let fromUser = connectedNode.user, + let toUser = node?.user else { + return + } + + var config = ModuleConfig.TAKConfig() + config.team = selectedTeam + config.role = selectedRole + + Task { + _ = try await accessoryManager.saveTAKModuleConfig(config: config, fromUser: fromUser, toUser: toUser) + Task { @MainActor in + hasChanges = false + goBack() + } + } + } + } + } + .navigationTitle("TAK Config") + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + deviceConnected: accessoryManager.isConnected, + name: accessoryManager.activeConnection?.device.shortName ?? "?" + ) + } + ) + .onFirstAppear { + if let deviceNum = accessoryManager.activeDeviceNum, let node { + let connectedNode = getNodeInfo(id: deviceNum, context: context) + if let connectedNode, node.num != deviceNum { + if UserDefaults.enableAdministration { + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.takConfig == nil { + Task { + do { + Logger.mesh.info("⚙️ Empty or expired TAK module config requesting via PKI admin") + try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) + } catch { + Logger.mesh.info("🚨 TAK module config request failed: \(error.localizedDescription)") + } + } + } + } else { + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") + } + } + } + } + .onChange(of: team) { _, newTeam in + if newTeam != Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) { + hasChanges = true + } + } + .onChange(of: role) { _, newRole in + if newRole != Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) { + hasChanges = true + } + } + } + + private func setTAKValues() { + team = Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) + role = Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) + hasChanges = false + } + + private func teamTitle(_ team: Team) -> String { + switch team { + case .unspecifedColor: + return "Default (Cyan)" + case .white: + return "White" + case .yellow: + return "Yellow" + case .orange: + return "Orange" + case .magenta: + return "Magenta" + case .red: + return "Red" + case .maroon: + return "Maroon" + case .purple: + return "Purple" + case .darkBlue: + return "Dark Blue" + case .blue: + return "Blue" + case .cyan: + return "Cyan" + case .teal: + return "Teal" + case .green: + return "Green" + case .darkGreen: + return "Dark Green" + case .brown: + return "Brown" + case .UNRECOGNIZED: + return "Unknown" + } + } + + private func roleTitle(_ role: MemberRole) -> String { + switch role { + case .unspecifed: + return "Default (Team Member)" + case .teamMember: + return "Team Member" + case .teamLead: + return "Team Lead" + case .hq: + return "HQ" + case .sniper: + return "Sniper" + case .medic: + return "Medic" + case .forwardObserver: + return "Forward Observer" + case .rto: + return "RTO" + case .k9: + return "K9" + case .UNRECOGNIZED: + return "Unknown" + } + } + + private func teamHelpText(_ team: Team) -> String { + switch team { + case .unspecifedColor: + return "Default uses Cyan." + case .UNRECOGNIZED: + return "Unknown team color." + default: + return "Shown to TAK clients as the \(teamTitle(team)) team color." + } + } + + private func roleHelpText(_ role: MemberRole) -> String { + switch role { + case .unspecifed: + return "Default uses Team Member." + case .UNRECOGNIZED: + return "Unknown TAK role." + default: + return "Shown to TAK clients as the \(roleTitle(role)) role." + } + } +} + +#Preview { + let context = PersistenceController.preview.container.viewContext + return TAKModuleConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} From 7964a6bab324269a7c57841337d3e61c509b5df9 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 7 Apr 2026 13:51:06 -0500 Subject: [PATCH 2/3] Moved some things around --- Localizable.xcstrings | 1 - Meshtastic.xcodeproj/project.pbxproj | 4 + .../AccessoryManager+ToRadio.swift | 4 +- .../Config/Module/TAKModuleConfig.swift | 264 ++++++++++++++++++ Meshtastic/Views/Settings/Settings.swift | 53 +++- .../Views/Settings/TAKServerConfig.swift | 234 ---------------- 6 files changed, 318 insertions(+), 242 deletions(-) create mode 100644 Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index dde69cfe..27047cb7 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -55318,7 +55318,6 @@ } }, "This node does not support any configurable modules." : { - "extractionState" : "stale", "localizations" : { "da" : { "stringUnit" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 5ff2936e..815a8db1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -102,6 +102,7 @@ 8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; }; 9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; }; A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; }; + AA0001032F07A4B000600001 /* TAKModuleConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042F07A4B000600001 /* TAKModuleConfig.swift */; }; AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; }; ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; }; ABB99DEB2E2EA1C500CFBD05 /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */; }; @@ -351,6 +352,7 @@ 01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = ""; }; 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = ""; }; 09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = ""; }; + AA0001042F07A4B000600001 /* TAKModuleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TAKModuleConfig.swift; sourceTree = ""; }; 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; 1D5AD8037A0D583C614B0597 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; @@ -1085,6 +1087,7 @@ DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */, DD6193782863875F00E59241 /* SerialConfig.swift */, DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */, + AA0001042F07A4B000600001 /* TAKModuleConfig.swift */, DD415827285859C4009B0E59 /* TelemetryConfig.swift */, ); path = Module; @@ -1946,6 +1949,7 @@ E3ED80145D0E873011982556 /* TAKServerManager.swift in Sources */, FE508F9AF5AD5DA20AA64DBF /* AccessoryManager+TAK.swift in Sources */, A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */, + AA0001032F07A4B000600001 /* TAKModuleConfig.swift in Sources */, DCC919C6B47C15BB0795456C /* Tools.swift in Sources */, 8398407DBA32EE7CFC16A385 /* TAKDataPackageGenerator.swift in Sources */, 655AF7816E76D5F310DF87A6 /* FountainCodec.swift in Sources */, diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 4fa73d2d..29870d16 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -1824,7 +1824,9 @@ extension AccessoryManager { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.takConfig - adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) diff --git a/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift b/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift new file mode 100644 index 00000000..56803bb8 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift @@ -0,0 +1,264 @@ +// +// TAKModuleConfig.swift +// Meshtastic +import SwiftUI +import CoreData +import OSLog +import MeshtasticProtobufs + +struct TAKModuleConfig: View { + @Environment(\.managedObjectContext) private var context + @EnvironmentObject private var accessoryManager: AccessoryManager + @Environment(\.dismiss) private var goBack + + let node: NodeInfoEntity? + + @State private var hasChanges = false + @State private var team = Team.unspecifedColor.rawValue + @State private var role = MemberRole.unspecifed.rawValue + + private var selectedTeam: Team { + Team(rawValue: team) ?? .unspecifedColor + } + + private var selectedRole: MemberRole { + MemberRole(rawValue: role) ?? .unspecifed + } + + private var deviceRole: DeviceRoles? { + guard let role = node?.deviceConfig?.role ?? node?.user?.role else { return nil } + return DeviceRoles(rawValue: Int(role)) + } + + private var isConnectedNode: Bool { + guard let node else { return false } + return node.num == accessoryManager.activeDeviceNum + } + + var body: some View { + Form { + ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues) + + if let deviceRole, deviceRole != .tak && deviceRole != .takTracker { + Section { + Text("These settings only apply when the device role is TAK or TAK Tracker.") + .font(.callout) + .foregroundColor(.orange) + } + } + + Section(header: Text("Identity")) { + VStack(alignment: .leading) { + Picker("Team", selection: $team) { + ForEach(Team.allCases, id: \.rawValue) { teamOption in + Text(teamTitle(teamOption)).tag(teamOption.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + Text(teamHelpText(selectedTeam)) + .foregroundColor(.gray) + .font(.callout) + } + + VStack(alignment: .leading) { + Picker("Role", selection: $role) { + ForEach(MemberRole.allCases, id: \.rawValue) { roleOption in + Text(roleTitle(roleOption)).tag(roleOption.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + Text(roleHelpText(selectedRole)) + .foregroundColor(.gray) + .font(.callout) + } + } + + Section { + Text("These values are included in TAK position reports. Leave either setting at Default to let firmware use Cyan and Team Member.") + .foregroundColor(.gray) + .font(.callout) + } + } + .disabled(!accessoryManager.isConnected || (!isConnectedNode && node?.takConfig == nil)) + .safeAreaInset(edge: .bottom, alignment: .center) { + HStack(spacing: 0) { + SaveConfigButton(node: node, hasChanges: $hasChanges) { + guard let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? -1, context: context), + let fromUser = connectedNode.user, + let toUser = node?.user else { + return + } + + var config = ModuleConfig.TAKConfig() + config.team = selectedTeam + config.role = selectedRole + + Task { + _ = try await accessoryManager.saveTAKModuleConfig(config: config, fromUser: fromUser, toUser: toUser) + Task { @MainActor in + hasChanges = false + goBack() + } + } + } + } + } + .navigationTitle("TAK Config") + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + deviceConnected: accessoryManager.isConnected, + name: accessoryManager.activeConnection?.device.shortName ?? "?" + ) + } + ) + .onAppear { + // Need to request a TAKModuleConfig from the connected node before allowing changes. + if let deviceNum = accessoryManager.activeDeviceNum, + let node, + node.num == deviceNum, + node.takConfig == nil { + let connectedNode = getNodeInfo(id: deviceNum, context: context) + if let connectedNode { + Task { + do { + Logger.mesh.info("⚙️ Empty TAK module config requesting from connected node") + try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) + } catch { + Logger.mesh.error("🚨 TAK module config request failed: \(error.localizedDescription)") + } + } + } + } + } + .onFirstAppear { + if let deviceNum = accessoryManager.activeDeviceNum, let node { + let connectedNode = getNodeInfo(id: deviceNum, context: context) + if let connectedNode, node.num != deviceNum { + if UserDefaults.enableAdministration { + let expiration = node.sessionExpiration ?? Date() + if expiration < Date() || node.takConfig == nil { + Task { + do { + Logger.mesh.info("⚙️ Empty or expired TAK module config requesting via PKI admin") + try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) + } catch { + Logger.mesh.info("🚨 TAK module config request failed: \(error.localizedDescription)") + } + } + } + } else { + Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") + } + } + } + } + .onChange(of: team) { _, newTeam in + if newTeam != Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) { + hasChanges = true + } + } + .onChange(of: role) { _, newRole in + if newRole != Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) { + hasChanges = true + } + } + } + + private func setTAKValues() { + team = Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) + role = Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) + hasChanges = false + } + + private func teamTitle(_ team: Team) -> String { + switch team { + case .unspecifedColor: + return "Default (Cyan)" + case .white: + return "White" + case .yellow: + return "Yellow" + case .orange: + return "Orange" + case .magenta: + return "Magenta" + case .red: + return "Red" + case .maroon: + return "Maroon" + case .purple: + return "Purple" + case .darkBlue: + return "Dark Blue" + case .blue: + return "Blue" + case .cyan: + return "Cyan" + case .teal: + return "Teal" + case .green: + return "Green" + case .darkGreen: + return "Dark Green" + case .brown: + return "Brown" + case .UNRECOGNIZED: + return "Unknown" + } + } + + private func roleTitle(_ role: MemberRole) -> String { + switch role { + case .unspecifed: + return "Default (Team Member)" + case .teamMember: + return "Team Member" + case .teamLead: + return "Team Lead" + case .hq: + return "HQ" + case .sniper: + return "Sniper" + case .medic: + return "Medic" + case .forwardObserver: + return "Forward Observer" + case .rto: + return "RTO" + case .k9: + return "K9" + case .UNRECOGNIZED: + return "Unknown" + } + } + + private func teamHelpText(_ team: Team) -> String { + switch team { + case .unspecifedColor: + return "Default uses Cyan." + case .UNRECOGNIZED: + return "Unknown team color." + default: + return "Shown to TAK clients as the \(teamTitle(team)) team color." + } + } + + private func roleHelpText(_ role: MemberRole) -> String { + switch role { + case .unspecifed: + return "Default uses Team Member." + case .UNRECOGNIZED: + return "Unknown TAK role." + default: + return "Shown to TAK clients as the \(roleTitle(role)) role." + } + } +} + +#Preview { + let context = PersistenceController.preview.container.viewContext + return TAKModuleConfig(node: nil) + .environmentObject(AccessoryManager.shared) + .environment(\.managedObjectContext, context) +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 26a2d247..363842a2 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -33,14 +33,48 @@ struct Settings: View { // MARK: Helper + private var moduleConfigurationNode: NodeInfoEntity? { + let nodeNum = selectedNode > 0 ? selectedNode : preferredNodeNum + return nodes.first(where: { $0.num == nodeNum }) + } + + private var showsAnyModuleConfiguration: Bool { + isAnySupported([ + .ambientlightingConfig, + .cannedmsgConfig, + .detectionsensorConfig, + .extnotifConfig, + .mqttConfig, + .rangetestConfig, + .paxcounterConfig, + .serialConfig, + .storeforwardConfig, + .telemetryConfig + ]) || isTAKModuleSupported() + } + private func isModuleSupported(_ module: ExcludedModules) -> Bool { - return Int(nodes.first(where: { $0.num == preferredNodeNum })?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0 + return Int(moduleConfigurationNode?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0 } private func isAnySupported(_ modules: [ExcludedModules]) -> Bool { return modules.map(isModuleSupported).contains(true) } + private func isTAKModuleSupported() -> Bool { + guard let node = moduleConfigurationNode else { return false } + if node.takConfig != nil { + return true + } + + guard let roleValue = node.deviceConfig?.role ?? node.user?.role, + let deviceRole = DeviceRoles(rawValue: Int(roleValue)) else { + return false + } + + return deviceRole == .tak || deviceRole == .takTracker + } + // MARK: Views var radioConfigurationSection: some View { @@ -276,13 +310,20 @@ struct Settings: View { } } - NavigationLink(value: SettingsNavigationState.takConfig) { - Label { - Text("TAK") - } icon: { - Image(systemName: "shield.checkered") + if isTAKModuleSupported() { + NavigationLink(value: SettingsNavigationState.takConfig) { + Label { + Text("TAK") + } icon: { + Image(systemName: "shield.checkered") + } } } + + if !showsAnyModuleConfiguration { + Text("This node does not support any configurable modules.") + .foregroundColor(.secondary) + } } header: { Text("Module Configuration") } diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index 3f615f04..7e8b6502 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -9,7 +9,6 @@ import SwiftUI import UniformTypeIdentifiers import OSLog import CoreData -import MeshtasticProtobufs enum CertificateImportType { case p12 @@ -564,236 +563,3 @@ struct ZipDocument: FileDocument { FileWrapper(regularFileWithContents: data) } } - -struct TAKModuleConfig: View { - @Environment(\.managedObjectContext) private var context - @EnvironmentObject private var accessoryManager: AccessoryManager - @Environment(\.dismiss) private var goBack - - let node: NodeInfoEntity? - - @State private var hasChanges = false - @State private var team = Team.unspecifedColor.rawValue - @State private var role = MemberRole.unspecifed.rawValue - - private var selectedTeam: Team { - Team(rawValue: team) ?? .unspecifedColor - } - - private var selectedRole: MemberRole { - MemberRole(rawValue: role) ?? .unspecifed - } - - private var deviceRole: DeviceRoles? { - guard let role = node?.deviceConfig?.role else { return nil } - return DeviceRoles(rawValue: Int(role)) - } - - var body: some View { - Form { - ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues) - - if let deviceRole, deviceRole != .tak && deviceRole != .takTracker { - Section { - Text("These settings only apply when the device role is TAK or TAK Tracker.") - .font(.callout) - .foregroundColor(.orange) - } - } - - Section(header: Text("Identity")) { - VStack(alignment: .leading) { - Picker("Team", selection: $team) { - ForEach(Team.allCases, id: \.rawValue) { teamOption in - Text(teamTitle(teamOption)).tag(teamOption.rawValue) - } - } - .pickerStyle(DefaultPickerStyle()) - Text(teamHelpText(selectedTeam)) - .foregroundColor(.gray) - .font(.callout) - } - - VStack(alignment: .leading) { - Picker("Role", selection: $role) { - ForEach(MemberRole.allCases, id: \.rawValue) { roleOption in - Text(roleTitle(roleOption)).tag(roleOption.rawValue) - } - } - .pickerStyle(DefaultPickerStyle()) - Text(roleHelpText(selectedRole)) - .foregroundColor(.gray) - .font(.callout) - } - } - - Section { - Text("These values are included in TAK position reports. Leave either setting at Default to let firmware use Cyan and Team Member.") - .foregroundColor(.gray) - .font(.callout) - } - } - .disabled(!accessoryManager.isConnected || node?.takConfig == nil) - .safeAreaInset(edge: .bottom, alignment: .center) { - HStack(spacing: 0) { - SaveConfigButton(node: node, hasChanges: $hasChanges) { - guard let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? -1, context: context), - let fromUser = connectedNode.user, - let toUser = node?.user else { - return - } - - var config = ModuleConfig.TAKConfig() - config.team = selectedTeam - config.role = selectedRole - - Task { - _ = try await accessoryManager.saveTAKModuleConfig(config: config, fromUser: fromUser, toUser: toUser) - Task { @MainActor in - hasChanges = false - goBack() - } - } - } - } - } - .navigationTitle("TAK Config") - .navigationBarItems( - trailing: ZStack { - ConnectedDevice( - deviceConnected: accessoryManager.isConnected, - name: accessoryManager.activeConnection?.device.shortName ?? "?" - ) - } - ) - .onFirstAppear { - if let deviceNum = accessoryManager.activeDeviceNum, let node { - let connectedNode = getNodeInfo(id: deviceNum, context: context) - if let connectedNode, node.num != deviceNum { - if UserDefaults.enableAdministration { - let expiration = node.sessionExpiration ?? Date() - if expiration < Date() || node.takConfig == nil { - Task { - do { - Logger.mesh.info("⚙️ Empty or expired TAK module config requesting via PKI admin") - try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!) - } catch { - Logger.mesh.info("🚨 TAK module config request failed: \(error.localizedDescription)") - } - } - } - } else { - Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.") - } - } - } - } - .onChange(of: team) { _, newTeam in - if newTeam != Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) { - hasChanges = true - } - } - .onChange(of: role) { _, newRole in - if newRole != Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) { - hasChanges = true - } - } - } - - private func setTAKValues() { - team = Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) - role = Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) - hasChanges = false - } - - private func teamTitle(_ team: Team) -> String { - switch team { - case .unspecifedColor: - return "Default (Cyan)" - case .white: - return "White" - case .yellow: - return "Yellow" - case .orange: - return "Orange" - case .magenta: - return "Magenta" - case .red: - return "Red" - case .maroon: - return "Maroon" - case .purple: - return "Purple" - case .darkBlue: - return "Dark Blue" - case .blue: - return "Blue" - case .cyan: - return "Cyan" - case .teal: - return "Teal" - case .green: - return "Green" - case .darkGreen: - return "Dark Green" - case .brown: - return "Brown" - case .UNRECOGNIZED: - return "Unknown" - } - } - - private func roleTitle(_ role: MemberRole) -> String { - switch role { - case .unspecifed: - return "Default (Team Member)" - case .teamMember: - return "Team Member" - case .teamLead: - return "Team Lead" - case .hq: - return "HQ" - case .sniper: - return "Sniper" - case .medic: - return "Medic" - case .forwardObserver: - return "Forward Observer" - case .rto: - return "RTO" - case .k9: - return "K9" - case .UNRECOGNIZED: - return "Unknown" - } - } - - private func teamHelpText(_ team: Team) -> String { - switch team { - case .unspecifedColor: - return "Default uses Cyan." - case .UNRECOGNIZED: - return "Unknown team color." - default: - return "Shown to TAK clients as the \(teamTitle(team)) team color." - } - } - - private func roleHelpText(_ role: MemberRole) -> String { - switch role { - case .unspecifed: - return "Default uses Team Member." - case .UNRECOGNIZED: - return "Unknown TAK role." - default: - return "Shown to TAK clients as the \(roleTitle(role)) role." - } - } -} - -#Preview { - let context = PersistenceController.preview.container.viewContext - return TAKModuleConfig(node: nil) - .environmentObject(AccessoryManager.shared) - .environment(\.managedObjectContext, context) -} From 85f07608cba8c7486c9e49f2c08942fe21a0fa6d Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 8 Apr 2026 13:26:57 -0500 Subject: [PATCH 3/3] Fix visibility --- Localizable.xcstrings | 3 +++ .../Config/Module/TAKModuleConfig.swift | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 27047cb7..34a26370 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -29054,6 +29054,9 @@ } } } + }, + "Loading TAK config from the node." : { + }, "Local Network Access" : { "comment" : "A label displayed above the options for local network access.", diff --git a/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift b/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift index 56803bb8..8125956f 100644 --- a/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift @@ -30,15 +30,20 @@ struct TAKModuleConfig: View { return DeviceRoles(rawValue: Int(role)) } - private var isConnectedNode: Bool { - guard let node else { return false } - return node.num == accessoryManager.activeDeviceNum - } - var body: some View { Form { ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues) + if accessoryManager.isConnected, node?.takConfig == nil { + Section { + HStack(spacing: 12) { + ProgressView() + Text("Loading TAK config from the node.") + .foregroundColor(.secondary) + } + } + } + if let deviceRole, deviceRole != .tak && deviceRole != .takTracker { Section { Text("These settings only apply when the device role is TAK or TAK Tracker.") @@ -79,7 +84,7 @@ struct TAKModuleConfig: View { .font(.callout) } } - .disabled(!accessoryManager.isConnected || (!isConnectedNode && node?.takConfig == nil)) + .disabled(!accessoryManager.isConnected || node?.takConfig == nil) .safeAreaInset(edge: .bottom, alignment: .center) { HStack(spacing: 0) { SaveConfigButton(node: node, hasChanges: $hasChanges) {