This commit is contained in:
Benjamin Faershtein 2026-02-21 17:21:12 -08:00
parent 239f1ac5cc
commit 67ecc493e8
5 changed files with 58 additions and 38 deletions

View file

@ -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?" : {

View file

@ -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)
}
}

View file

@ -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"
)
}

View file

@ -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: // 🏢

View file

@ -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