diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2901645c..7d7ccb17 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -17821,7 +17821,7 @@ } }, "Fix Channel" : { - "comment" : "The text on a button that, when pressed, will attempt to fix the primary LoRaWAN channel.", + "comment" : "The text on a button that, when pressed, will attempt to fix the primary LoRa channel.", "isCommentAutoGenerated" : true }, "Fix Primary Channel?" : { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 0e1c5a84..5e1a46bd 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -516,16 +516,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { if let text = String(bytes: data.payload, encoding: .utf8) { Logger.tak.debug("Text message received, calling broadcast") let server = TAKServerManager.shared - if server.meshToCotEnabled && server.isRunning && !server.connectedClients.isEmpty { - if server.bridge == nil { - Logger.tak.info("Initializing bridge for text message") - let bridge = TAKMeshtasticBridge( - accessoryManager: AccessoryManager.shared, - takServerManager: server - ) - bridge.context = AccessoryManager.shared.context - server.bridge = bridge - } + if server.ensureBridgeReadyForMeshToCot() { await server.bridge?.broadcastMeshTextMessageToTAK(text: text, from: packet.from, channel: packet.channel, to: packet.to) } } @@ -537,16 +528,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { if let position = try? Position(serializedBytes: data.payload) { Logger.tak.debug("Position received, calling broadcast") let server = TAKServerManager.shared - if server.meshToCotEnabled && server.isRunning && !server.connectedClients.isEmpty { - if server.bridge == nil { - Logger.tak.info("Initializing bridge for position") - let bridge = TAKMeshtasticBridge( - accessoryManager: AccessoryManager.shared, - takServerManager: server - ) - bridge.context = AccessoryManager.shared.context - server.bridge = bridge - } + if server.ensureBridgeReadyForMeshToCot() { await server.bridge?.broadcastMeshPositionToTAK(position: position, from: packet.from) } } diff --git a/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift b/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift index efc60282..21c67fdf 100644 --- a/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift +++ b/Meshtastic/Helpers/TAK/MeshToCoTConverter.swift @@ -9,6 +9,7 @@ import Foundation import MeshtasticProtobufs import CoreLocation import OSLog +import Combine /// Converts Meshtastic packets to CoT format for bridging to TAK Server final class MeshToCoTConverter: ObservableObject { @@ -123,12 +124,28 @@ final class MeshToCoTConverter: ObservableObject { // Get emoji based on waypoint icon/expire time let iconEmoji = getEmojiForWaypoint(waypoint) + // Handle expiry - if expire is 0, never expire. Otherwise use the expire time as Unix timestamp + let stale: Date + if waypoint.expire == 0 { + // Never expire - set to 1 year from now + stale = Date().addingTimeInterval(365 * 24 * 60 * 60) + } else { + // expire is Unix timestamp when waypoint expires + let expireDate = Date(timeIntervalSince1970: TimeInterval(waypoint.expire)) + if expireDate > Date() { + stale = expireDate + } else { + // Already expired, don't broadcast + return nil + } + } + return CoTMessage( uid: uid, type: "b-ttf-ff", // Point feature friend time: Date(), start: Date(), - stale: Date().addingTimeInterval(TimeInterval(waypoint.expire * 60)), + stale: stale, how: "m-g", latitude: latitude, longitude: longitude, @@ -156,9 +173,8 @@ final class MeshToCoTConverter: ObservableObject { return CoTMessage.chat( senderUid: senderUid, senderCallsign: senderName, - messageId: messageId, message: text, - channelName: "Primary" + chatroom: "Primary" ) } diff --git a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift index ec15f2fc..2c6f584a 100644 --- a/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift +++ b/Meshtastic/Helpers/TAK/TAKMeshtasticBridge.swift @@ -511,7 +511,7 @@ final class TAKMeshtasticBridge { /// Send all known mesh node positions to TAK clients /// Useful when a new TAK client connects - /// Only sends nodes with positions updated within the last hour + /// Only sends nodes with positions updated within the last 2 hours /// Excludes the node we're currently connected to func broadcastAllNodesToTAK() async { guard let takServerManager, takServerManager.isRunning else { return } @@ -844,7 +844,7 @@ final class TAKMeshtasticBridge { case 0x1F681, 11: // 🚁 return ("a-u-G", "\(googleUUID)/Google/heliport.png", "-16776961") // ⛵ Boat - Google marina - case 0x1F6B5, 12: // ⛵ + case 0x26F5, 12: // ⛵ return ("a-u-G", "\(googleUUID)/Google/marina.png", "-16776961") // 🚢 Ship - Google marina case 0x1F6A2: // 🚢 @@ -853,7 +853,7 @@ final class TAKMeshtasticBridge { case 0x1F680: // 🚀 return ("a-u-G", "\(googleUUID)/Google/target.png", "-16776961") // 🛸 UFO - Generic purple pushpin - case 0x1F6B5, 13: // 🛸 + case 0x1F6B8, 13: // 🛸 return ("a-u-G", "\(genericUUID)/Tacks/purple-pushpin.png", "-65281") // 🚲 Bicycle - Google cycling case 0x1F6B2: // 🚲 @@ -1357,13 +1357,13 @@ final class TAKMeshtasticBridge { case 0x1F6A6: // 🚩 return ("a-u-G", "\(googleUUID)/Google/flag.png", "-16776961") // ⛔ No Entry - Google caution - case 0x1F6D1: // ⛔ + case 0x26D4: // ⛔ return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") // 🛑 Stop - Google caution case 0x1F6D1: // 🛑 return ("a-u-G", "\(googleUUID)/Google/caution.png", "-16776961") // 🏕️ Base Camp - Google campground - case 0x1F6D1: // 🏕️ + case 0x1F3D5: // 🏕️ return ("a-u-G", "\(googleUUID)/Google/campground.png", "-16776961") // 🏢 Office Building - Google homegardenbusiness case 0x1F3E2: // 🏢 diff --git a/Meshtastic/Helpers/TAK/TAKServerManager.swift b/Meshtastic/Helpers/TAK/TAKServerManager.swift index 94b2c7b2..15cf3509 100644 --- a/Meshtastic/Helpers/TAK/TAKServerManager.swift +++ b/Meshtastic/Helpers/TAK/TAKServerManager.swift @@ -159,10 +159,10 @@ final class TAKServerManager: ObservableObject { return } - let channelName = primaryChannel.name ?? "" - let channelPsk = primaryChannel.psk ?? Data() - let pskBase64 = channelPsk.base64EncodedString() - + let channelName = primaryChannel.name ?? "" + let channelPsk = primaryChannel.psk ?? Data() + let pskBase64 = channelPsk.base64EncodedString() + if channelName.isEmpty { issues.append(PrimaryChannelIssue( title: "Unnamed Primary Channel", @@ -172,22 +172,25 @@ final class TAKServerManager: ObservableObject { isValid = false } - let pskLength = pskBase64.count - if pskLength == 0 { + // Use byte length for encryption strength checks (not Base64 string length) + let pskBytes = channelPsk.count + if pskBytes == 0 { issues.append(PrimaryChannelIssue( title: "Public Channel Not Supported", description: "TAK Server requires a private channel with encryption. Public channels expose your location and messages. Tap the button below to set up a private TAK channel.", canAutoFix: true )) isValid = false - } else if pskBase64 == "AQ==" { + } else if channelPsk == Data([0x01]) { + // Default key is single byte 0x01 issues.append(PrimaryChannelIssue( title: "Default Encryption Key", description: "TAK Server requires a unique private channel key. The default key is not secure. Tap the button below to set up a proper private TAK channel.", canAutoFix: true )) isValid = false - } else if pskLength == 4 { + } else if pskBytes < 16 { + // Less than 128-bit (16 bytes) issues.append(PrimaryChannelIssue( title: "Weak Encryption Key", description: "TAK Server requires at least 128-bit encryption for your privacy. Tap the button below to set up a secure private TAK channel.", @@ -527,6 +530,25 @@ final class TAKServerManager: ObservableObject { } } } + + /// Ensure bridge is initialized and ready for mesh-to-CoT broadcasting + /// Returns true if broadcasting is possible (meshToCotEnabled, server running, clients connected) + /// Call this before any mesh-to-CoT broadcast operations + func ensureBridgeReadyForMeshToCot() -> Bool { + guard meshToCotEnabled, isRunning, !connectedClients.isEmpty else { return false } + + if bridge == nil { + Logger.tak.info("Initializing bridge for mesh-to-CoT broadcast") + let accessoryManager = AccessoryManager.shared + let newBridge = TAKMeshtasticBridge( + accessoryManager: accessoryManager, + takServerManager: self + ) + newBridge.context = accessoryManager.context + bridge = newBridge + } + return true + } /// Send a CoT message to a specific client func send(_ cotMessage: CoTMessage, to clientId: UUID) async throws { @@ -548,7 +570,7 @@ final class TAKServerManager: ObservableObject { // MARK: - Auto-fix Primary Channel /// Automatically fix the primary channel to TAK-compatible settings - /// Sets: Name="TAK", 256-bit AES key, LoRa channel=0 + /// Sets: Name="TAK", 256-bit AES key, preserves existing LoRa channel /// Returns true if successful func autoFixPrimaryChannel() async -> Bool { let accessoryManager = AccessoryManager.shared