diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 03458818..25bac479 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -41604,7 +41604,6 @@ } }, "TAK" : { - "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { @@ -49740,4 +49739,4 @@ } }, "version" : "1.1" -} +} \ No newline at end of file diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1544d0eb..4c264fbb 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -2170,7 +2170,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.8; + MARKETING_VERSION = 2.7.9; OTHER_LDFLAGS = ( "-weak_framework", SwiftUI, @@ -2209,7 +2209,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.8; + MARKETING_VERSION = 2.7.9; OTHER_LDFLAGS = ( "-weak_framework", SwiftUI, @@ -2245,7 +2245,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.8; + MARKETING_VERSION = 2.7.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2278,7 +2278,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.8; + MARKETING_VERSION = 2.7.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift index 9ed42c90..23a08afe 100644 --- a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift +++ b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift @@ -147,6 +147,8 @@ final class TAKMeshtasticBridge { return } + let channel = UInt32(TAKServerManager.shared.channel) + // Determine send method based on CoT type let sendMethod = GenericCoTHandler.shared.classifySendMethod(for: cotMessage) @@ -159,7 +161,7 @@ final class TAKMeshtasticBridge { } do { - try await accessoryManager.sendTAKPacket(takPacket) + try await accessoryManager.sendTAKPacket(takPacket, channel: channel) Logger.tak.info("Sent TAKPacket to mesh: \(cotMessage.type)") } catch { Logger.tak.error("Failed to send TAKPacket to mesh: \(error.localizedDescription)") @@ -169,7 +171,7 @@ final class TAKMeshtasticBridge { // Use EXI compression on ATAK_FORWARDER port (257) GenericCoTHandler.shared.accessoryManager = accessoryManager do { - try await GenericCoTHandler.shared.sendGenericCoT(cotMessage) + try await GenericCoTHandler.shared.sendGenericCoT(cotMessage, channel: channel) Logger.tak.info("Sent generic CoT to mesh via ATAK_FORWARDER: \(cotMessage.type)") } catch { Logger.tak.error("Failed to send generic CoT to mesh: \(error.localizedDescription)") diff --git a/Meshtastic/Helpers/TAK/TAKServerManager.swift b/Meshtastic/Helpers/TAK/TAKServerManager.swift index e3e3caa7..182e47bb 100644 --- a/Meshtastic/Helpers/TAK/TAKServerManager.swift +++ b/Meshtastic/Helpers/TAK/TAKServerManager.swift @@ -26,6 +26,8 @@ final class TAKServerManager: ObservableObject { // MARK: - Configuration (persisted via AppStorage) + @AppStorage("takServerChannel") var channel: Int = 0 + @AppStorage("takServerEnabled") var enabled = false { didSet { Task { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index ed5b2af2..32d9bc99 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -156,10 +156,16 @@ extension MeshPackets { nonisolated public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { do { - let objects = channel.allPrivateMessages + // Copied logic from ChannelEntity.allPrivateMessages, which is always on the MainActor + // But this code may not be on the MainActor. + let fetchRequest = MessageEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", channel.index) + let objects = (try? context.fetch(fetchRequest)) ?? [MessageEntity]() + for object in objects { context.delete(object) } + try context.save() } catch let error as NSError { Logger.data.error("\(error.localizedDescription, privacy: .public)") diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 477a03ed..bf3c2752 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -162,8 +162,13 @@ struct ChannelList: View { Button(role: .destructive) { Task { await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!) - context.refresh(myInfo, mergeChanges: true) - channelToDeleteMessages = nil + await MainActor.run { + context.refresh(channel, mergeChanges: true) + context.refresh(myInfo, mergeChanges: true) + + // Reset state + channelToDeleteMessages = nil + } } } label: { Text("Delete") diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 23ed226e..495a2910 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -70,7 +70,7 @@ struct AppSettings: View { } #endif } - Section(header: Text("environment")) { + Section(header: Text("Environment")) { VStack(alignment: .leading) { Toggle(isOn: $environmentEnableWeatherKit) { Label("Weather Conditions", systemImage: "cloud.sun") diff --git a/Meshtastic/Views/Settings/TAKServerConfig.swift b/Meshtastic/Views/Settings/TAKServerConfig.swift index 749b54fc..09d9d60d 100644 --- a/Meshtastic/Views/Settings/TAKServerConfig.swift +++ b/Meshtastic/Views/Settings/TAKServerConfig.swift @@ -8,6 +8,7 @@ import SwiftUI import UniformTypeIdentifiers import OSLog +import CoreData enum CertificateImportType { case p12 @@ -15,6 +16,15 @@ enum CertificateImportType { } struct TAKServerConfig: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var accessoryManager: AccessoryManager + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)], + predicate: NSPredicate(format: "role > 0"), + animation: .default + ) private var channels: FetchedResults + @StateObject private var takServer = TAKServerManager.shared @State private var showingFileImporter = false @State private var importType: CertificateImportType = .p12 @@ -140,6 +150,17 @@ struct TAKServerConfig: View { .foregroundColor(.secondary) } + if !channels.isEmpty { + Picker(selection: $takServer.channel) { + ForEach(channels, id: \.index) { channel in + channelLabel(channel) + .tag(Int(channel.index)) + } + } label: { + Label("TAK Channel Index", systemImage: "bubble.left.and.bubble.right") + } + } + if takServer.isRunning { Button { Task { @@ -152,7 +173,7 @@ struct TAKServerConfig: View { } header: { Text("Configuration") } footer: { - Text("Secure mTLS connection on port 8089. Both server and client certificates are required.") + Text("Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent.") } } @@ -280,6 +301,21 @@ struct TAKServerConfig: View { } + // MARK: - Channel Label + + @ViewBuilder + private func channelLabel(_ channel: ChannelEntity) -> some View { + if channel.name?.isEmpty ?? false { + if channel.role == 1 { + Text(String("PrimaryChannel").camelCaseToWords()) + } else { + Text(String("Channel \(channel.index)").camelCaseToWords()) + } + } else { + Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()) + } + } + // MARK: - Import Handlers private func handleP12Import(_ result: Result<[URL], Error>) {