From 2d26621a35fc071f88b6f94a9b3a778d0e1b4a48 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 15 Apr 2026 21:11:56 -0500 Subject: [PATCH] Add TAK server configuration and test fixtures --- app/src/main/AndroidManifest.xml | 6 +- .../core/data/manager/MeshDataHandlerImpl.kt | 2 +- .../core/navigation/NavigationConfig.kt | 1 + .../org/meshtastic/core/navigation/Routes.kt | 2 + core/proto/src/main/proto | 2 +- .../meshtastic/core/service/MeshService.kt | 48 ++ .../service/MeshServiceOrchestratorTest.kt | 9 +- core/takserver/build.gradle.kts | 46 +- core/takserver/libs/meshtastic-tak-0.1.0.jar | Bin 297196 -> 0 bytes .../CoTHandler.kt => AtakFileWriter.kt} | 21 +- .../core/takserver/CoTDetailStripper.kt | 176 +++++++ .../org/meshtastic/core/takserver/CoTXml.kt | 28 +- .../meshtastic/core/takserver/CoTXmlParser.kt | 29 ++ .../takserver/RouteDataPackageGenerator.kt | 125 +++++ .../core/takserver/TAKClientConnection.kt | 228 --------- .../core/takserver/TAKDataPackageGenerator.kt | 144 +++++- .../meshtastic/core/takserver/TAKDefaults.kt | 43 +- .../core/takserver/TAKMeshIntegration.kt | 416 +++++++++++++--- .../meshtastic/core/takserver/TAKModels.kt | 19 +- .../core/takserver/TAKPacketConversion.kt | 196 -------- .../core/takserver/TAKPacketV2Conversion.kt | 270 ++++++++++ .../meshtastic/core/takserver/TAKServer.kt | 212 ++------ .../core/takserver/TAKServerManager.kt | 131 ++--- .../core/takserver/TakMeshTestRunner.kt | 183 +++++++ .../core/takserver/TakV2Compressor.kt | 56 +++ .../core/takserver/TakV2TypeMapper.kt | 70 +++ .../core/takserver/di/CoreTakServerModule.kt | 15 +- .../core/takserver/fountain/FountainCodec.kt | 466 ------------------ .../takserver/fountain/GenericCoTHandler.kt | 231 --------- .../core/takserver/CoTDetailStripperTest.kt | 228 +++++++++ .../core/takserver/CoTXmlParserTest.kt | 73 +++ .../meshtastic/core/takserver/CoTXmlTest.kt | 9 +- .../core/takserver/TAKPacketConversionTest.kt | 155 ------ .../takserver/TAKPacketV2RawDetailTest.kt | 132 +++++ .../takserver/fountain/FountainCodecTest.kt | 115 ----- .../core/takserver/AtakFileWriter.kt} | 13 +- .../meshtastic/core/takserver/TAKServerIos.kt | 46 ++ .../core/takserver/TakV2Compressor.kt | 59 +++ .../core/takserver/fountain/CodecActual.kt | 124 ----- .../core/takserver/AtakFileWriter.kt | 54 ++ .../core/takserver/TAKClientConnection.kt | 334 +++++++++++++ .../meshtastic/core/takserver/TAKServerJvm.kt | 290 +++++++++++ .../core/takserver/TakCertLoader.kt | 152 ++++++ .../core/takserver/TakV2Compressor.kt | 11 + .../core/takserver/fountain/CodecActual.kt | 75 --- .../jvmAndroidMain/resources/tak_certs/ca.pem | 23 + .../resources/tak_certs/client.p12 | Bin 0 -> 3827 bytes .../resources/tak_certs/server.p12 | Bin 0 -> 3859 bytes .../tak_test_fixtures/aircraft_adsb.xml | 5 + .../tak_test_fixtures/aircraft_hostile.xml | 5 + .../resources/tak_test_fixtures/alert_tic.xml | 8 + .../resources/tak_test_fixtures/casevac.xml | 10 + .../tak_test_fixtures/casevac_medline.xml | 10 + .../chat_receipt_delivered.xml | 9 + .../tak_test_fixtures/chat_receipt_read.xml | 9 + .../tak_test_fixtures/delete_event.xml | 5 + .../tak_test_fixtures/drawing_circle.xml | 25 + .../drawing_circle_large.xml | 15 + .../tak_test_fixtures/drawing_ellipse.xml | 17 + .../tak_test_fixtures/drawing_freeform.xml | 19 + .../tak_test_fixtures/drawing_polygon.xml | 19 + .../tak_test_fixtures/drawing_rectangle.xml | 19 + .../drawing_rectangle_itak.xml | 16 + .../drawing_telestration.xml | 53 ++ .../tak_test_fixtures/emergency_911.xml | 10 + .../tak_test_fixtures/emergency_cancel.xml | 11 + .../tak_test_fixtures/geochat_broadcast.xml | 12 + .../tak_test_fixtures/geochat_dm.xml | 12 + .../tak_test_fixtures/geochat_simple.xml | 12 + .../tak_test_fixtures/marker_2525.xml | 14 + .../tak_test_fixtures/marker_goto.xml | 12 + .../tak_test_fixtures/marker_goto_itak.xml | 10 + .../tak_test_fixtures/marker_icon_set.xml | 14 + .../tak_test_fixtures/marker_spot.xml | 14 + .../tak_test_fixtures/marker_tank.xml | 14 + .../resources/tak_test_fixtures/pli_basic.xml | 5 + .../resources/tak_test_fixtures/pli_full.xml | 5 + .../resources/tak_test_fixtures/pli_itak.xml | 11 + .../tak_test_fixtures/pli_stationary.xml | 12 + .../tak_test_fixtures/pli_takaware.xml | 11 + .../tak_test_fixtures/pli_webtak.xml | 5 + .../tak_test_fixtures/ranging_bullseye.xml | 17 + .../tak_test_fixtures/ranging_circle.xml | 17 + .../tak_test_fixtures/ranging_line.xml | 14 + .../resources/tak_test_fixtures/route_3wp.xml | 16 + .../tak_test_fixtures/route_itak_3wp.xml | 11 + .../tak_test_fixtures/task_engage.xml | 10 + .../resources/tak_test_fixtures/waypoint.xml | 12 + .../settings/ModuleConfigurationScreen.kt | 5 +- .../settings/navigation/SettingsNavigation.kt | 5 + .../feature/settings/radio/RadioConfig.kt | 7 + .../radio/component/TAKConfigItemList.kt | 256 +++++++--- 92 files changed, 3837 insertions(+), 2029 deletions(-) delete mode 100644 core/takserver/libs/meshtastic-tak-0.1.0.jar rename core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/{fountain/CoTHandler.kt => AtakFileWriter.kt} (56%) create mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTDetailStripper.kt create mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/RouteDataPackageGenerator.kt delete mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt delete mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt create mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt create mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt create mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt create mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2TypeMapper.kt delete mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/FountainCodec.kt delete mode 100644 core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/GenericCoTHandler.kt create mode 100644 core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTDetailStripperTest.kt delete mode 100644 core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketConversionTest.kt create mode 100644 core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketV2RawDetailTest.kt delete mode 100644 core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/fountain/FountainCodecTest.kt rename core/takserver/src/{commonMain/kotlin/org/meshtastic/core/takserver/fountain/CodecExpect.kt => iosMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt} (72%) create mode 100644 core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TAKServerIos.kt create mode 100644 core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt delete mode 100644 core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt create mode 100644 core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt create mode 100644 core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt create mode 100644 core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt create mode 100644 core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakCertLoader.kt delete mode 100644 core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_certs/ca.pem create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_certs/client.p12 create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_certs/server.p12 create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/aircraft_adsb.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/aircraft_hostile.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/alert_tic.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac_medline.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_delivered.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_read.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/delete_event.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle_large.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_ellipse.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_freeform.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_polygon.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle_itak.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_telestration.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_911.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_cancel.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_broadcast.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_dm.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_simple.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_2525.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto_itak.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_icon_set.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_spot.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_tank.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_basic.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_full.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_itak.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_stationary.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_takaware.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_webtak.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_bullseye.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_circle.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_line.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_3wp.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_itak_3wp.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/task_engage.xml create mode 100644 core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/waypoint.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3bea85f7..16b87cd52 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,9 +60,9 @@ --> - + + diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 0a3f03004..0c55c6556 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -191,7 +191,7 @@ class MeshDataHandlerImpl( } PortNum.ATAK_PLUGIN, - PortNum.ATAK_FORWARDER, + PortNum.ATAK_PLUGIN_V2, PortNum.PRIVATE_APP, -> { shouldBroadcast = true diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt index fe5c6225a..180c6c659 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/NavigationConfig.kt @@ -100,6 +100,7 @@ val MeshtasticNavSavedStateConfig = SavedStateConfiguration { // Settings - Advanced routes subclass(SettingsRoutes.CleanNodeDb::class, SettingsRoutes.CleanNodeDb.serializer()) + subclass(SettingsRoutes.TakServer::class, SettingsRoutes.TakServer.serializer()) subclass(SettingsRoutes.DebugPanel::class, SettingsRoutes.DebugPanel.serializer()) subclass(SettingsRoutes.About::class, SettingsRoutes.About.serializer()) subclass(SettingsRoutes.FilterSettings::class, SettingsRoutes.FilterSettings.serializer()) diff --git a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt index b58b20f2b..c442d7b20 100644 --- a/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt +++ b/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt @@ -162,6 +162,8 @@ object SettingsRoutes { @Serializable data object CleanNodeDb : Route + @Serializable data object TakServer : Route + @Serializable data object DebugPanel : Route @Serializable data object About : Route diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index 10a86bf0b..9b123f392 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit 10a86bf0b9c1fc9242363c17e4dfc54185967232 +Subproject commit 9b123f392fab424db3f413243db2ea66f1880334 diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt index c8b7fdfab..1a0029edb 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt @@ -22,6 +22,7 @@ import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder +import android.os.PowerManager import androidx.core.app.ServiceCompat import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -73,6 +74,16 @@ class MeshService : Service() { private val serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + /** + * Partial wake lock held while the foreground service is running. Prevents the CPU + * from being throttled while the TAK server's keepalive coroutines, socket writes, + * and mesh packet handlers need to run on a regular cadence. Without this, OEM + * battery optimizations can pause coroutines for long enough that connected TAK + * clients (ATAK/iTAK) time out waiting for data, even though the foreground + * service itself keeps the process alive. + */ + private var wakeLock: PowerManager.WakeLock? = null + private val myNodeNum: Int get() = nodeManager.myNodeNum.value ?: throw RadioNotConnectedException() @@ -162,14 +173,50 @@ class MeshService : Service() { return if (!wantForeground) { Logger.i { "Stopping mesh service because no device is selected" } + releaseWakeLock() ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) stopSelf() START_NOT_STICKY } else { + acquireWakeLock() START_STICKY } } + private fun acquireWakeLock() { + if (wakeLock?.isHeld == true) return + try { + val powerManager = getSystemService(POWER_SERVICE) as PowerManager + val lock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "Meshtastic::MeshServiceWakeLock", + ).apply { + setReferenceCounted(false) + } + lock.acquire() + wakeLock = lock + Logger.i { "Acquired partial wake lock for mesh service" } + } catch (e: SecurityException) { + Logger.w(e) { "Failed to acquire wake lock — WAKE_LOCK permission missing?" } + } catch (e: Exception) { + Logger.w(e) { "Failed to acquire wake lock" } + } + } + + private fun releaseWakeLock() { + val lock = wakeLock ?: return + try { + if (lock.isHeld) { + lock.release() + Logger.i { "Released partial wake lock for mesh service" } + } + } catch (e: Exception) { + Logger.w(e) { "Failed to release wake lock" } + } finally { + wakeLock = null + } + } + override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) Logger.i { "Mesh service: onTaskRemoved" } @@ -179,6 +226,7 @@ class MeshService : Service() { override fun onDestroy() { Logger.i { "Destroying mesh service" } + releaseWakeLock() ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) orchestrator.stop() serviceJob.cancel() diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt index 611454d05..428c44ac4 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt @@ -40,14 +40,13 @@ import org.meshtastic.core.repository.MeshMessageProcessor import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.NodeRepository + import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.TakPrefs import org.meshtastic.core.takserver.TAKMeshIntegration import org.meshtastic.core.takserver.TAKServerManager -import org.meshtastic.core.takserver.fountain.CoTHandler import org.meshtastic.proto.LocalModuleConfig import kotlin.test.Test import kotlin.test.assertFalse @@ -59,7 +58,7 @@ class MeshServiceOrchestratorTest { private val serviceRepository: ServiceRepository = mock(MockMode.autofill) private val packetHandler: PacketHandler = mock(MockMode.autofill) private val nodeManager: NodeManager = mock(MockMode.autofill) - private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val messageProcessor: MeshMessageProcessor = mock(MockMode.autofill) private val commandSender: CommandSender = mock(MockMode.autofill) private val connectionManager: MeshConnectionManager = mock(MockMode.autofill) @@ -69,7 +68,6 @@ class MeshServiceOrchestratorTest { private val serviceNotifications: MeshServiceNotifications = mock(MockMode.autofill) private val takServerManager: TAKServerManager = mock(MockMode.autofill) private val takPrefs: TakPrefs = mock(MockMode.autofill) - private val cotHandler: CoTHandler = mock(MockMode.autofill) private val databaseManager: DatabaseManager = mock(MockMode.autofill) private val testDispatcher = UnconfinedTestDispatcher() @@ -91,17 +89,14 @@ class MeshServiceOrchestratorTest { every { takPrefs.isTakServerEnabled } returns takEnabledFlow every { takServerManager.isRunning } returns takRunningFlow every { takServerManager.inboundMessages } returns MutableSharedFlow() - every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) every { router.actionHandler } returns actionHandler val takMeshIntegration = TAKMeshIntegration( takServerManager = takServerManager, commandSender = commandSender, - nodeRepository = nodeRepository, serviceRepository = serviceRepository, meshConfigHandler = meshConfigHandler, - cotHandler = cotHandler, ) return MeshServiceOrchestrator( diff --git a/core/takserver/build.gradle.kts b/core/takserver/build.gradle.kts index a1b1a7acb..e36cf1852 100644 --- a/core/takserver/build.gradle.kts +++ b/core/takserver/build.gradle.kts @@ -51,7 +51,51 @@ kotlin { implementation(libs.kermit) } - jvmAndroidMain.dependencies {} + jvmAndroidMain.dependencies { + // TAKPacket-SDK for v2 compression/decompression (via JitPack). + // + // We depend on the `-jvm` variant directly rather than the parent + // `com.github.meshtastic:TAKPacket-SDK` coordinate. JitPack does + // not publish a root-level Gradle module metadata (.module) file + // for the KMP parent, only per-target ones. With just the parent + // POM, Gradle reads the four KMP variants (jvm, iosarm64, + // iossimulatorarm64, metadata) as unconditional Maven deps and + // tries to resolve them ALL against this Android consumer — the + // iOS klibs declare `platform.type=native` with no androidJvm + // variant, so variant selection fails with "No matching variant". + // + // Depending directly on `takpacket-sdk-jvm` skips the parent POM + // entirely and goes straight to the JVM artifact's own module + // metadata, which is compatible with both `jvm()` and Android + // targets in this `jvmAndroidMain` source set. It still pulls + // zstd-jni + xpp3 + wire-runtime-jvm + kotlin-stdlib as + // transitive deps from the JVM variant's POM. + // + // zstd-jni's @aar variant is still declared explicitly in the + // androidMain source set below so Android gets the .so files. + implementation("com.github.meshtastic.TAKPacket-SDK:takpacket-sdk-jvm:v0.1.3") { + // The SDK's jvmMain declares zstd-jni as a runtime dep (standard + // JAR with desktop native libs). Android needs the @aar variant + // instead (ships arm/arm64/x86/x86_64 .so files). Both packaging + // formats contain the same Java classes, so Android's dex merger + // hits "Duplicate class" errors if both land on the classpath. + // Exclude here; androidMain re-adds it as @aar below, and jvmMain + // re-adds the JAR for desktop. + exclude(group = "com.github.luben", module = "zstd-jni") + } + } + + jvmMain.dependencies { + // Desktop JVM: standard JAR bundles native libs for desktop archs. + implementation("com.github.luben:zstd-jni:1.5.7-7") + } + + androidMain.dependencies { + // Android: @aar variant ships .so files for arm/arm64/x86/x86_64. + // Without this, zstd-jni's ZstdDictCompress. throws + // UnsatisfiedLinkError and poisons TakV2Compressor permanently. + implementation("com.github.luben:zstd-jni:1.5.7-7@aar") + } commonTest.dependencies { implementation(projects.core.testing) diff --git a/core/takserver/libs/meshtastic-tak-0.1.0.jar b/core/takserver/libs/meshtastic-tak-0.1.0.jar deleted file mode 100644 index 8cd8eccbd9feff6ff95042d2abb6a7ee662b9a4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 297196 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�hNY{$MjA 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ℜ%;8qt?MFubo2;URRCz$^600g_MD zizJXw@NF951LJy7=n1(mI{1n#ln=EhoAg5(lF!fO8iC-WqAz>*;`sq2!sWUXg(unG z1IcmDEvzJ5`)!Ac-ZzqKqbJwIUBg>f8%!|i3G_aC7uxTGVFiV0heq;}1;+<^{EE#ShYt}O>a1JSx zf-KoWbELRY-d!5ifq*_?awHv(`io4Xz#7VURVJaj2FKCtr+HmCJJgkClzQS3iZUu<)XiC~;LrHbdA8nd>)T<2ljILjILjw=RoitE6GHHX<3 zLU4>(SVEc{)Ow&nea)fpY0n1~vHO6+5+ci2j!6gz}9QB6oV9pLPYRElJB zdP))|btlhEsgN&>uB<{Rlo!k*lb4CaV<;RMW>emW�WAw@!LppNon54i}2gVkj!T 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 diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/CoTHandler.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt similarity index 56% rename from core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/CoTHandler.kt rename to core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt index 544aabfad..bbe6af5f9 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/CoTHandler.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt @@ -14,18 +14,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.takserver.fountain - -import org.meshtastic.core.takserver.CoTMessage +package org.meshtastic.core.takserver /** - * Handles incoming and outgoing generic Cursor on Target (CoT) messages wrapped in Meshtastic DataPackets. + * Writes data package files to ATAK's auto-import directory. * - * Defines the contract for routing Direct (unfragmented) vs Fountain-encoded packets, and processing decompressed - * EXI/Zlib XML payloads. + * On Android, the actual implementation writes to + * `/sdcard/atak/tools/datapackage/` which ATAK monitors for new zip files. + * On other platforms this is a no-op. */ -interface CoTHandler { - suspend fun sendGenericCoT(cotMessage: CoTMessage) - - suspend fun handleIncomingForwarderPacket(payload: ByteArray, senderNodeNum: Int) +internal expect object AtakFileWriter { + /** + * Write a data package zip to ATAK's monitored import directory. + * @return true if the file was written successfully, false otherwise. + */ + fun writeToImportDir(fileName: String, zipBytes: ByteArray): Boolean } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTDetailStripper.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTDetailStripper.kt new file mode 100644 index 000000000..8aec460ad --- /dev/null +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTDetailStripper.kt @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +/** + * Removes bloat elements from the `` content of a CoT event before it is + * stuffed into a [org.meshtastic.proto.TAKPacketV2] `raw_detail` field for mesh + * transmission. + * + * # Why this exists + * + * A LoRa mesh packet has a hard payload limit of + * [org.meshtastic.proto.Constants.DATA_PAYLOAD_LEN] = 233 bytes for the entire encoded + * `Data` proto (portnum + payload + reply_id + emoji). Subtracting the wrapper + * overhead leaves roughly **~225 bytes** for the TAK wire payload, and the wire payload + * itself is `[1 byte dict-id flag][zstd-compressed TAKPacketV2 protobuf]`. + * + * ATAK emits CoT events with rich visual metadata that is **never useful over a mesh**: + * icon set paths, ARGB colors, shape geometry, archive flags, file references, etc. + * A typical `u-d-c-c` (user-drawn circle) event from ATAK is **800+ bytes of XML**, of + * which maybe 80 bytes are actually meaningful to a receiving node. Even with + * dictionary compression, the full payload overflows the MTU. + * + * This stripper deletes elements the receiving node can synthesize or ignore, leaving + * only the minimum needed to rebuild a usable `` on the other side: who sent + * it, where they are, what team/role they're on, battery status, chat content, and + * the high-level CoT type (which rides separately on [TAKPacketV2.cot_type_id] / + * [TAKPacketV2.cot_type_str]). + * + * # What gets dropped + * + * **Cosmetic / rendering-only** (pure visual, no situational awareness value): + * - `` — ARGB stroke/fill colors + * - ``, ``, `` — shape styling + * - `` — label visibility toggle + * - `` — icon set path (`COT_MAPPING_2525B/...`) + * - `` — 3D model reference + * + * **Geometric detail** (we keep lat/lon on the event; shape primitives are too big): + * - `...` — ellipse/polyline/polygon geometry + * - ``, `` — rendering hints + * + * **Resource references** (useless without the resource being reachable): + * - `` — file transfer references + * - `<__video .../>` — video stream URL + * + * **Flags and redundant metadata**: + * - `` — "save to archive" flag + * - `` — redundant with the event's `` attributes + * - `` — rectangle "toggle" UI state flag + * - `<_flow-tags_ .../>` — TAK Server routing metadata (server-to-server, not needed on mesh) + * + * # What gets preserved + * + * Anything the stripper doesn't explicitly match is passed through untouched. That + * includes all of the structured elements that the regular [CoTXmlParser] understands + * (contact, __group, status, track, remarks, __chat, chatgrp, link, uid, + * __serverdestination) plus any unknown extensions — better to over-preserve than + * silently drop something the receiving ATAK actually needs. + * + * # Whitespace + * + * All inter-element whitespace and indentation is collapsed. Whitespace inside text + * nodes (e.g. `hello world`) is preserved. + * + * # Not a real XML parser + * + * This is intentionally string/regex based, not DOM. The input is a small, well-formed + * fragment produced by ATAK's serializer, so a full parser is overkill — and we want + * this to be dependency-free so it can run on every KMP target without pulling in + * xmlutil for a one-off job. If ATAK starts emitting namespaced elements or embedded + * CDATA that tangles with these patterns, the stripper will leave them alone rather + * than corrupt the output, which is the safer failure mode. + */ +internal object CoTDetailStripper { + + /** + * Element names whose entire subtree (or self-closing tag) is removed. + * + * Order matters only for documentation. Each entry is tried against both the + * self-closing form `` and the paired form `...`. + */ + private val STRIPPED_ELEMENTS = listOf( + // Cosmetic / rendering + "color", + "strokeColor", + "strokeWeight", + "fillColor", + "labels_on", + "usericon", + "model", + // Geometric + "shape", + "height", + "height_unit", + // Resource refs + "fileshare", + "__video", + // Flags / redundant + "archive", + "precisionlocation", + // Rectangle/polyline "toggle" UI flag, and TAK Server routing metadata. + // The underscore-prefixed element names are legal XML identifiers ATAK uses + // for internal state that receiving meshtastic nodes have no use for. + "tog", + "_flow-tags_", + ) + + /** + * Pre-compiled regex list: for each stripped element, one pattern that matches + * either a self-closing tag or a paired open/close tag (non-greedy content). + * + * `[^>]*?` inside the open tag tolerates attribute quoting with both single and + * double quotes but bails if it encounters a `>` (so it won't accidentally swallow + * unrelated content). + * + * The leading `(?s)` inline flag is the KMP-portable equivalent of + * `RegexOption.DOT_MATCHES_ALL` — it lets `.` match newlines so a multi-line + * `...` subtree is captured in one pass. `RegexOption.DOT_MATCHES_ALL` + * itself is JVM-only and breaks the Kotlin/Native build. + */ + private val STRIPPED_ELEMENT_PATTERNS: List = + STRIPPED_ELEMENTS.map { name -> + // Escape the name in case it contains regex metacharacters (e.g. __video). + val escaped = Regex.escape(name) + // Matches: + // + // + // ...content... + Regex("""(?s)<$escaped(?:\s[^>]*?)?/>|<$escaped(?:\s[^>]*?)?>.*?""") + } + + /** Matches whitespace between tags: `> \n <` → `><`. */ + private val INTER_TAG_WHITESPACE = Regex(""">\s+<""") + + /** Collapse leading / trailing whitespace across the whole fragment. */ + private val EDGE_WHITESPACE = Regex("""^\s+|\s+$""") + + /** + * Strip bloat elements and normalize whitespace on an inner `` fragment. + * + * The input is assumed to be the concatenated children of `` — i.e., what + * [CoTXmlParser.extractDetailInnerXml] returns. It is NOT the full `` or + * the `` wrapper itself. + * + * Returns an empty string if every element was stripped (so callers can treat + * "empty" and "nothing worth sending" uniformly). + */ + fun strip(detailInnerXml: String): String { + if (detailInnerXml.isEmpty()) return "" + var result = detailInnerXml + for (pattern in STRIPPED_ELEMENT_PATTERNS) { + result = pattern.replace(result, "") + } + // Collapse whitespace between remaining tags. Preserves whitespace inside + // text nodes (e.g. hello world) because that whitespace + // isn't bracketed by '>' and '<'. + result = INTER_TAG_WHITESPACE.replace(result, "><") + result = EDGE_WHITESPACE.replace(result, "") + return result + } +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXml.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXml.kt index cd616417d..662f430fa 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXml.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXml.kt @@ -20,10 +20,19 @@ package org.meshtastic.core.takserver import kotlin.time.Instant +/** + * Serialize this [CoTMessage] to a single `` XML element suitable for the CoT streaming + * TCP protocol used by ATAK / iTAK / WinTAK clients. + * + * **Important:** the output must NOT include an `` declaration. The CoT stream + * protocol is a continuous sequence of `` elements concatenated together; an XML + * declaration is only legal at the very start of a document and ATAK will drop the connection + * as malformed the moment it sees a second declaration mid-stream. + */ fun CoTMessage.toXml(): String { val sb = StringBuilder() sb.append( - "", + "", ) contact?.let { @@ -63,4 +72,19 @@ fun CoTMessage.toXml(): String { return sb.toString() } -private fun Instant.toXmlString(): String = this.toString() +/** + * Format this [Instant] for CoT XML `time` / `start` / `stale` attributes. + * + * Always emits millisecond precision (`YYYY-MM-DDThh:mm:ss.SSSZ`). kotlinx-datetime's default + * [Instant.toString] can emit up to nanosecond precision; some TAK implementations choke on + * anything beyond milliseconds, so we truncate to ms and always include the millisecond field + * even when it would otherwise be zero. + */ +private fun Instant.toXmlString(): String { + val millis = this.toEpochMilliseconds() + val truncated = Instant.fromEpochMilliseconds(millis) + val base = truncated.toString() + // kotlinx-datetime omits the fractional part when it's zero; pad it ourselves so the + // CoT timestamp format is stable at ms precision. + return if (base.contains('.')) base else base.removeSuffix("Z") + ".000Z" +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt index 41d5f78dd..04c83b693 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/CoTXmlParser.kt @@ -59,9 +59,38 @@ class CoTXmlParser(private val xml: String) { track = detail?.track?.let { CoTTrack(speed = it.speed, course = it.course) }, chat = buildChat(detail), remarks = buildRemarks(detail), + // Stripped version used as the raw_detail protobuf payload: drops bloat + // elements (colors, icons, archives, shapes, etc.) so unmapped CoT types + // have any chance of fitting in a LoRa mesh packet. See [CoTDetailStripper]. + parsedDetailXml = extractDetailInnerXml(xml)?.let(CoTDetailStripper::strip), + // Verbatim original event XML kept for diagnostic logging only — never + // goes on the wire. + sourceEventXml = xml, ) } + /** + * Extract the exact content between `` and `` from the original XML + * string. Used as the `raw_detail` fallback payload when we can't map the CoT type to + * a structured [org.meshtastic.proto.TAKPacketV2] payload. Preserves any extension + * elements the xmlutil parser discarded as "unknown children". + * + * Returns null for self-closed `` or when no detail element is present. + */ + private fun extractDetailInnerXml(xml: String): String? { + // Match `` (not ``) through its matching close tag. + val openIdx = xml.indexOf("', openIdx) + if (openEnd < 0) return null + // Self-closed tag like `` has no content. + if (xml[openEnd - 1] == '/') return null + val closeIdx = xml.indexOf("", openEnd) + if (closeIdx < 0) return null + val inner = xml.substring(openEnd + 1, closeIdx).trim() + return inner.ifEmpty { null } + } + private fun buildContact(detail: CoTDetailXml?): CoTContact? = detail?.contact?.let { if (it.callsign.isNotEmpty() || it.endpoint != null || it.phone != null) { CoTContact(callsign = it.callsign, endpoint = it.endpoint, phone = it.phone) diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/RouteDataPackageGenerator.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/RouteDataPackageGenerator.kt new file mode 100644 index 000000000..23913f85d --- /dev/null +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/RouteDataPackageGenerator.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +/** + * Converts route CoT XML (b-m-r) into ATAK-importable KML data packages. + * + * ATAK silently ignores route CoT events received over TCP streaming + * connections — it only accepts routes from KML/GPX file import, TAK Server + * mission sync, or data packages auto-imported from the monitored directory + * `/sdcard/atak/tools/datapackage/`. This generator bridges the gap by + * extracting waypoints from the SDK-reconstructed route XML and packaging + * them as a KML LineString inside a MissionPackageManifest v2 zip. + */ +object RouteDataPackageGenerator { + + private val EVENT_UID_RE = Regex("""]*\buid="([^"]*)"""") + private val CONTACT_CALLSIGN_RE = Regex("""]*\bcallsign="([^"]*)"""") + private val LINK_POINT_RE = Regex("""]*\bpoint="([^"]*)"[^>]*/>""") + + data class RouteKmlResult( + val kml: String, + val routeUid: String, + val routeName: String, + ) + + /** + * Extract waypoints from route CoT XML and generate a KML LineString. + * Returns null if fewer than 2 waypoints are found. + */ + fun generateKml(routeXml: String): RouteKmlResult? { + val uid = EVENT_UID_RE.find(routeXml)?.groupValues?.getOrNull(1) ?: return null + val name = CONTACT_CALLSIGN_RE.find(routeXml)?.groupValues?.getOrNull(1) ?: "Mesh Route" + + // Extract all waypoint coordinates from elements + val waypoints = LINK_POINT_RE.findAll(routeXml).mapNotNull { match -> + val point = match.groupValues[1] // "lat,lon,hae" or "lat,lon" + val parts = point.split(",").map { it.trim() } + if (parts.size >= 2) { + val lat = parts[0] + val lon = parts[1] + val hae = parts.getOrElse(2) { "0" } + // KML coordinate order is lon,lat,hae (opposite of CoT's lat,lon,hae) + "$lon,$lat,$hae" + } else null + }.toList() + + if (waypoints.size < 2) return null + + val kml = buildString { + appendLine("""""") + appendLine("""""") + appendLine(" ") + appendLine(" ${name.xmlEscaped()}") + appendLine(" ") + appendLine(" ${name.xmlEscaped()}") + appendLine(" ") + appendLine(" ") + appendLine(" ") + for (coord in waypoints) { + appendLine(" $coord") + } + appendLine(" ") + appendLine(" ") + appendLine(" ") + appendLine(" ") + append("") + } + + return RouteKmlResult(kml = kml, routeUid = uid, routeName = name) + } + + /** + * Generate a complete ATAK data package (zip) containing the route as KML. + * Returns (fileName, zipBytes) or null if the route XML can't be parsed. + */ + fun generateDataPackage(routeXml: String): Pair? { + val result = generateKml(routeXml) ?: return null + val kmlFileName = "${result.routeUid}.kml" + val zipFileName = "${result.routeUid}.zip" + + val manifest = buildString { + appendLine("""""") + appendLine(" ") + appendLine(""" """) + appendLine(""" """) + appendLine(""" """) + appendLine(" ") + appendLine(" ") + appendLine(""" """) + appendLine(" ") + append("") + } + + val zipBytes = ZipArchiver.createZip( + mapOf( + kmlFileName to result.kml.encodeToByteArray(), + "manifest.xml" to manifest.encodeToByteArray(), + ), + ) + + return zipFileName to zipBytes + } + + private fun String.xmlEscaped(): String = replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt deleted file mode 100644 index 9a24d6721..000000000 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("TooManyFunctions", "TooGenericExceptionCaught") - -package org.meshtastic.core.takserver - -import co.touchlab.kermit.Logger -import io.ktor.network.sockets.Socket -import io.ktor.network.sockets.isClosed -import io.ktor.network.sockets.openReadChannel -import io.ktor.network.sockets.openWriteChannel -import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.ByteWriteChannel -import io.ktor.utils.io.readAvailable -import io.ktor.utils.io.writeStringUtf8 -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlin.concurrent.Volatile -import kotlin.random.Random -import kotlin.time.Clock -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Instant -import kotlinx.coroutines.isActive as coroutineIsActive - -class TAKClientConnection( - private val socket: Socket, - val clientInfo: TAKClientInfo, - private val onEvent: (TAKConnectionEvent) -> Unit, - private val scope: CoroutineScope, -) { - private var currentClientInfo = clientInfo - private val frameBuffer = CoTXmlFrameBuffer() - - private val readChannel: ByteReadChannel = socket.openReadChannel() - private val writeChannel: ByteWriteChannel = socket.openWriteChannel(autoFlush = true) - private val writeMutex = Mutex() - - /** Guards against emitting [TAKConnectionEvent.Disconnected] more than once. */ - @Volatile private var disconnectedEmitted = false - - fun start() { - onEvent(TAKConnectionEvent.Connected(currentClientInfo)) - sendProtocolSupport() - - scope.launch { readLoop() } - - scope.launch { keepaliveLoop() } - } - - private fun sendProtocolSupport() { - val serverUid = "Meshtastic-TAK-Server-${Random.nextInt().toString(TAK_HEX_RADIX)}" - val now = Clock.System.now() - val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds - val detail = - """ - - - - """ - .trimIndent() - sendXml(buildEventXml(uid = serverUid, type = "t-x-takp-v", now = now, stale = stale, detail = detail)) - } - - private suspend fun readLoop() { - try { - val buffer = ByteArray(TAK_XML_READ_BUFFER_SIZE) - while (scope.coroutineIsActive && !socket.isClosed) { - // Suspend until data is available — no polling delay needed - readChannel.awaitContent() - val bytesRead = readChannel.readAvailable(buffer) - if (bytesRead > 0) { - processReceivedData(buffer.copyOfRange(0, bytesRead)) - } else if (bytesRead == -1) { - break // EOF - } - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.w(e) { "TAK client read error: ${currentClientInfo.id}" } - emitDisconnected(TAKConnectionEvent.Error(e)) - return - } - emitDisconnected(TAKConnectionEvent.Disconnected) - } - - private suspend fun keepaliveLoop() { - while (scope.coroutineIsActive && !socket.isClosed) { - kotlinx.coroutines.delay(TAK_KEEPALIVE_INTERVAL_MS) - sendKeepalive() - } - } - - private fun sendKeepalive() { - val now = Clock.System.now() - val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds - sendXml(buildEventXml(uid = "takPong", type = "t-x-d-d", now = now, stale = stale, detail = "")) - } - - private fun processReceivedData(newData: ByteArray) { - // frameBuffer.append returns List — pass directly without re-encoding - frameBuffer.append(newData).forEach { xmlString -> parseAndHandleMessage(xmlString) } - } - - private fun parseAndHandleMessage(xmlString: String) { - // Parse first, then filter on the structured type field to avoid false positives - val parser = CoTXmlParser(xmlString) - val result = parser.parse() - - result.onSuccess { cotMessage -> - when { - cotMessage.type.startsWith("t-x-takp") -> { - handleProtocolControl(cotMessage.type, xmlString) - return - } - cotMessage.type == "t-x-c-t" || cotMessage.uid == "ping" -> { - // Keepalive / ping — discard silently - return - } - else -> { - cotMessage.contact?.let { contact -> - val updatedClientInfo = - currentClientInfo.copy( - callsign = currentClientInfo.callsign ?: contact.callsign, - uid = currentClientInfo.uid ?: cotMessage.uid, - ) - if (updatedClientInfo != currentClientInfo) { - currentClientInfo = updatedClientInfo - onEvent(TAKConnectionEvent.ClientInfoUpdated(updatedClientInfo)) - } - } - - onEvent(TAKConnectionEvent.Message(cotMessage)) - } - } - } - } - - private fun handleProtocolControl(type: String, xmlString: String) { - if (type == "t-x-takp-q") { - sendProtocolResponse() - } else { - Logger.d { "Unhandled protocol control type: $type (raw=$xmlString)" } - } - } - - private fun sendProtocolResponse() { - val serverUid = "Meshtastic-TAK-Server-${Random.nextInt().toString(TAK_HEX_RADIX)}" - val now = Clock.System.now() - val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds - val detail = - """ - - - - """ - .trimIndent() - sendXml(buildEventXml(uid = serverUid, type = "t-x-takp-r", now = now, stale = stale, detail = detail)) - } - - fun send(cotMessage: CoTMessage) { - val xml = cotMessage.toXml() - sendXml(xml) - } - - private fun buildEventXml(uid: String, type: String, now: Instant, stale: Instant, detail: String): String { - val detailContent = if (detail.isBlank()) "" else "$detail" - val point = """""" - return """""" + - point + - detailContent + - "" - } - - private fun sendXml(xml: String) { - scope.launch { - try { - writeMutex.withLock { - if (!socket.isClosed) { - writeChannel.writeStringUtf8(xml) - } - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.w(e) { "TAK client send error: ${currentClientInfo.id}" } - } - } - } - - fun close() { - frameBuffer.clear() - try { - socket.close() - } catch (e: Exception) { - Logger.w(e) { "Error closing TAK client socket: ${currentClientInfo.id}" } - } - emitDisconnected(TAKConnectionEvent.Disconnected) - } - - /** - * Emits [event] (expected to be [TAKConnectionEvent.Disconnected] or [TAKConnectionEvent.Error]) at most once - * across all code paths. - */ - private fun emitDisconnected(event: TAKConnectionEvent) { - if (!disconnectedEmitted) { - disconnectedEmitted = true - onEvent(event) - } - } -} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDataPackageGenerator.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDataPackageGenerator.kt index e9a7ae668..b0e2645eb 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDataPackageGenerator.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDataPackageGenerator.kt @@ -21,17 +21,28 @@ import nl.adaptivity.xmlutil.serialization.XML import kotlin.uuid.Uuid /** - * Generates TAK data packages (.zip) compatible with ATAK/iTAK import. + * Generates TAK data packages (.zip) compatible with ATAK/iTAK/WinTAK import. * * The data package follows the MissionPackageManifest v2 format: * ``` * Meshtastic_TAK_Server.zip * ├── meshtastic-server.pref (ATAK connection preferences) + * ├── truststore.p12 (server cert — matches iOS "truststore.p12") + * ├── client.p12 (client identity for mTLS) * └── manifest.xml (MissionPackageManifest v2) * ``` + * + * The bundled certificates / password match Meshtastic-Apple so a single + * exported package works on both ATAK (Android) and iTAK (iOS) without + * reconfiguration. + * + * Override [bundledCertBytesProvider] in tests to avoid touching the real classpath + * resources. In production the default reads from [TakCertLoader]. */ object TAKDataPackageGenerator { private const val PREF_FILE_NAME = "meshtastic-server.pref" + private const val TRUSTSTORE_FILE_NAME = "truststore.p12" + private const val CLIENT_P12_FILE_NAME = "client.p12" private const val PACKAGE_NAME = "Meshtastic_TAK_Server" private val xmlSerializer = XML { @@ -39,24 +50,38 @@ object TAKDataPackageGenerator { indentString = " " } + /** + * Platform-specific hook for reading the bundled TLS certificate bytes. Default + * implementation lives in `jvmAndroidMain` and reads them from classpath resources + * via [TakCertLoader]. + */ + var bundledCertBytesProvider: BundledCertBytesProvider = DefaultBundledCertBytesProvider + /** * Generate a complete TAK data package zip. * + * @param useTls when true, package includes `truststore.p12` + `client.p12` and + * the pref file uses `ssl`; when false, package is TCP-only (legacy). + * * @return zip file contents as a [ByteArray] */ fun generateDataPackage( serverHost: String = "127.0.0.1", port: Int = DEFAULT_TAK_PORT, + useTls: Boolean = true, description: String = "Meshtastic TAK Server", ): ByteArray { - val prefContent = generateConfigPref(serverHost, port, description) - val manifestContent = generateManifest(uid = Uuid.random().toString(), description = description) + val prefContent = generateConfigPref(serverHost, port, useTls, description) + val manifestContent = generateManifest(uid = Uuid.random().toString(), description = description, useTls = useTls) - val entries = - mapOf( - PREF_FILE_NAME to prefContent.encodeToByteArray(), - "manifest.xml" to manifestContent.encodeToByteArray(), - ) + val entries = mutableMapOf() + entries[PREF_FILE_NAME] = prefContent.encodeToByteArray() + entries["manifest.xml"] = manifestContent.encodeToByteArray() + + if (useTls) { + bundledCertBytesProvider.serverP12Bytes()?.let { entries[TRUSTSTORE_FILE_NAME] = it } + bundledCertBytesProvider.clientP12Bytes()?.let { entries[CLIENT_P12_FILE_NAME] = it } + } return ZipArchiver.createZip(entries) } @@ -64,31 +89,89 @@ object TAKDataPackageGenerator { internal fun generateConfigPref( serverHost: String = "127.0.0.1", port: Int = DEFAULT_TAK_PORT, + useTls: Boolean = true, description: String = "Meshtastic TAK Server", ): String { - val prefs = + val protocolType = if (useTls) "ssl" else "tcp" + val prefs = if (useTls) { + // TLS / mTLS mode — matches the iOS data package format exactly. TAKPreferencesXml( - preferences = - listOf( + preferences = listOf( TAKPreferenceXml( version = "1", name = "cot_streams", - entries = - listOf( + entries = listOf( TAKEntryXml("count", "class java.lang.Integer", "1"), TAKEntryXml("description0", "class java.lang.String", description), TAKEntryXml("enabled0", "class java.lang.Boolean", "true"), - TAKEntryXml("connectString0", "class java.lang.String", "$serverHost:$port:tcp"), + TAKEntryXml( + "connectString0", + "class java.lang.String", + "$serverHost:$port:$protocolType", + ), ), ), TAKPreferenceXml( version = "1", name = "com.atakmap.app_preferences", - entries = - listOf(TAKEntryXml("displayServerConnectionWidget", "class java.lang.Boolean", "true")), + entries = listOf( + TAKEntryXml( + "displayServerConnectionWidget", + "class java.lang.Boolean", + "true", + ), + TAKEntryXml( + "caLocation", + "class java.lang.String", + "cert/$TRUSTSTORE_FILE_NAME", + ), + TAKEntryXml( + "caPassword", + "class java.lang.String", + TAK_BUNDLED_CERT_PASSWORD, + ), + TAKEntryXml( + "certificateLocation", + "class java.lang.String", + "cert/$CLIENT_P12_FILE_NAME", + ), + TAKEntryXml( + "clientPassword", + "class java.lang.String", + TAK_BUNDLED_CERT_PASSWORD, + ), + ), ), ), ) + } else { + // Legacy plain-TCP mode (not used in production, kept for tests / fallback) + TAKPreferencesXml( + preferences = listOf( + TAKPreferenceXml( + version = "1", + name = "cot_streams", + entries = listOf( + TAKEntryXml("count", "class java.lang.Integer", "1"), + TAKEntryXml("description0", "class java.lang.String", description), + TAKEntryXml("enabled0", "class java.lang.Boolean", "true"), + TAKEntryXml( + "connectString0", + "class java.lang.String", + "$serverHost:$port:$protocolType", + ), + ), + ), + TAKPreferenceXml( + version = "1", + name = "com.atakmap.app_preferences", + entries = listOf( + TAKEntryXml("displayServerConnectionWidget", "class java.lang.Boolean", "true"), + ), + ), + ), + ) + } return xmlSerializer .encodeToString(TAKPreferencesXml.serializer(), prefs) @@ -98,7 +181,11 @@ object TAKDataPackageGenerator { ) } - internal fun generateManifest(uid: String, description: String = "Meshtastic TAK Server"): String = buildString { + internal fun generateManifest( + uid: String, + description: String = "Meshtastic TAK Server", + useTls: Boolean = true, + ): String = buildString { appendLine("""""") appendLine(" ") appendLine(""" """) @@ -107,7 +194,30 @@ object TAKDataPackageGenerator { appendLine(" ") appendLine(" ") appendLine(""" """) + if (useTls) { + appendLine(""" """) + appendLine(""" """) + } appendLine(" ") append("") } } + +/** + * Supplies the bundled server / client PKCS#12 bytes for [TAKDataPackageGenerator]. + * Platform implementations live in `jvmAndroidMain`. + */ +interface BundledCertBytesProvider { + fun serverP12Bytes(): ByteArray? + fun clientP12Bytes(): ByteArray? +} + +/** + * Default provider that returns `null` on platforms without a real implementation. + * Overridden at startup on JVM / Android by pointing + * [TAKDataPackageGenerator.bundledCertBytesProvider] at [TakCertLoader]. + */ +private object DefaultBundledCertBytesProvider : BundledCertBytesProvider { + override fun serverP12Bytes(): ByteArray? = null + override fun clientP12Bytes(): ByteArray? = null +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDefaults.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDefaults.kt index eef798bf9..e899c1dcd 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDefaults.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKDefaults.kt @@ -20,20 +20,59 @@ import org.meshtastic.proto.MemberRole import org.meshtastic.proto.Team import org.meshtastic.proto.User -internal const val DEFAULT_TAK_PORT = 8087 +// Port 8089 is the standard TAK TLS port. Matches the iOS implementation so that +// a single exported data package (containing truststore.p12 + client.p12) works for +// both Meshtastic-iOS and Meshtastic-Android without reconfiguration in ATAK/iTAK. +internal const val DEFAULT_TAK_PORT = 8089 internal const val DEFAULT_TAK_ENDPOINT = "0.0.0.0:4242:tcp" + +// Bundled certificate password — matches iOS (`"meshtastic"`). Used for the +// server.p12 / client.p12 PKCS#12 files shipped under `tak_certs/` on the classpath. +internal const val TAK_BUNDLED_CERT_PASSWORD = "meshtastic" internal const val DEFAULT_TAK_TEAM_NAME = "Cyan" internal const val DEFAULT_TAK_ROLE_NAME = "Team Member" internal const val DEFAULT_TAK_BATTERY = 100 internal const val DEFAULT_TAK_STALE_MINUTES = 10 internal const val TAK_HEX_RADIX = 16 internal const val TAK_XML_READ_BUFFER_SIZE = 4_096 -internal const val TAK_KEEPALIVE_INTERVAL_MS = 30_000L +// ATAK's native commo library declares the connection dead after 25 seconds of +// silence (RX_TIMEOUT_SECONDS in streamingsocketmanagement.cpp) and starts +// sending t-x-c-t pings at 15 seconds (RX_STALE_SECONDS). Send keepalives +// well under the 15-second threshold so ATAK never enters its stale phase. +internal const val TAK_KEEPALIVE_INTERVAL_MS = 10_000L internal const val TAK_ACCEPT_LOOP_DELAY_MS = 100L internal const val TAK_COORDINATE_SCALE = 1e7 internal const val TAK_UNKNOWN_POINT_VALUE = 9_999_999.0 internal const val TAK_DIRECT_MESSAGE_PARTS_MIN = 3 +/** + * Hard cap on the size of a TAK v2 wire payload we will hand to the mesh layer. + * + * `CommandSenderImpl.sendData` checks `Data.ADAPTER.isWithinSizeLimit(data, + * Constants.DATA_PAYLOAD_LEN.value)` where `DATA_PAYLOAD_LEN = 233`. That 233 applies + * to the ENTIRE encoded `Data` proto (portnum tag + payload length-delim + reply_id + + * emoji), not just the `payload` bytes. The wrapper for a port-78 (`ATAK_PLUGIN_V2`) + * message costs roughly: + * * portnum varint + tag: 2 bytes + * * payload length prefix + tag: 2–3 bytes (depending on size) + * * reply_id / emoji: 0 bytes when unset + * + * That leaves ~228 bytes for the `payload` field alone. We use 225 to keep a small + * margin for future proto evolution. Anything larger than this is dropped in + * [TAKMeshIntegration.sendCoTToMesh] rather than being handed to the mesh layer, + * because the mesh layer would throw and the outer `SharedFlow` collector would eat + * the crash on every subsequent emission. + */ +internal const val MAX_TAK_WIRE_PAYLOAD_BYTES = 225 + +/** + * Max characters of raw CoT XML we'll write to logcat when dropping an oversized + * packet. ATAK can emit events several KB long; logging the whole thing floods + * logcat and buries the signal. 1024 chars is enough to see the event type, point, + * and the first few detail elements. + */ +internal const val TAK_LOG_XML_MAX_CHARS = 1_024 + internal fun Team?.toTakTeamName(): String = when (this) { null, Team.Unspecifed_Color, diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt index 4f3001427..ac231efbd 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt @@ -24,31 +24,37 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest + import kotlinx.coroutines.launch import okio.ByteString.Companion.toByteString import org.meshtastic.core.model.DataPacket import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshConfigHandler -import org.meshtastic.core.repository.NodeRepository + import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.takserver.TAKPacketConversion.toCoTMessage -import org.meshtastic.core.takserver.TAKPacketConversion.toTAKPacket -import org.meshtastic.core.takserver.fountain.CoTHandler +import org.meshtastic.core.takserver.TAKPacketV2Conversion.toCoTMessage +import org.meshtastic.core.takserver.TAKPacketV2Conversion.toTAKPacketV2 import org.meshtastic.proto.MemberRole import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.PortNum import org.meshtastic.proto.TAKPacket import org.meshtastic.proto.Team import kotlin.concurrent.Volatile +import kotlin.time.Clock +import kotlin.time.Duration.Companion.minutes +/** + * Bidirectional bridge between the local TAK server and the Meshtastic mesh network. + * + * V2 protocol only: All traffic uses port 78 (ATAK_PLUGIN_V2). + * Legacy V1 port 72 is still received for backward compatibility but will be removed. + */ class TAKMeshIntegration( private val takServerManager: TAKServerManager, private val commandSender: CommandSender, - private val nodeRepository: NodeRepository, + private val serviceRepository: ServiceRepository, private val meshConfigHandler: MeshConfigHandler, - private val cotHandler: CoTHandler, ) { @Volatile private var isRunning = false private val jobs = mutableListOf() @@ -61,103 +67,363 @@ class TAKMeshIntegration( takServerManager.start(scope) - val newJobs = - listOf( - // Forward incoming CoT from TAK clients to mesh - scope.launch { takServerManager.inboundMessages.collect { cotMessage -> sendCoTToMesh(cotMessage) } }, + val newJobs = listOf( + // Forward incoming CoT from TAK clients to mesh + scope.launch { + takServerManager.inboundMessages.collect { (cotMessage, clientInfo) -> + // Enrich GeoChat messages with the originating TAK client's + // callsign when the message itself lacks one. This only applies + // to messages FROM the connected TAK client — mesh-originated + // messages flow through handleMeshPacket() instead. + val enriched = if (cotMessage.type == "b-t-f" && + cotMessage.contact?.callsign.isNullOrEmpty() && + clientInfo?.callsign != null + ) { + cotMessage.copy( + contact = (cotMessage.contact ?: CoTContact(callsign = "")) + .copy(callsign = clientInfo.callsign) + ) + } else { + cotMessage + } + sendCoTToMesh(enriched) + } + }, - // Forward incoming ATAK packets from mesh to TAK clients - scope.launch { - serviceRepository.meshPacketFlow - .filter { - it.decoded?.portnum == PortNum.ATAK_PLUGIN || it.decoded?.portnum == PortNum.ATAK_FORWARDER - } - .collect { packet -> handleMeshPacket(packet) } - }, + // Forward incoming ATAK packets from mesh to TAK clients + scope.launch { + serviceRepository.meshPacketFlow + .filter { + it.decoded?.portnum == PortNum.ATAK_PLUGIN_V2 || + it.decoded?.portnum == PortNum.ATAK_PLUGIN + } + .collect { packet -> handleMeshPacket(packet) } + }, - // Broadcast node positions to TAK clients. - // mapLatest cancels any in-flight broadcast loop when a new node-map emission arrives, - // preventing N×M fan-out from stacking up across rapid consecutive updates. - scope.launch { - nodeRepository.nodeDBbyNum - .mapLatest { nodes -> - nodes.forEach { (_, node) -> - takServerManager.broadcastNode( - node = node, - team = currentTeam.toTakTeamName(), - role = currentRole.toTakRoleName(), - ) - } - } - .collect {} - }, - scope.launch { - meshConfigHandler.moduleConfig - .map { it.tak } - .distinctUntilChanged() - .collect { takConfig -> - currentTeam = takConfig?.team ?: Team.Unspecifed_Color - currentRole = takConfig?.role ?: MemberRole.Unspecifed - } - }, - ) + // Track TAK config changes + scope.launch { + meshConfigHandler.moduleConfig + .map { it.tak } + .distinctUntilChanged() + .collect { takConfig -> + currentTeam = takConfig?.team ?: Team.Unspecifed_Color + currentRole = takConfig?.role ?: MemberRole.Unspecifed + } + }, + ) jobs.addAll(newJobs) - - Logger.i { "TAK Mesh Integration started" } + Logger.i { "TAK Mesh Integration started (v2 protocol)" } } fun stop() { if (!isRunning) return isRunning = false - // Cancel all tracked jobs and clear the list - val toCancel: List - toCancel = jobs.toList() + val toCancel = jobs.toList() jobs.clear() toCancel.forEach(Job::cancel) takServerManager.stop() Logger.i { "TAK Mesh Integration stopped" } } + // ── Send: TAK client → mesh ───────────────────────────────────────────── + private suspend fun sendCoTToMesh(cotMessage: CoTMessage) { - val takPacket = cotMessage.toTAKPacket() - if (takPacket == null) { - cotHandler.sendGenericCoT(cotMessage) - return + // Prefer the sourceEventXml for shape/marker/route types — the SDK's + // CotXmlParser extracts compact typed payloads (DrawnShape, Marker, + // Route, etc.) that compress far better than raw_detail encoding. + // For PLI and GeoChat, use the enriched CoTMessage (which may have + // had callsign/contact injected by the upstream enrichment step). + val rawXml = cotMessage.sourceEventXml ?: cotMessage.toXml() + // Extend stale for static objects (routes, shapes, markers) that may + // arrive over LoRa mesh past their original TTL. iTAK uses 2-min stale + // for routes; ATAK uses 24h. 5 min ensures it survives mesh delivery. + val freshXml = ensureMinimumStaleForMesh(rawXml) + // Strip non-essential elements before compression to save wire bytes + val xml = stripNonEssentialElements(freshXml) + + Logger.d { "RAW CoT OUT (mesh, ${cotMessage.type}): $rawXml" } + + // Route through the SDK parser/compressor which handles all typed + // payloads (DrawnShape, Marker, Route, Aircraft, etc.) with compact + // proto fields instead of raw_detail XML. Falls back to the app's + // own conversion only if the SDK path fails. + // + // compressWithRemarksFallback preserves text when the + // compressed packet fits under the LoRa MTU, and strips remarks + // automatically if needed to fit. Returns null if even without + // remarks the packet exceeds the limit. + val wirePayload: ByteArray = try { + val sdkParser = org.meshtastic.tak.CotXmlParser() + val sdkData = sdkParser.parse(xml) + val compressor = org.meshtastic.tak.TakCompressor() + compressor.compressWithRemarksFallback(sdkData, MAX_TAK_WIRE_PAYLOAD_BYTES) ?: run { + Logger.w { + buildString { + append("Dropping oversized TAK packet: type=${cotMessage.type} max=$MAX_TAK_WIRE_PAYLOAD_BYTES") + cotMessage.sourceEventXml?.let { src -> + append('\n') + append("Source CoT event: ") + append(if (src.length <= TAK_LOG_XML_MAX_CHARS) src else src.take(TAK_LOG_XML_MAX_CHARS) + "…") + } + } + } + return + } + } catch (e: Throwable) { + Logger.d(e) { "SDK parser/compressor failed for ${cotMessage.type}, trying app conversion" } + val takPacketV2 = cotMessage.toTAKPacketV2() + if (takPacketV2 == null) { + Logger.w { "Cannot convert CoT type ${cotMessage.type} to TAKPacketV2, dropping" } + return + } + try { + TakV2Compressor.compress(takPacketV2) + } catch (e2: Throwable) { + Logger.w(e2) { "V2 compression failed for ${cotMessage.type}, using uncompressed wire format" } + encodeUncompressed(takPacketV2) + } } - val payload = TAKPacket.ADAPTER.encode(takPacket) - - val dataPacket = - DataPacket( + try { + val dataPacket = DataPacket( to = DataPacket.ID_BROADCAST, - bytes = payload.toByteString(), - dataType = PortNum.ATAK_PLUGIN.value, + bytes = wirePayload.toByteString(), + dataType = PortNum.ATAK_PLUGIN_V2.value, ) - - commandSender.sendData(dataPacket) - Logger.d { "Forwarded CoT to mesh as TAKPacket: ${cotMessage.type}" } + commandSender.sendData(dataPacket) + Logger.d { "Sent V2 to mesh: ${cotMessage.type} (${wirePayload.size} bytes)" } + } catch (e: Throwable) { + // Something other than size — radio not connected, queue full, etc. + Logger.e(e) { "Failed to send TAKPacketV2 to mesh (${cotMessage.type}, ${wirePayload.size} bytes): ${e.message}" } + } } + /** + * Wrap a [org.meshtastic.proto.TAKPacketV2] into the uncompressed v2 wire format: + * `[0xFF flag byte][raw protobuf]`. Used as a fallback when the zstd native lib + * isn't loaded. + */ + private fun encodeUncompressed(takPacketV2: org.meshtastic.proto.TAKPacketV2): ByteArray { + val protoBytes = org.meshtastic.proto.TAKPacketV2.ADAPTER.encode(takPacketV2) + val out = ByteArray(1 + protoBytes.size) + out[0] = TakV2Compressor.DICT_ID_UNCOMPRESSED.toByte() + protoBytes.copyInto(out, 1) + return out + } + + // ── Receive: mesh → TAK client ────────────────────────────────────────── + private suspend fun handleMeshPacket(packet: MeshPacket) { val payload = packet.decoded?.payload ?: return - if (packet.decoded?.portnum == PortNum.ATAK_FORWARDER) { - cotHandler.handleIncomingForwarderPacket(payload.toByteArray(), packet.from) - return + when (packet.decoded?.portnum) { + PortNum.ATAK_PLUGIN_V2 -> handleV2Packet(payload.toByteArray()) + PortNum.ATAK_PLUGIN -> handleV1Packet(payload) + else -> return + } + } + + private suspend fun handleV2Packet(wirePayload: ByteArray) { + try { + // Decompress to CoT XML via the SDK's CotXmlBuilder, which handles + // ALL typed payloads (DrawnShape, Marker, Route, etc.) and preserves + // shape detail elements (vertices, colors, stroke weight) that the + // app's own CoTXmlParser would strip. Forward the SDK-generated XML + // directly to TAK clients without re-parsing. + val rawXml = TakV2Compressor.decompressToXml(wirePayload) + // Strip the XML declaration and collapse whitespace — ATAK's TCP + // streaming parser expects bare ... on a single + // line, not a formatted XML document with prologue. + val xml = rawXml + .replace("""""", "") + .replace(Regex("""\s*\n\s*"""), "") + .trim() + Logger.d { "RAW CoT IN (mesh): $xml" } + // Routes: ATAK ignores b-m-r CoT events over TCP streaming. + // Convert to a KML data package and write to ATAK's auto-import dir. + if (xml.contains("""type="b-m-r"""")) { + try { + val pkg = RouteDataPackageGenerator.generateDataPackage(xml) + if (pkg != null) { + val (fileName, zipBytes) = pkg + AtakFileWriter.writeToImportDir(fileName, zipBytes) + } else { + Logger.w { "Route data package generation failed — not enough waypoints?" } + } + } catch (e2: Throwable) { + Logger.w(e2) { "Route data package write failed: ${e2.message}" } + } + } + takServerManager.broadcastRawXml(xml) + Logger.d { "V2 → TAK clients (raw XML)" } + } catch (e: Throwable) { + Logger.w(e) { "Failed to handle V2 packet: ${e.message}" } + } + } + + /** Backward compat for legacy V1 devices. Will be removed. */ + private suspend fun handleV1Packet(payload: okio.ByteString) { + try { + val takPacket = TAKPacket.ADAPTER.decode(payload) + val cotMessage = convertV1ToCoT(takPacket) ?: return + takServerManager.broadcast(cotMessage) + Logger.d { "V1 → TAK clients: ${cotMessage.type}" } + } catch (e: Throwable) { + Logger.w(e) { "Failed to handle V1 packet: ${e.message}" } + } + } + + private fun convertV1ToCoT(takPacket: TAKPacket): CoTMessage? { + val callsign = takPacket.contact?.callsign ?: "UNKNOWN" + val senderUid = takPacket.contact?.device_callsign ?: "unknown" + val teamName = takPacket.group?.team?.toTakTeamName() ?: DEFAULT_TAK_TEAM_NAME + val roleName = takPacket.group?.role?.toTakRoleName() ?: DEFAULT_TAK_ROLE_NAME + val battery = takPacket.status?.battery ?: DEFAULT_TAK_BATTERY + + val pli = takPacket.pli + if (pli != null) { + return CoTMessage.pli( + uid = senderUid, + callsign = callsign, + latitude = pli.latitude_i.toDouble() / TAK_COORDINATE_SCALE, + longitude = pli.longitude_i.toDouble() / TAK_COORDINATE_SCALE, + altitude = pli.altitude.toDouble(), + speed = pli.speed.toDouble(), + course = pli.course.toDouble(), + team = teamName, + role = roleName, + battery = battery, + staleMinutes = DEFAULT_TAK_STALE_MINUTES, + ) } - val takPacket = - try { - TAKPacket.ADAPTER.decode(payload) - } catch (e: Exception) { - Logger.w(e) { "Failed to decode TAKPacket from mesh" } - return + val chat = takPacket.chat + if (chat != null) { + val timeNow = Clock.System.now() + return CoTMessage( + uid = "GeoChat.$senderUid.All Chat Rooms", + type = "b-t-f", + how = "h-g-i-g-o", + time = timeNow, + start = timeNow, + stale = timeNow + DEFAULT_TAK_STALE_MINUTES.minutes, + latitude = 0.0, + longitude = 0.0, + contact = CoTContact(callsign = callsign, endpoint = DEFAULT_TAK_ENDPOINT), + group = CoTGroup(name = teamName, role = roleName), + status = CoTStatus(battery = battery), + chat = CoTChat( + chatroom = chat.to ?: "All Chat Rooms", + senderCallsign = callsign, + message = chat.message, + ), + ) + } + + return null + } + + companion object { + /** + * Minimum stale TTL (5 min) for static CoT types sent over mesh. + * iTAK uses 2-min stale for routes/shapes; over LoRa mesh with + * multi-hop relay, these arrive past stale and ATAK discards them. + * PLI and GeoChat are left untouched — their stale is meaningful. + */ + private val MIN_MESH_STALE_TTL = 15.minutes + private val STATIC_COT_PREFIXES = listOf("b-m-r", "u-d-", "b-m-p-") + private val EVENT_TYPE_RE = Regex("""]*\btype="([^"]*)"""") + private val STALE_ATTR_RE = Regex("""\bstale="([^"]*)"""") + + fun ensureMinimumStaleForMesh(xml: String): String { + val type = EVENT_TYPE_RE.find(xml)?.groupValues?.getOrNull(1) ?: return xml + if (STATIC_COT_PREFIXES.none { type.startsWith(it) }) return xml + val staleMatch = STALE_ATTR_RE.find(xml) ?: return xml + val staleStr = staleMatch.groupValues[1] + val staleInstant = try { + kotlin.time.Instant.parse(staleStr) + } catch (_: IllegalArgumentException) { + // Handle edge-case formats like missing "Z" + try { + val cleaned = staleStr.replace(Regex("""\.\d+"""), "").replace("Z", "+00:00") + kotlin.time.Instant.parse(cleaned) + } catch (_: IllegalArgumentException) { return xml } } - val cotMessage = takPacket.toCoTMessage() ?: return + val now = Clock.System.now() + val remaining = staleInstant - now + if (remaining >= MIN_MESH_STALE_TTL) return xml - takServerManager.broadcast(cotMessage) - Logger.d { "Forwarded ATAK mesh packet to TAK clients: ${cotMessage.type}" } + val newStale = now + MIN_MESH_STALE_TTL + val newStaleStr = newStale.toString().replace(Regex("""\.\d+"""), "") // strip fractional seconds + Logger.i { "Extended stale for $type: $staleStr → $newStaleStr (was ${remaining.inWholeSeconds}s remaining, now ${MIN_MESH_STALE_TTL.inWholeSeconds}s)" } + return xml.replaceRange(staleMatch.range, """stale="$newStaleStr"""") + } + + /** + * Strip non-essential XML elements before mesh compression to save wire bytes. + * These elements add 100-200 bytes but aren't needed for rendering shapes, + * routes, chats, markers, PLI, or any other payload on the receiving end. + */ + private val STRIP_PATTERNS = listOf( + """]*/>""", // TAK version (self-closing) + """]*>.*?""", // TAK version (paired) + """]*/>""", // voice chat state + """]*>.*?""", + """]*/>""", // empty marti + """]*>.*?""", + """<__geofence[^>]*/>""", // geofence config + """<__geofence[^>]*>.*?""", + """]*/>""", // toggle state + """]*/>""", // archive marker + """<__shapeExtras[^>]*/>""", // shape extras + """<__shapeExtras[^>]*>.*?""", + """]*/>""", // creator info + """]*>.*?""", + """]*/>""", // empty remarks (self-closing) + """]*>""", // empty remarks (paired) + """]*/>""", // stroke style (SDK uses color fields) + """]*/>""", // precision location metadata + """]*>.*?""", + """]*/>""", // iTAK camelCase variant + """]*>.*?""", + ).map { Regex(it, RegexOption.DOT_MATCHES_ALL) } + + // Strip any attribute with value "???" — unknown/placeholder metadata + private val UNKNOWN_ATTR_PATTERN = Regex("""\s+\w+\s*=\s*"[?]{3}"""") + + // Strip specific named attributes that the SDK doesn't use (display-only) + private val STRIP_ATTR_PATTERNS = listOf( + """\s+routetype\s*=\s*"[^"]*"""", // route display type (SDK doesn't use) + """\s+order\s*=\s*"[^"]*"""", // checkpoint order label (SDK doesn't use) + """\s+color\s*=\s*"[^"]*"""", // link_attr color (SDK uses strokeColor instead) + """\s+access\s*=\s*"[^"]*"""", // access control (not relevant for mesh) + """\s+callsign\s*=\s*""""", // empty callsign attributes (e.g. checkpoints) + """\s+phone\s*=\s*""""", // empty phone attributes + ).map { Regex(it) } + + // Route waypoint UID stripping — UIDs are full 36-char UUIDs that cost + // ~40 bytes each in the proto wire format. The receiving TAK client derives + // its own UIDs, so these are pure overhead. Only targets elements + // with a point= attribute (route waypoints / shape vertices). + private val ROUTE_LINK_ELEM_RE = Regex("""]*\bpoint="[^"]*"[^>]*/>""") + private val LINK_UID_ATTR_RE = Regex("""\s+uid="[^"]*"""") + + fun stripNonEssentialElements(xml: String): String { + var result = xml + for (pattern in STRIP_PATTERNS) { + result = pattern.replace(result, "") + } + // Strip ??? attributes from remaining elements + result = UNKNOWN_ATTR_PATTERN.replace(result, "") + // Strip specific display-only attributes + for (pattern in STRIP_ATTR_PATTERNS) { + result = pattern.replace(result, "") + } + // Strip uid from route waypoint elements (receiver derives UIDs) + result = ROUTE_LINK_ELEM_RE.replace(result) { LINK_UID_ATTR_RE.replace(it.value, "") } + return result + } } } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKModels.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKModels.kt index c301a5a06..1dad3f618 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKModels.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKModels.kt @@ -43,6 +43,23 @@ data class CoTMessage( val chat: CoTChat? = null, val remarks: String? = null, val rawDetailXml: String? = null, + /** + * Inner XML content of `...` captured by [CoTXmlParser] when this message + * was parsed from an incoming ATAK client event. Used as the `raw_detail` fallback payload + * when converting to [org.meshtastic.proto.TAKPacketV2] for CoT types that don't fit any + * structured payload (PLI / GeoChat / Aircraft). Null for messages constructed in-app. + * + * Distinct from [rawDetailXml], which is an output-only passthrough used by [toXml] to + * append extension content during serialization. + */ + val parsedDetailXml: String? = null, + /** + * The entire original `...` XML string as received from the ATAK client, + * captured by [CoTXmlParser]. Kept solely for diagnostic logging (e.g. when a packet + * exceeds the mesh MTU and is dropped) so the operator can see what the client actually + * sent. Null for messages constructed in-app. + */ + val sourceEventXml: String? = null, ) { companion object { fun pli( @@ -130,7 +147,7 @@ sealed class TAKConnectionEvent { data class ClientInfoUpdated(val clientInfo: TAKClientInfo) : TAKConnectionEvent() - data class Message(val cotMessage: CoTMessage) : TAKConnectionEvent() + data class Message(val cotMessage: CoTMessage, val clientInfo: TAKClientInfo? = null) : TAKConnectionEvent() data object Disconnected : TAKConnectionEvent() diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt deleted file mode 100644 index 25af8abf9..000000000 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketConversion.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -@file:Suppress("CyclomaticComplexMethod", "ReturnCount") - -package org.meshtastic.core.takserver - -import co.touchlab.kermit.Logger -import org.meshtastic.proto.Contact -import org.meshtastic.proto.GeoChat -import org.meshtastic.proto.Group -import org.meshtastic.proto.MemberRole -import org.meshtastic.proto.PLI -import org.meshtastic.proto.Status -import org.meshtastic.proto.TAKPacket -import org.meshtastic.proto.Team -import kotlin.random.Random -import kotlin.time.Clock -import kotlin.time.Duration.Companion.minutes - -object TAKPacketConversion { - - fun CoTMessage.toTAKPacket(): TAKPacket? { - val group = - this.group?.let { - Group( - role = MemberRole.fromValue(getMemberRoleValue(it.role)) ?: MemberRole.Unspecifed, - team = Team.fromValue(getTeamValue(it.name)) ?: Team.Unspecifed_Color, - ) - } - - val status = this.status?.let { Status(battery = it.battery.coerceAtLeast(0)) } - - if (type.startsWith("a-f-G") || type.startsWith("a-f-g")) { - return createPliPacket(group, status) - } - - if (type == "b-t-f") { - return createChatPacket(group, status) - } - - Logger.w { "Cannot convert CoT to TAKPacket for type $type" } - return null - } - - private fun CoTMessage.createPliPacket(group: Group?, status: Status?): TAKPacket { - val contact = this.contact?.let { Contact(callsign = it.callsign, device_callsign = this.uid) } - val pli = - PLI( - latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt(), - longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt(), - altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt(), - speed = track?.speed?.coerceAtLeast(0.0)?.toInt() ?: 0, - course = track?.course?.coerceAtLeast(0.0)?.toInt() ?: 0, - ) - - return TAKPacket(is_compressed = false, contact = contact, group = group, status = status, pli = pli) - } - - private fun CoTMessage.createChatPacket(group: Group?, status: Status?): TAKPacket? { - val localChat = this.chat ?: return null - val chatMsg = localChat.message - var toUid: String? = null - var toCallsign: String? = null - - val actualDeviceUid = this.uid.geoChatSenderUid() - val messageId = - if (this.uid.startsWith("GeoChat.")) { - this.uid.geoChatMessageId() - } else { - Random.nextInt().toString(TAK_HEX_RADIX) - } - - val contact = - this.contact?.let { - val smuggledCallsign = - if (actualDeviceUid.isNotEmpty()) { - "$actualDeviceUid|$messageId" - } else { - it.endpoint ?: "" - } - Contact(callsign = it.callsign, device_callsign = smuggledCallsign) - } - - if (localChat.chatroom.startsWith(this.uid) || this.uid.startsWith("GeoChat")) { - val parts = this.uid.split(".") - if (parts.size >= TAK_DIRECT_MESSAGE_PARTS_MIN && parts[0] == "GeoChat") { - toUid = localChat.chatroom - } - } else if (localChat.chatroom != "All Chat Rooms") { - toCallsign = localChat.chatroom - } - - val chat = - GeoChat( - message = chatMsg, - to = toUid ?: if (toCallsign == null) "All Chat Rooms" else null, - to_callsign = toCallsign, - ) - - return TAKPacket(is_compressed = false, contact = contact, group = group, status = status, chat = chat) - } - - fun TAKPacket.toCoTMessage(): CoTMessage? { - val rawDeviceCallsign = contact?.device_callsign ?: "UNKNOWN" - val senderCallsign = contact?.callsign ?: "UNKNOWN" - val timeNow = Clock.System.now() - val staleTime = timeNow + DEFAULT_TAK_STALE_MINUTES.minutes - - val (senderUid, messageId) = parseDeviceCallsign(rawDeviceCallsign) - - val localPli = pli - if (localPli != null) { - return CoTMessage.pli( - uid = senderUid, - callsign = senderCallsign, - latitude = localPli.latitude_i.toDouble() / TAK_COORDINATE_SCALE, - longitude = localPli.longitude_i.toDouble() / TAK_COORDINATE_SCALE, - altitude = localPli.altitude.toDouble(), - speed = localPli.speed.toDouble(), - course = localPli.course.toDouble(), - team = teamToColorName(group?.team), - role = roleToName(group?.role), - battery = status?.battery ?: DEFAULT_TAK_BATTERY, - staleMinutes = DEFAULT_TAK_STALE_MINUTES, - ) - } - - val localChat = chat - if (localChat != null) { - val chatroom = - if (localChat.to != null || localChat.to_callsign != null) { - localChat.to_callsign ?: localChat.to ?: "Direct Message" - } else { - "All Chat Rooms" - } - - val msgId = messageId ?: Random.nextInt().toString(TAK_HEX_RADIX) - - return CoTMessage( - uid = "GeoChat.$senderUid.$chatroom.$msgId", - type = "b-t-f", - how = "h-g-i-g-o", - time = timeNow, - start = timeNow, - stale = staleTime, - latitude = 0.0, - longitude = 0.0, - contact = CoTContact(callsign = senderCallsign, endpoint = DEFAULT_TAK_ENDPOINT), - group = CoTGroup(name = teamToColorName(group?.team), role = roleToName(group?.role)), - status = CoTStatus(battery = status?.battery ?: DEFAULT_TAK_BATTERY), - chat = CoTChat(chatroom = chatroom, senderCallsign = senderCallsign, message = localChat.message), - ) - } - - return null - } - - private fun parseDeviceCallsign(combined: String): Pair { - val parts = combined.split("|", limit = 2) - return if (parts.size == 2) { - Pair(parts[0], parts[1].ifEmpty { null }) - } else { - Pair(combined, null) - } - } - - private fun getTeamValue(name: String): Int = - Team.entries.find { it.name.equals(name, ignoreCase = true) }?.value ?: 0 - - private fun getMemberRoleValue(roleName: String): Int = - MemberRole.entries.find { it.name.equals(roleName.replace(" ", ""), ignoreCase = true) }?.value ?: 0 - - private fun teamToColorName(team: Team?): String { - if (team == null || team == Team.Unspecifed_Color) return DEFAULT_TAK_TEAM_NAME - return team.toTakTeamName() - } - - private fun roleToName(role: MemberRole?): String { - if (role == null || role == MemberRole.Unspecifed) return DEFAULT_TAK_ROLE_NAME - return role.toTakRoleName() - } -} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt new file mode 100644 index 000000000..fc61c1be3 --- /dev/null +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKPacketV2Conversion.kt @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ +@file:Suppress("CyclomaticComplexMethod", "ReturnCount") + +package org.meshtastic.core.takserver + +import co.touchlab.kermit.Logger +import okio.ByteString.Companion.toByteString +import org.meshtastic.proto.CotHow +import org.meshtastic.proto.CotType +import org.meshtastic.proto.GeoChat +import org.meshtastic.proto.GeoPointSource +import org.meshtastic.proto.MemberRole +import org.meshtastic.proto.TAKPacketV2 +import org.meshtastic.proto.Team +import kotlin.random.Random +import kotlin.time.Clock +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +/** + * Conversion between CoTMessage and TAKPacketV2 (v2 wire protocol). + */ +object TAKPacketV2Conversion { + + fun CoTMessage.toTAKPacketV2(): TAKPacketV2? { + val cotTypeEnum = TakV2TypeMapper.cotTypeFromString(type) + val cotTypeStr = if (cotTypeEnum == CotType.CotType_Other) type else "" + val howEnum = TakV2TypeMapper.cotHowFromString(how) + + val teamEnum = group?.let { + val teamValue = Team.entries.find { t -> t.name.equals(it.name, ignoreCase = true) }?.value ?: 0 + Team.fromValue(teamValue) + } ?: Team.Unspecifed_Color + + val roleEnum = group?.let { + val roleValue = MemberRole.entries.find { r -> r.name.equals(it.role.replace(" ", ""), ignoreCase = true) }?.value ?: 0 + MemberRole.fromValue(roleValue) + } ?: MemberRole.Unspecifed + + val battery = status?.battery?.coerceAtLeast(0) ?: 0 + + // PLI (position reports) + if (type.startsWith("a-f-G") || type.startsWith("a-f-g") || type.startsWith("a-")) { + val callsign = contact?.callsign ?: "UNKNOWN" + val deviceCallsign = uid + + return TAKPacketV2( + cot_type_id = cotTypeEnum, + cot_type_str = cotTypeStr, + how = howEnum, + callsign = callsign, + device_callsign = deviceCallsign, + uid = uid, + team = teamEnum, + role = roleEnum, + latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt(), + longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt(), + altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt(), + speed = (track?.speed?.coerceAtLeast(0.0)?.times(100))?.toInt() ?: 0, // m/s -> cm/s + course = (track?.course?.coerceAtLeast(0.0)?.times(100))?.toInt() ?: 0, // deg -> deg*100 + battery = battery, + geo_src = GeoPointSource.GeoPointSource_GPS, + alt_src = GeoPointSource.GeoPointSource_GPS, + pli = true, + ) + } + + // GeoChat + if (type == "b-t-f") { + val localChat = chat ?: return null + // ATAK GeoChat events often omit — the + // sender identity is only in <__chat senderCallsign="..."/>. + val callsign = contact?.callsign + ?: localChat.senderCallsign + ?: "UNKNOWN" + val actualDeviceUid = uid.geoChatSenderUid() + val messageId = if (uid.startsWith("GeoChat.")) { + uid.geoChatMessageId() + } else { + Random.nextInt().toString(TAK_HEX_RADIX) + } + + val smuggledCallsign = if (actualDeviceUid.isNotEmpty()) { + "$actualDeviceUid|$messageId" + } else { + contact?.endpoint ?: "" + } + + var toUid: String? = null + var toCallsign: String? = null + if (localChat.chatroom != "All Chat Rooms") { + if (localChat.chatroom.startsWith(uid) || uid.startsWith("GeoChat")) { + val parts = uid.split(".") + if (parts.size >= TAK_DIRECT_MESSAGE_PARTS_MIN && parts[0] == "GeoChat") { + toUid = localChat.chatroom + } + } else { + toCallsign = localChat.chatroom + } + } + + return TAKPacketV2( + cot_type_id = CotType.CotType_b_t_f, + how = CotHow.CotHow_h_g_i_g_o, + callsign = callsign, + device_callsign = smuggledCallsign, + uid = uid, + team = teamEnum, + role = roleEnum, + battery = battery, + chat = GeoChat( + message = localChat.message, + to = toUid ?: if (toCallsign == null) "All Chat Rooms" else null, + to_callsign = toCallsign, + ), + ) + } + + // Fallback: wrap the whole detail XML in raw_detail for unmapped types + // (user-drawn shapes like u-d-c-c, markers like b-m-*, alerts, etc.) + val detailBytes = parsedDetailXml?.encodeToByteArray() + if (detailBytes != null) { + val callsign = contact?.callsign ?: "UNKNOWN" + return TAKPacketV2( + cot_type_id = cotTypeEnum, + cot_type_str = cotTypeStr, + how = howEnum, + callsign = callsign, + device_callsign = uid, + uid = uid, + team = teamEnum, + role = roleEnum, + latitude_i = (latitude * TAK_COORDINATE_SCALE).toInt(), + longitude_i = (longitude * TAK_COORDINATE_SCALE).toInt(), + altitude = if (hae >= TAK_UNKNOWN_POINT_VALUE || hae.isNaN()) 0 else hae.toInt(), + battery = battery, + raw_detail = detailBytes.toByteString(), + ) + } + + Logger.w { "Cannot convert CoT to TAKPacketV2 for type $type (no parsed detail)" } + return null + } + + fun TAKPacketV2.toCoTMessage(): CoTMessage? { + val senderCallsign = callsign.ifEmpty { "UNKNOWN" } + val rawDeviceCallsign = device_callsign.ifEmpty { uid.ifEmpty { "UNKNOWN" } } + val timeNow = Clock.System.now() + val (senderUid, messageId) = parseDeviceCallsign(rawDeviceCallsign) + + // PLI + if (pli != null) { + val staleMinutes = if (stale_seconds > 0) (stale_seconds / 60) else DEFAULT_TAK_STALE_MINUTES + return CoTMessage.pli( + uid = senderUid.ifEmpty { uid }, + callsign = senderCallsign, + latitude = latitude_i.toDouble() / TAK_COORDINATE_SCALE, + longitude = longitude_i.toDouble() / TAK_COORDINATE_SCALE, + altitude = altitude.toDouble(), + speed = speed.toDouble() / 100.0, // cm/s -> m/s + course = course.toDouble() / 100.0, // deg*100 -> deg + team = teamToColorName(team), + role = roleToName(role), + battery = battery, + staleMinutes = staleMinutes, + ) + } + + // GeoChat + val localChat = chat + if (localChat != null) { + // chat.to carries the recipient/room ID for DMs; null means broadcast. + // Do NOT fall through to chat.to_callsign here — despite the name, + // it holds the SENDER's callsign (the parser stores __chat[@senderCallsign] + // there), not a chatroom name. + val chatroom = localChat.to ?: "All Chat Rooms" + + val msgId = messageId ?: Random.nextInt().toString(TAK_HEX_RADIX) + val staleTime = timeNow + if (stale_seconds > 0) { + stale_seconds.seconds + } else { + DEFAULT_TAK_STALE_MINUTES.minutes + } + + return CoTMessage( + uid = "GeoChat.$senderUid.$chatroom.$msgId", + type = "b-t-f", + how = "h-g-i-g-o", + time = timeNow, + start = timeNow, + stale = staleTime, + latitude = latitude_i.toDouble() / TAK_COORDINATE_SCALE, + longitude = longitude_i.toDouble() / TAK_COORDINATE_SCALE, + contact = CoTContact(callsign = senderCallsign, endpoint = DEFAULT_TAK_ENDPOINT), + group = CoTGroup(name = teamToColorName(team), role = roleToName(role)), + status = CoTStatus(battery = battery), + chat = CoTChat( + chatroom = chatroom, + senderCallsign = senderCallsign, + message = localChat.message, + ), + ) + } + + // Raw detail: unmapped CoT types round-tripped as opaque detail bytes. + // Emit a bare CoTMessage whose is the raw bytes verbatim. Do NOT populate + // contact/group/status here — those would be double-emitted by toXml() alongside + // rawDetailXml, corrupting the CoT stream. + val rawDetail = raw_detail + if (rawDetail != null) { + val rawXml = rawDetail.utf8() + val resolvedType = cot_type_str.ifEmpty { + TakV2TypeMapper.cotTypeToString(cot_type_id) ?: "a-f-G-U-C" + } + val resolvedHow = TakV2TypeMapper.cotHowToString(how) ?: "m-g" + val staleTime = timeNow + if (stale_seconds > 0) { + stale_seconds.seconds + } else { + DEFAULT_TAK_STALE_MINUTES.minutes + } + return CoTMessage( + uid = uid.ifEmpty { senderUid.ifEmpty { "tak-raw" } }, + type = resolvedType, + how = resolvedHow, + time = timeNow, + start = timeNow, + stale = staleTime, + latitude = latitude_i.toDouble() / TAK_COORDINATE_SCALE, + longitude = longitude_i.toDouble() / TAK_COORDINATE_SCALE, + hae = if (altitude == 0) TAK_UNKNOWN_POINT_VALUE else altitude.toDouble(), + rawDetailXml = rawXml, + ) + } + + Logger.w { "Cannot convert TAKPacketV2 to CoTMessage: no PLI, chat, or raw_detail payload" } + return null + } + + private fun parseDeviceCallsign(combined: String): Pair { + val parts = combined.split("|", limit = 2) + return if (parts.size == 2) { + Pair(parts[0], parts[1].ifEmpty { null }) + } else { + Pair(combined, null) + } + } + + private fun teamToColorName(team: Team?): String { + if (team == null || team == Team.Unspecifed_Color) return DEFAULT_TAK_TEAM_NAME + return team.toTakTeamName() + } + + private fun roleToName(role: MemberRole?): String { + if (role == null || role == MemberRole.Unspecifed) return DEFAULT_TAK_ROLE_NAME + return role.toTakRoleName() + } + + private fun getTeamValue(name: String): Int = + Team.entries.find { it.name.equals(name, ignoreCase = true) }?.value ?: 0 + + private fun getMemberRoleValue(roleName: String): Int = + MemberRole.entries.find { it.name.equals(roleName.replace(" ", ""), ignoreCase = true) }?.value ?: 0 +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServer.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServer.kt index 05f717aee..b0449e249 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServer.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServer.kt @@ -14,194 +14,60 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:Suppress("TooGenericExceptionCaught") - package org.meshtastic.core.takserver -import co.touchlab.kermit.Logger -import io.ktor.network.selector.SelectorManager -import io.ktor.network.sockets.ServerSocket -import io.ktor.network.sockets.Socket -import io.ktor.network.sockets.SocketAddress -import io.ktor.network.sockets.aSocket -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import org.meshtastic.core.di.CoroutineDispatchers -import kotlin.random.Random -import kotlinx.coroutines.isActive as coroutineIsActive -class TAKServer(private val dispatchers: CoroutineDispatchers, private val port: Int = DEFAULT_TAK_PORT) { - private var serverSocket: ServerSocket? = null - private var selectorManager: SelectorManager? = null - private var running = false - private var serverScope: CoroutineScope? = null - private var acceptJob: Job? = null - private val connectionsMutex = Mutex() +/** + * Platform-agnostic contract for the Meshtastic TAK server. + * + * The production implementation on Android / JVM runs a TLS (mTLS) listener on port + * [DEFAULT_TAK_PORT] (8089) using the bundled server identity. This matches the + * Meshtastic-Apple (iOS) implementation so that a single exported `.zip` data package + * is valid for ATAK on Android AND iTAK on iOS without re-configuration. + * + * The interface deliberately hides the platform socket / TLS primitives so that + * `commonMain` code (`TAKServerManagerImpl`, DI, tests) can depend on it without + * pulling `javax.net.ssl.*` into the common source set. + */ +interface TAKServer { - private val connections = mutableMapOf() + /** Observable count of currently-connected TAK clients (ATAK/iTAK). */ + val connectionCount: StateFlow - private val _connectionCount = MutableStateFlow(0) - val connectionCount: StateFlow = _connectionCount.asStateFlow() + /** Callback invoked on the IO dispatcher for every inbound CoT message from a client. */ + var onMessage: ((CoTMessage, TAKClientInfo?) -> Unit)? - var onMessage: ((CoTMessage) -> Unit)? = null + /** Callback invoked when a TAK client connects. Use to drain queued messages. */ + var onClientConnected: (() -> Unit)? - suspend fun start(scope: CoroutineScope): Result { - // Double-start guard: prevents SelectorManager / ServerSocket leaks - if (running) { - Logger.w { "TAK Server already running on port $port" } - return Result.success(Unit) - } + /** Bind the listener and begin accepting connections. Idempotent if already running. */ + suspend fun start(scope: CoroutineScope): Result - return try { - serverScope = scope - // Close any stale SelectorManager before creating a new one - selectorManager?.close() - selectorManager = SelectorManager(dispatchers.default) - serverSocket = aSocket(selectorManager!!).tcp().bind(hostname = "127.0.0.1", port = port) + /** Stop the listener, close all client sockets, and release OS resources. */ + fun stop() - running = true - acceptJob = scope.launch(dispatchers.io) { acceptLoop() } - Result.success(Unit) - } catch (e: Exception) { - Logger.e(e) { "Failed to bind TAK Server to 127.0.0.1:$port" } - Result.failure(e) - } - } + /** Broadcast a CoT message to every currently-connected client. */ + suspend fun broadcast(cotMessage: CoTMessage) - private suspend fun acceptLoop() { - val scope = serverScope ?: return - while (running && scope.coroutineIsActive) { - try { - val clientSocket = serverSocket?.accept() - if (clientSocket != null) { - handleConnection(clientSocket) - } - // No delay on the success path — accept() is already suspending - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - Logger.w(e) { "TAK server accept loop iteration failed" } - // Back-off only in the error path - delay(TAK_ACCEPT_LOOP_DELAY_MS) - } - } - } + /** Broadcast raw CoT XML to every currently-connected client. + * Used for mesh-originated messages that should be forwarded verbatim + * without re-parsing through the app's CoTXmlParser (which strips + * shape detail elements like strokeColor, fillColor, vertices, etc.). */ + suspend fun broadcastRawXml(xml: String) - private fun handleConnection(clientSocket: Socket) { - val scope = serverScope ?: return - val endpoint = clientSocket.remoteAddress.toString() - - if (!clientSocket.remoteAddress.isLoopback()) { - Logger.w { "TAK server rejected non-loopback connection from $endpoint" } - clientSocket.close() - return - } - - val connectionId = Random.nextInt().toString(TAK_HEX_RADIX) - val clientInfo = TAKClientInfo(id = connectionId, endpoint = endpoint) - - val connection = - TAKClientConnection( - socket = clientSocket, - clientInfo = clientInfo, - onEvent = { event -> handleConnectionEvent(connectionId, event) }, - scope = scope, - ) - - scope.launch { - connectionsMutex.withLock { - connections[connectionId] = connection - _connectionCount.value = connections.size - } - connection.start() - } - } - - private fun handleConnectionEvent(connectionId: String, event: TAKConnectionEvent) { - when (event) { - is TAKConnectionEvent.Message -> { - onMessage?.invoke(event.cotMessage) - } - is TAKConnectionEvent.Disconnected -> { - serverScope?.launch { - connectionsMutex.withLock { - connections.remove(connectionId) - _connectionCount.value = connections.size - } - } - } - is TAKConnectionEvent.Error -> { - Logger.w(event.error) { "TAK client connection error: $connectionId" } - serverScope?.launch { - connectionsMutex.withLock { - connections.remove(connectionId) - _connectionCount.value = connections.size - } - } - } - is TAKConnectionEvent.Connected -> { - /* no-op: logged by TAKClientConnection.start() */ - } - is TAKConnectionEvent.ClientInfoUpdated -> { - /* no-op: TAKClientConnection tracks updated info locally */ - } - } - } - - fun stop() { - running = false - acceptJob?.cancel() - acceptJob = null - - // Close connections synchronously — TAKClientConnection.close() is non-suspending, - // so we don't need to launch into the (possibly-cancelled) serverScope. - val toClose: List - // We can't use Mutex.withLock here (non-suspending context) so we swap & clear under a - // best-effort copy — worst case a connection added concurrently is closed by socket teardown. - toClose = connections.values.toList() - connections.clear() - _connectionCount.value = 0 - toClose.forEach { it.close() } - - serverSocket?.close() - serverSocket = null - - selectorManager?.close() - selectorManager = null - serverScope = null - } - - suspend fun broadcast(cotMessage: CoTMessage) { - val currentConnections = connectionsMutex.withLock { connections.values.toList() } - currentConnections.forEach { connection -> - try { - connection.send(cotMessage) - } catch (e: Exception) { - Logger.w(e) { "Failed to broadcast CoT to TAK client ${connection.clientInfo.id}" } - connection.close() - } - } - } - - suspend fun hasConnections(): Boolean = connectionsMutex.withLock { connections.isNotEmpty() } + /** Returns true if at least one TAK client is currently connected. */ + suspend fun hasConnections(): Boolean } /** - * Returns true if this [SocketAddress] represents a loopback address (IPv4 127.x.x.x or IPv6 ::1). - * - * Ktor's [SocketAddress.toString] returns strings like "/127.0.0.1:4242" (JVM) or "127.0.0.1:4242" on other platforms, - * so we strip any leading slash and check prefixes without parsing the host. This keeps the check in commonMain without - * an expect/actual. + * Platform factory for [TAKServer]. The JVM/Android implementation lives in + * `jvmAndroidMain` and uses JSSE (`SSLServerSocket`) with the bundled + * `server.p12` identity and `ca.pem` client trust store. */ -private fun SocketAddress.isLoopback(): Boolean { - val addr = toString().removePrefix("/") - return addr.startsWith("127.") || addr.startsWith("::1") || addr.startsWith("[::1]") -} +expect fun createTAKServer( + dispatchers: CoroutineDispatchers, + port: Int = DEFAULT_TAK_PORT, +): TAKServer diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServerManager.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServerManager.kt index 31248ec41..9ac3957e6 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServerManager.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKServerManager.kt @@ -27,27 +27,32 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import org.meshtastic.core.model.Node + +import kotlin.time.Clock +import kotlin.time.Duration.Companion.minutes + +/** A CoT message received from a connected TAK client, paired with the client's identity. */ +data class InboundCoTMessage(val cotMessage: CoTMessage, val clientInfo: TAKClientInfo? = null) interface TAKServerManager { val isRunning: StateFlow val connectionCount: StateFlow - val inboundMessages: SharedFlow + val inboundMessages: SharedFlow /** Start the TAK server using [scope]. Port is fixed at [TAKServer] construction time. */ fun start(scope: CoroutineScope) fun stop() - fun broadcastNode(node: Node, team: String = DEFAULT_TAK_TEAM_NAME, role: String = DEFAULT_TAK_ROLE_NAME) - fun broadcast(cotMessage: CoTMessage) + + /** Broadcast raw XML verbatim to TAK clients, bypassing CoTMessage parsing. */ + fun broadcastRawXml(xml: String) } class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServerManager { private var scope: CoroutineScope? = null - private val lastBroadcastPositionsMutex = Mutex() private val _isRunning = MutableStateFlow(false) override val isRunning: StateFlow = _isRunning.asStateFlow() @@ -55,10 +60,20 @@ class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServerManager // Mirror TAKServer's event-driven connection count — no polling needed override val connectionCount: StateFlow = takServer.connectionCount - private val _inboundMessages = MutableSharedFlow() - override val inboundMessages: SharedFlow = _inboundMessages.asSharedFlow() + private val _inboundMessages = MutableSharedFlow() + override val inboundMessages: SharedFlow = _inboundMessages.asSharedFlow() - private var lastBroadcastPositions = mutableMapOf() + // Offline message queue — buffers mesh-originated CoT messages when no TAK + // clients are connected, then drains them when a client reconnects. Entries + // expire after OFFLINE_QUEUE_TTL to avoid delivering stale situational data. + private data class QueuedMessage(val cotMessage: CoTMessage, val enqueuedAt: kotlin.time.Instant) + private val offlineQueue = ArrayDeque() + private val offlineQueueMutex = Mutex() + + companion object { + private val OFFLINE_QUEUE_TTL = 5.minutes + private const val OFFLINE_QUEUE_MAX_SIZE = 50 + } override fun start(scope: CoroutineScope) { this.scope = scope @@ -69,7 +84,10 @@ class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServerManager scope.launch { // Wire up inbound message handler BEFORE starting so no messages are lost - takServer.onMessage = { cotMessage -> scope.launch { _inboundMessages.emit(cotMessage) } } + takServer.onMessage = { cotMessage, clientInfo -> + scope.launch { _inboundMessages.emit(InboundCoTMessage(cotMessage, clientInfo)) } + } + takServer.onClientConnected = { drainOfflineQueue() } val result = takServer.start(scope) if (result.isSuccess) { @@ -91,61 +109,46 @@ class TAKServerManagerImpl(private val takServer: TAKServer) : TAKServerManager Logger.i { "TAK Server stopped" } } - override fun broadcastNode(node: Node, team: String, role: String) { - if (!_isRunning.value) return - val currentScope = scope ?: return - - currentScope.launch { - if (!takServer.hasConnections()) return@launch - - val position = node.validPosition - if (position == null) { - broadcastNodeInfoOnly(node, team, role) - return@launch - } - - val shouldBroadcast = - lastBroadcastPositionsMutex.withLock { - val last = lastBroadcastPositions[node.num] - if (position.time == last) { - false - } else { - lastBroadcastPositions[node.num] = position.time - true - } - } - if (!shouldBroadcast) return@launch - - val cotMessage = - position.toCoTMessage( - uid = node.user.id, - callsign = node.user.toTakCallsign(), - team = team, - role = role, - battery = node.deviceMetrics.battery_level ?: 100, - ) - - takServer.broadcast(cotMessage) - } - } - - private fun broadcastNodeInfoOnly(node: Node, team: String, role: String) { - val currentScope = scope ?: return - val cotMessage = - node.user.toCoTMessage( - position = null, - team = team, - role = role, - battery = node.deviceMetrics.battery_level ?: 100, - ) - - currentScope.launch { - if (!takServer.hasConnections()) return@launch - takServer.broadcast(cotMessage) - } - } - override fun broadcast(cotMessage: CoTMessage) { - scope?.launch { takServer.broadcast(cotMessage) } + scope?.launch { + if (takServer.hasConnections()) { + takServer.broadcast(cotMessage) + } else { + // No TAK clients connected — queue for delivery when one reconnects + offlineQueueMutex.withLock { + // Evict expired entries + val cutoff = Clock.System.now() - OFFLINE_QUEUE_TTL + while (offlineQueue.isNotEmpty() && offlineQueue.first().enqueuedAt < cutoff) { + offlineQueue.removeFirst() + } + // Cap size to prevent unbounded growth + if (offlineQueue.size >= OFFLINE_QUEUE_MAX_SIZE) { + offlineQueue.removeFirst() + } + offlineQueue.addLast(QueuedMessage(cotMessage, Clock.System.now())) + } + } + } + } + + override fun broadcastRawXml(xml: String) { + scope?.launch { takServer.broadcastRawXml(xml) } + } + + /** Drain any queued messages to the newly connected TAK client. Called by the server + * when a TAK client connects (Connected event). */ + internal fun drainOfflineQueue() { + scope?.launch { + val messages = offlineQueueMutex.withLock { + val cutoff = Clock.System.now() - OFFLINE_QUEUE_TTL + val valid = offlineQueue.filter { it.enqueuedAt >= cutoff }.map { it.cotMessage } + offlineQueue.clear() + valid + } + if (messages.isNotEmpty()) { + Logger.i { "Draining ${messages.size} queued message(s) to reconnected TAK client" } + messages.forEach { takServer.broadcast(it) } + } + } } } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt new file mode 100644 index 000000000..5a013f599 --- /dev/null +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakMeshTestRunner.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ +package org.meshtastic.core.takserver + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import okio.ByteString.Companion.toByteString +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.proto.PortNum + +/** + * Result of sending a single test fixture through the TAK mesh pipeline. + */ +data class TakTestResult( + val fixtureName: String, + val xmlBytes: Int, + val compressedBytes: Int, + val passed: Boolean, + val error: String? = null, +) + +/** + * Debug-only test runner that sends the SDK's CoT XML test fixtures through the + * real TAK mesh pipeline: strip → parse → compress → send to mesh radio. + * + * Paces sends by waiting [sendDelayMs] between each fixture to avoid flooding + * the radio's TX queue. + */ +class TakMeshTestRunner( + private val commandSender: CommandSender, +) { + private val _results = MutableStateFlow>(emptyList()) + val results: StateFlow> = _results.asStateFlow() + + private val _isRunning = MutableStateFlow(false) + val isRunning: StateFlow = _isRunning.asStateFlow() + + private val _currentFixture = MutableStateFlow(null) + val currentFixture: StateFlow = _currentFixture.asStateFlow() + + companion object { + /** Delay between sends to let the radio transmit and receive ACK. */ + private const val SEND_DELAY_MS = 5_000L + private const val MAX_TAK_WIRE_PAYLOAD_BYTES = 225 + + /** All bundled fixture filenames. */ + val FIXTURE_NAMES = listOf( + "aircraft_adsb.xml", + "aircraft_hostile.xml", + "alert_tic.xml", + "casevac.xml", + "casevac_medline.xml", + "chat_receipt_delivered.xml", + "chat_receipt_read.xml", + "delete_event.xml", + "drawing_circle.xml", + "drawing_circle_large.xml", + "drawing_ellipse.xml", + "drawing_freeform.xml", + "drawing_polygon.xml", + "drawing_rectangle.xml", + "drawing_rectangle_itak.xml", + "drawing_telestration.xml", + "emergency_911.xml", + "emergency_cancel.xml", + "geochat_broadcast.xml", + "geochat_dm.xml", + "geochat_simple.xml", + "marker_2525.xml", + "marker_goto.xml", + "marker_goto_itak.xml", + "marker_icon_set.xml", + "marker_spot.xml", + "marker_tank.xml", + "pli_basic.xml", + "pli_full.xml", + "pli_itak.xml", + "pli_stationary.xml", + "pli_takaware.xml", + "pli_webtak.xml", + "ranging_bullseye.xml", + "ranging_circle.xml", + "ranging_line.xml", + "route_3wp.xml", + "route_itak_3wp.xml", + "task_engage.xml", + "waypoint.xml", + ) + } + + /** + * Run all test fixtures sequentially, sending each through the mesh pipeline. + * Updates [results] and [currentFixture] as each fixture is processed. + */ + suspend fun runAll() { + if (_isRunning.value) return + _isRunning.value = true + _results.value = emptyList() + + val allResults = mutableListOf() + + for (name in FIXTURE_NAMES) { + _currentFixture.value = name + val result = runSingleFixture(name) + allResults.add(result) + _results.value = allResults.toList() + + if (result.passed) { + // Wait for radio airtime + ACK before next send + delay(SEND_DELAY_MS) + } + } + + _currentFixture.value = null + _isRunning.value = false + + val passed = allResults.count { it.passed } + val failed = allResults.size - passed + Logger.i { "TAK Mesh Test complete: $passed/${allResults.size} passed, $failed failed" } + } + + private suspend fun runSingleFixture(name: String): TakTestResult { + // Load fixture XML from bundled resources + val xml = try { + loadFixtureXml(name) + } catch (e: Throwable) { + Logger.w(e) { "Failed to load fixture $name" } + return TakTestResult(name, 0, 0, false, "Load failed: ${e.message}") + } + + // Apply the same pipeline as TAKMeshIntegration.sendCoTToMesh() + val freshXml = TAKMeshIntegration.ensureMinimumStaleForMesh(xml) + val strippedXml = TAKMeshIntegration.stripNonEssentialElements(freshXml) + + // Parse and compress via SDK + val wirePayload: ByteArray + try { + val sdkParser = org.meshtastic.tak.CotXmlParser() + val sdkData = sdkParser.parse(strippedXml) + val compressor = org.meshtastic.tak.TakCompressor() + val compressed = compressor.compressWithRemarksFallback(sdkData, MAX_TAK_WIRE_PAYLOAD_BYTES) + if (compressed == null) { + Logger.w { "TAK Test: $name oversized even without remarks (xml=${xml.length}B)" } + return TakTestResult(name, xml.length, 0, false, "Oversized (>${MAX_TAK_WIRE_PAYLOAD_BYTES}B)") + } + wirePayload = compressed + } catch (e: Throwable) { + Logger.w(e) { "TAK Test: $name compression failed: ${e.message}" } + return TakTestResult(name, xml.length, 0, false, "Compress failed: ${e.message}") + } + + // Send to mesh + try { + val dataPacket = DataPacket( + to = DataPacket.ID_BROADCAST, + bytes = wirePayload.toByteString(), + dataType = PortNum.ATAK_PLUGIN_V2.value, + ) + commandSender.sendData(dataPacket) + Logger.i { "TAK Test: $name → ${wirePayload.size}B (xml=${xml.length}B)" } + return TakTestResult(name, xml.length, wirePayload.size, true) + } catch (e: Throwable) { + Logger.w(e) { "TAK Test: $name send failed: ${e.message}" } + return TakTestResult(name, xml.length, wirePayload.size, false, "Send failed: ${e.message}") + } + } + + private fun loadFixtureXml(name: String): String { + val stream = this::class.java.classLoader?.getResourceAsStream("tak_test_fixtures/$name") + ?: throw IllegalStateException("Fixture not found: tak_test_fixtures/$name") + return stream.bufferedReader().readText() + } +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt new file mode 100644 index 000000000..04c358cb5 --- /dev/null +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package org.meshtastic.core.takserver + +import org.meshtastic.proto.TAKPacketV2 + +/** + * TAKPacket V2 wire format compressor/decompressor. + * + * Wire format: [1 byte flags][zstd-compressed TAKPacketV2 protobuf] + * Flags byte bits 0-5 = dictionary ID, bits 6-7 = reserved. + * Special value 0xFF = uncompressed raw protobuf (from TAK_TRACKER firmware). + * + * Platform-specific implementations use zstd with pre-trained dictionaries. + */ +internal expect object TakV2Compressor { + + /** Maximum allowed decompressed payload size (bytes). */ + val MAX_DECOMPRESSED_SIZE: Int + + /** Dictionary ID for non-aircraft types. */ + val DICT_ID_NON_AIRCRAFT: Int + + /** Dictionary ID for aircraft types. */ + val DICT_ID_AIRCRAFT: Int + + /** Special flags byte value indicating uncompressed raw protobuf. */ + val DICT_ID_UNCOMPRESSED: Int + + /** + * Compress a TAKPacketV2 into wire payload: [flags byte][zstd compressed protobuf]. + * Selects dictionary based on the CoT type classification. + */ + fun compress(packet: TAKPacketV2): ByteArray + + /** + * Decompress a wire payload back to TAKPacketV2. + * Handles both compressed (dict-based) and uncompressed (0xFF) payloads. + * @throws IllegalArgumentException if payload is malformed or exceeds size limits. + */ + fun decompress(wirePayload: ByteArray): TAKPacketV2 + + /** + * Decompress a wire payload and reconstruct CoT XML via the SDK's CotXmlBuilder. + * Handles ALL payload types (DrawnShape, Marker, Route, etc.) without going + * through the Wire proto intermediate. + */ + fun decompressToXml(wirePayload: ByteArray): String +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2TypeMapper.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2TypeMapper.kt new file mode 100644 index 000000000..a67cd9383 --- /dev/null +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TakV2TypeMapper.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package org.meshtastic.core.takserver + +import org.meshtastic.proto.CotHow +import org.meshtastic.proto.CotType + +/** + * Maps CoT type strings (e.g. "a-f-G-U-C") to CotType enum values and back. + */ +internal object TakV2TypeMapper { + + private val stringToType: Map = mapOf( + "a-f-G-U-C" to CotType.CotType_a_f_G_U_C, + "a-f-G-U-C-I" to CotType.CotType_a_f_G_U_C_I, + "a-n-A-C-F" to CotType.CotType_a_n_A_C_F, + "a-n-A-C-H" to CotType.CotType_a_n_A_C_H, + "a-n-A-C" to CotType.CotType_a_n_A_C, + "a-f-A-M-H" to CotType.CotType_a_f_A_M_H, + "a-f-A-M" to CotType.CotType_a_f_A_M, + "a-h-A-M-F-F" to CotType.CotType_a_h_A_M_F_F, + "a-u-A-C" to CotType.CotType_a_u_A_C, + "t-x-d-d" to CotType.CotType_t_x_d_d, + "b-t-f" to CotType.CotType_b_t_f, + "b-r-f-h-c" to CotType.CotType_b_r_f_h_c, + "b-a-o-pan" to CotType.CotType_b_a_o_pan, + "b-a-o-opn" to CotType.CotType_b_a_o_opn, + "a-f-G" to CotType.CotType_a_f_G, + "a-f-G-U" to CotType.CotType_a_f_G_U, + "a-h-G" to CotType.CotType_a_h_G, + "a-u-G" to CotType.CotType_a_u_G, + "a-n-G" to CotType.CotType_a_n_G, + "b-m-r" to CotType.CotType_b_m_r, + "b-m-p-s-p-i" to CotType.CotType_b_m_p_s_p_i, + "u-d-f" to CotType.CotType_u_d_f, + "a-f-A-C-F" to CotType.CotType_a_f_A_C_F, + "a-f-A" to CotType.CotType_a_f_A, + "a-f-G-E-S" to CotType.CotType_a_f_G_E_S, + "b-m-p-s-p-loc" to CotType.CotType_b_m_p_s_p_loc, + "b-i-v" to CotType.CotType_b_i_v, + ) + + private val typeToString: Map = + stringToType.entries.associate { (k, v) -> v to k } + + private val stringToHow: Map = mapOf( + "h-e" to CotHow.CotHow_h_e, + "m-g" to CotHow.CotHow_m_g, + "h-g-i-g-o" to CotHow.CotHow_h_g_i_g_o, + "m-r" to CotHow.CotHow_m_r, + ) + + private val howToStr: Map = + stringToHow.entries.associate { (k, v) -> v to k } + + fun cotTypeFromString(s: String): CotType = stringToType[s] ?: CotType.CotType_Other + + fun cotTypeToString(type: CotType): String? = typeToString[type] + + fun cotHowFromString(s: String): CotHow = stringToHow[s] ?: CotHow.CotHow_Unspecified + + fun cotHowToString(how: CotHow): String? = howToStr[how] +} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt index 66fa34a93..acb4bb667 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt +++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt @@ -21,39 +21,30 @@ import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshConfigHandler -import org.meshtastic.core.repository.NodeRepository + import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.takserver.TAKMeshIntegration import org.meshtastic.core.takserver.TAKServer import org.meshtastic.core.takserver.TAKServerManager import org.meshtastic.core.takserver.TAKServerManagerImpl -import org.meshtastic.core.takserver.fountain.CoTHandler -import org.meshtastic.core.takserver.fountain.GenericCoTHandler +import org.meshtastic.core.takserver.createTAKServer @Module class CoreTakServerModule { - @Single fun provideTAKServer(dispatchers: CoroutineDispatchers): TAKServer = TAKServer(dispatchers = dispatchers) + @Single fun provideTAKServer(dispatchers: CoroutineDispatchers): TAKServer = createTAKServer(dispatchers = dispatchers) @Single fun provideTAKServerManager(takServer: TAKServer): TAKServerManager = TAKServerManagerImpl(takServer) - @Single - fun provideGenericCoTHandler(commandSender: CommandSender, takServerManager: TAKServerManager): CoTHandler = - GenericCoTHandler(commandSender, takServerManager) - @Single fun provideTAKMeshIntegration( takServerManager: TAKServerManager, commandSender: CommandSender, - nodeRepository: NodeRepository, serviceRepository: ServiceRepository, meshConfigHandler: MeshConfigHandler, - cotHandler: CoTHandler, ): TAKMeshIntegration = TAKMeshIntegration( takServerManager, commandSender, - nodeRepository, serviceRepository, meshConfigHandler, - cotHandler, ) } diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/FountainCodec.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/FountainCodec.kt deleted file mode 100644 index 4ed743ebf..000000000 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/FountainCodec.kt +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.takserver.fountain - -import co.touchlab.kermit.Logger -import kotlin.math.ceil -import kotlin.math.ln -import kotlin.math.sqrt -import kotlin.random.Random -import kotlin.time.Clock - -internal object FountainConstants { - val MAGIC = byteArrayOf(0x46, 0x54, 0x4E) // "FTN" - const val BLOCK_SIZE = 220 - const val DATA_HEADER_SIZE = 11 - const val FOUNTAIN_THRESHOLD = 233 - const val TRANSFER_TYPE_COT: Byte = 0x00 - const val ACK_TYPE_COMPLETE: Byte = 0x02 - const val ACK_PACKET_SIZE = 19 -} - -internal data class FountainBlock( - val seed: Int, // UInt16 - var indices: MutableSet, - var payload: ByteArray, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as FountainBlock - return seed == other.seed && indices == other.indices && payload.contentEquals(other.payload) - } - - override fun hashCode(): Int { - var result = seed - result = 31 * result + indices.hashCode() - result = 31 * result + payload.contentHashCode() - return result - } -} - -internal class FountainReceiveState( - val transferId: Int, // UInt24 - val k: Int, - val totalLength: Int, -) { - val blocks = mutableListOf() - private val createdAt = Clock.System.now().toEpochMilliseconds() - - fun addBlock(block: FountainBlock) { - if (blocks.none { it.seed == block.seed }) { - blocks.add(block) - } - } - - val isExpired: Boolean - get() = (Clock.System.now().toEpochMilliseconds() - createdAt) > 60_000 -} - -internal data class FountainDataHeader( - val transferId: Int, // UInt24 - val seed: Int, // UInt16 - val k: Int, // UInt8 - val totalLength: Int, // UInt16 -) - -internal data class FountainAck( - val transferId: Int, - val type: Byte, - val received: Int, - val needed: Int, - val dataHash: ByteArray, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as FountainAck - return transferId == other.transferId && - type == other.type && - received == other.received && - needed == other.needed && - dataHash.contentEquals(other.dataHash) - } - - override fun hashCode(): Int { - var result = transferId - result = 31 * result + type.toInt() - result = 31 * result + received - result = 31 * result + needed - result = 31 * result + dataHash.contentHashCode() - return result - } -} - -@Suppress("MagicNumber") -internal class JavaRandom(seed: Long) { - private var seed: Long = (seed xor 0x5DEECE66DL) and ((1L shl 48) - 1) - - private fun next(bits: Int): Int { - seed = (seed * 0x5DEECE66DL + 0xBL) and ((1L shl 48) - 1) - return (seed ushr (48 - bits)).toInt() - } - - fun nextInt(bound: Int): Int = when { - bound <= 0 -> 0 - (bound and -bound) == bound -> ((bound.toLong() * next(31).toLong()) shr 31).toInt() - else -> { - var bits: Int - var valResult: Int - do { - bits = next(31) - valResult = bits % bound - } while (bits - valResult + (bound - 1) < 0) - valResult - } - } - - fun nextDouble(): Double { - val high = next(26).toLong() - val low = next(27).toLong() - return ((high shl 27) + low).toDouble() / (1L shl 53).toDouble() - } -} - -@Suppress("MagicNumber", "TooManyFunctions") -internal class FountainCodec { - private val receiveStates = mutableMapOf() - - fun generateTransferId(): Int { - val random = Random.nextInt(0, 0xFFFFFF + 1) - val time = (Clock.System.now().toEpochMilliseconds() / 1000).toInt() and 0xFFFF - return (random xor time) and 0xFFFFFF - } - - fun encode(data: ByteArray, transferId: Int): List { - if (data.isEmpty()) { - Logger.w { "Fountain encode: empty data" } - return emptyList() - } - - val k = maxOf(1, ceil(data.size.toDouble() / FountainConstants.BLOCK_SIZE).toInt()) - val overhead = getAdaptiveOverhead(k) - val blocksToSend = maxOf(1, ceil(k.toDouble() * (1.0 + overhead)).toInt()) - - val sourceBlocks = splitIntoBlocks(data, k) - val packets = mutableListOf() - - for (i in 0 until blocksToSend) { - val seed = generateSeed(transferId, i) - val indices = generateBlockIndices(seed, k, i) - - var blockPayload = ByteArray(FountainConstants.BLOCK_SIZE) { 0 } - for (idx in indices) { - blockPayload = xor(blockPayload, sourceBlocks[idx]) - } - - val packet = buildDataBlock(transferId, seed, k, data.size, blockPayload) - packets.add(packet) - } - - Logger.i { "Fountain encode: ${data.size} bytes -> $k source blocks -> $blocksToSend packets" } - return packets - } - - private fun splitIntoBlocks(data: ByteArray, k: Int): List { - val blocks = mutableListOf() - for (i in 0 until k) { - val start = i * FountainConstants.BLOCK_SIZE - val end = minOf(start + FountainConstants.BLOCK_SIZE, data.size) - - if (start < data.size) { - val block = data.copyOfRange(start, end) - if (block.size < FountainConstants.BLOCK_SIZE) { - val padded = ByteArray(FountainConstants.BLOCK_SIZE) { 0 } - block.copyInto(padded) - blocks.add(padded) - } else { - blocks.add(block) - } - } else { - blocks.add(ByteArray(FountainConstants.BLOCK_SIZE) { 0 }) - } - } - return blocks - } - - private fun buildDataBlock(transferId: Int, seed: Int, k: Int, totalLength: Int, payload: ByteArray): ByteArray { - val packet = ByteArray(FountainConstants.DATA_HEADER_SIZE + payload.size) - - packet[0] = FountainConstants.MAGIC[0] - packet[1] = FountainConstants.MAGIC[1] - packet[2] = FountainConstants.MAGIC[2] - - packet[3] = ((transferId shr 16) and 0xFF).toByte() - packet[4] = ((transferId shr 8) and 0xFF).toByte() - packet[5] = (transferId and 0xFF).toByte() - - packet[6] = ((seed shr 8) and 0xFF).toByte() - packet[7] = (seed and 0xFF).toByte() - - packet[8] = (k and 0xFF).toByte() - - packet[9] = ((totalLength shr 8) and 0xFF).toByte() - packet[10] = (totalLength and 0xFF).toByte() - - payload.copyInto(packet, FountainConstants.DATA_HEADER_SIZE) - return packet - } - - fun isFountainPacket(data: ByteArray): Boolean { - if (data.size < 3) return false - return data[0] == FountainConstants.MAGIC[0] && - data[1] == FountainConstants.MAGIC[1] && - data[2] == FountainConstants.MAGIC[2] - } - - fun parseDataHeader(data: ByteArray): FountainDataHeader? { - if (data.size < FountainConstants.DATA_HEADER_SIZE || !isFountainPacket(data)) return null - - val transferId = - ((data[3].toInt() and 0xFF) shl 16) or ((data[4].toInt() and 0xFF) shl 8) or (data[5].toInt() and 0xFF) - val seed = ((data[6].toInt() and 0xFF) shl 8) or (data[7].toInt() and 0xFF) - val k = data[8].toInt() and 0xFF - val totalLength = ((data[9].toInt() and 0xFF) shl 8) or (data[10].toInt() and 0xFF) - - return FountainDataHeader(transferId, seed, k, totalLength) - } - - fun handleIncomingPacket(data: ByteArray): Pair? { - cleanupExpiredStates() - - val header = parseDataHeader(data) - if (header != null) { - val payload = data.copyOfRange(FountainConstants.DATA_HEADER_SIZE, data.size) - if (payload.size == FountainConstants.BLOCK_SIZE) { - return processValidIncomingPacket(header, payload) - } else { - Logger.w { "Invalid fountain payload size: ${payload.size}" } - } - } - return null - } - - private fun processValidIncomingPacket(header: FountainDataHeader, payload: ByteArray): Pair? { - val state = - receiveStates.getOrPut(header.transferId) { - FountainReceiveState(header.transferId, header.k, header.totalLength) - } - - val indices = regenerateIndices(header.seed, state.k, header.transferId) - val block = FountainBlock(header.seed, indices.toMutableSet(), payload) - state.addBlock(block) - - if (state.blocks.size >= state.k) { - val decoded = peelingDecode(state) - if (decoded != null) { - receiveStates.remove(header.transferId) - Logger.i { "Fountain decode complete: ${decoded.size} bytes from ${state.blocks.size} blocks" } - return Pair(decoded, header.transferId) - } - } - return null - } - - fun buildAck(transferId: Int, type: Byte, received: Int, needed: Int, dataHash: ByteArray): ByteArray { - val packet = ByteArray(FountainConstants.ACK_PACKET_SIZE) - - packet[0] = FountainConstants.MAGIC[0] - packet[1] = FountainConstants.MAGIC[1] - packet[2] = FountainConstants.MAGIC[2] - - packet[3] = ((transferId shr 16) and 0xFF).toByte() - packet[4] = ((transferId shr 8) and 0xFF).toByte() - packet[5] = (transferId and 0xFF).toByte() - - packet[6] = type - - packet[7] = ((received shr 8) and 0xFF).toByte() - packet[8] = (received and 0xFF).toByte() - - packet[9] = ((needed shr 8) and 0xFF).toByte() - packet[10] = (needed and 0xFF).toByte() - - val hashLen = minOf(8, dataHash.size) - dataHash.copyInto(packet, 11, 0, hashLen) - - return packet - } - - fun parseAck(data: ByteArray): FountainAck? { - if (data.size < FountainConstants.ACK_PACKET_SIZE || !isFountainPacket(data)) return null - - val transferId = - ((data[3].toInt() and 0xFF) shl 16) or ((data[4].toInt() and 0xFF) shl 8) or (data[5].toInt() and 0xFF) - val type = data[6] - val received = ((data[7].toInt() and 0xFF) shl 8) or (data[8].toInt() and 0xFF) - val needed = ((data[9].toInt() and 0xFF) shl 8) or (data[10].toInt() and 0xFF) - val dataHash = data.copyOfRange(11, 19) - - return FountainAck(transferId, type, received, needed, dataHash) - } - - private fun peelingDecode(state: FountainReceiveState): ByteArray? { - val decoded = mutableMapOf() - val workingBlocks = - state.blocks.map { FountainBlock(it.seed, it.indices.toMutableSet(), it.payload.copyOf()) }.toMutableList() - - var progress = true - while (progress && decoded.size < state.k) { - progress = processWorkingBlocks(workingBlocks, decoded) - } - - if (decoded.size < state.k) { - Logger.d { "Peeling decode incomplete: ${decoded.size}/${state.k} blocks decoded" } - return null - } - return assembleDecodedData(state, decoded) - } - - private fun processWorkingBlocks(workingBlocks: List, decoded: MutableMap): Boolean { - var progress = false - for (i in workingBlocks.indices) { - val block = workingBlocks[i] - val toRemove = mutableListOf() - for (idx in block.indices) { - val decodedBlock = decoded[idx] - if (decodedBlock != null) { - block.payload = xor(block.payload, decodedBlock) - toRemove.add(idx) - } - } - block.indices.removeAll(toRemove) - - if (block.indices.size == 1) { - val idx = block.indices.first() - if (!decoded.containsKey(idx)) { - decoded[idx] = block.payload - progress = true - } - } - } - return progress - } - - private fun assembleDecodedData(state: FountainReceiveState, decoded: Map): ByteArray? { - val result = ByteArray(state.k * FountainConstants.BLOCK_SIZE) - for (i in 0 until state.k) { - val block = decoded[i] ?: return null - block.copyInto(result, i * FountainConstants.BLOCK_SIZE) - } - return result.copyOfRange(0, state.totalLength) - } - - private fun cleanupExpiredStates() { - val expiredIds = receiveStates.filter { it.value.isExpired }.map { it.key } - for (id in expiredIds) { - receiveStates.remove(id) - Logger.d { "Cleaned up expired fountain state: $id" } - } - } - - private fun getAdaptiveOverhead(k: Int): Double = when { - k <= 10 -> 0.50 - k <= 50 -> 0.25 - else -> 0.15 - } - - private fun generateSeed(transferId: Int, blockIndex: Int): Int { - val combined = transferId * 31337 + blockIndex * 7919 - return combined and 0xFFFF - } - - private fun generateBlockIndices(seed: Int, k: Int, blockIndex: Int): Set { - val rng = JavaRandom(seed.toLong()) - val sampledDegree = sampleRobustSolitonDegree(rng, k) - val degree = if (blockIndex == 0) 1 else sampledDegree - return selectIndices(rng, k, degree) - } - - private fun regenerateIndices(seed: Int, k: Int, transferId: Int): Set { - val rng = JavaRandom(seed.toLong()) - val sampledDegree = sampleRobustSolitonDegree(rng, k) - val expectedSeed0 = generateSeed(transferId, 0) - val degree = if (seed == expectedSeed0) 1 else sampledDegree - return selectIndices(rng, k, degree) - } - - private fun selectIndices(rng: JavaRandom, k: Int, degree: Int): Set { - val indices = mutableSetOf() - while (indices.size < degree && indices.size < k) { - val idx = rng.nextInt(k) - indices.add(idx) - } - return indices - } - - private fun sampleRobustSolitonDegree(rng: JavaRandom, k: Int): Int { - val cdf = buildRobustSolitonCDF(k) - val u = rng.nextDouble() - for (d in 1..k) { - if (u <= cdf[d]) return d - } - return k - } - - private fun buildRobustSolitonCDF(k: Int, c: Double = 0.1, delta: Double = 0.5): DoubleArray { - if (k <= 0) return doubleArrayOf(1.0) - - val rho = DoubleArray(k + 1) - rho[1] = 1.0 / k.toDouble() - for (d in 2..k) { - rho[d] = 1.0 / (d.toDouble() * (d - 1).toDouble()) - } - - val rVal = c * ln(k.toDouble() / delta) * sqrt(k.toDouble()) - val tau = DoubleArray(k + 1) - val threshold = (k.toDouble() / rVal).toInt() - - for (d in 1..k) { - if (d < threshold) { - tau[d] = rVal / (d.toDouble() * k.toDouble()) - } else if (d == threshold) { - tau[d] = rVal * ln(rVal / delta) / k.toDouble() - } - } - - val mu = DoubleArray(k + 1) - var sum = 0.0 - for (d in 1..k) { - mu[d] = rho[d] + tau[d] - sum += mu[d] - } - - val cdf = DoubleArray(k + 1) - var cumulative = 0.0 - for (d in 1..k) { - cumulative += mu[d] / sum - cdf[d] = cumulative - } - return cdf - } - - private fun xor(a: ByteArray, b: ByteArray): ByteArray { - val result = ByteArray(maxOf(a.size, b.size)) - for (i in result.indices) { - val byteA = if (i < a.size) a[i] else 0 - val byteB = if (i < b.size) b[i] else 0 - result[i] = (byteA.toInt() xor byteB.toInt()).toByte() - } - return result - } -} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/GenericCoTHandler.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/GenericCoTHandler.kt deleted file mode 100644 index c6bfb5f1e..000000000 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/GenericCoTHandler.kt +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.takserver.fountain - -import co.touchlab.kermit.Logger -import kotlinx.coroutines.delay -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import okio.ByteString.Companion.toByteString -import org.meshtastic.core.model.DataPacket -import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.takserver.CoTMessage -import org.meshtastic.core.takserver.CoTXmlParser -import org.meshtastic.core.takserver.TAKServerManager -import org.meshtastic.core.takserver.toXml -import org.meshtastic.proto.PortNum -import kotlin.time.Clock - -class GenericCoTHandler(private val commandSender: CommandSender, private val takServerManager: TAKServerManager) : - CoTHandler { - companion object { - private const val INTER_PACKET_DELAY_MS = 100L - private const val ACK_RETRANSMIT_DELAY_MS = 50L - private const val PENDING_TRANSFER_TTL_MS = 60_000L - } - - private val fountainCodec = FountainCodec() - private val pendingTransfersMutex = Mutex() - private val pendingTransfers = mutableMapOf() - - private data class PendingTransfer( - val transferId: Int, - val totalBlocks: Int, - val dataHash: ByteArray, - val startTime: Long = Clock.System.now().toEpochMilliseconds(), - ) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - other as PendingTransfer - return transferId == other.transferId && - totalBlocks == other.totalBlocks && - dataHash.contentEquals(other.dataHash) && - startTime == other.startTime - } - - override fun hashCode(): Int { - var result = transferId - result = 31 * result + totalBlocks - result = 31 * result + dataHash.contentHashCode() - result = 31 * result + startTime.hashCode() - return result - } - } - - override suspend fun sendGenericCoT(cotMessage: CoTMessage) { - val xml = cotMessage.toXml() - val xmlBytes = xml.encodeToByteArray() - - val compressed = ZlibCodec.compress(xmlBytes) - if (compressed == null) { - Logger.w { "Failed to compress CoT to Zlib" } - return - } - - val payload = ByteArray(compressed.size + 1) - payload[0] = FountainConstants.TRANSFER_TYPE_COT - compressed.copyInto(payload, 1) - - Logger.d { "Generic CoT: type=${cotMessage.type}, xml=${xmlBytes.size}B, compressed=${payload.size}B" } - - if (payload.size < FountainConstants.FOUNTAIN_THRESHOLD) { - sendDirect(payload) - } else { - sendFountainCoded(payload) - } - } - - private fun sendDirect(payload: ByteArray) { - val dataPacket = - DataPacket( - to = DataPacket.ID_BROADCAST, - bytes = payload.toByteString(), - dataType = PortNum.ATAK_FORWARDER.value, - ) - commandSender.sendData(dataPacket) - Logger.i { "Sent generic CoT directly: ${payload.size} bytes on port 257" } - } - - private suspend fun sendFountainCoded(payload: ByteArray) { - val transferId = fountainCodec.generateTransferId() - val packets = fountainCodec.encode(payload, transferId) - val hash = CryptoCodec.sha256Prefix8(payload) - - pendingTransfersMutex.withLock { - pendingTransfers[transferId] = PendingTransfer(transferId, packets.size, hash) - } - - Logger.i { "Sending fountain-coded CoT: ${payload.size} bytes -> ${packets.size} blocks, xferId=$transferId" } - - for ((index, packetData) in packets.withIndex()) { - val dataPacket = - DataPacket( - to = DataPacket.ID_BROADCAST, - bytes = packetData.toByteString(), - dataType = PortNum.ATAK_FORWARDER.value, - ) - commandSender.sendData(dataPacket) - - if (index < packets.size - 1) { - delay(INTER_PACKET_DELAY_MS) // Inter-packet delay - } - } - } - - override suspend fun handleIncomingForwarderPacket(payload: ByteArray, senderNodeNum: Int) { - if (payload.isEmpty()) return - - if (fountainCodec.isFountainPacket(payload)) { - if (payload.size == FountainConstants.ACK_PACKET_SIZE) { - handleIncomingAck(payload, senderNodeNum) - } else { - handleFountainPacket(payload, senderNodeNum) - } - } else { - handleDirectPacket(payload, senderNodeNum) - } - } - - private fun handleDirectPacket(payload: ByteArray, senderNodeNum: Int) { - if (payload.size <= 1) return - val transferType = payload[0] - if (transferType != FountainConstants.TRANSFER_TYPE_COT) return - - val exiData = payload.copyOfRange(1, payload.size) - processDecompressedCoT(exiData, senderNodeNum) - } - - private suspend fun handleFountainPacket(payload: ByteArray, senderNodeNum: Int) { - fountainCodec.handleIncomingPacket(payload)?.let { (decodedData, transferId) -> - val hash = CryptoCodec.sha256Prefix8(decodedData) - sendFountainAck(transferId, hash, senderNodeNum) - delay(ACK_RETRANSMIT_DELAY_MS) - sendFountainAck(transferId, hash, senderNodeNum) - - if (decodedData.size > 1 && decodedData[0] == FountainConstants.TRANSFER_TYPE_COT) { - val exiData = decodedData.copyOfRange(1, decodedData.size) - processDecompressedCoT(exiData, senderNodeNum) - } - } - } - - private fun processDecompressedCoT(exiData: ByteArray, senderNodeNum: Int) { - val xmlBytes = ZlibCodec.decompress(exiData) ?: return - val xml = xmlBytes.decodeToString() - - val result = CoTXmlParser(xml).parse() - val cot = result.getOrNull() - - if (cot != null) { - takServerManager.broadcast(cot) - Logger.i { "Received generic CoT from node $senderNodeNum: ${cot.type}" } - } else { - Logger.w(result.exceptionOrNull() ?: Exception("Unknown parse error")) { "Failed to parse CoT XML" } - } - } - - private fun sendFountainAck(transferId: Int, hash: ByteArray, toNodeNum: Int) { - val ackPacket = - fountainCodec.buildAck( - transferId, - FountainConstants.ACK_TYPE_COMPLETE, - received = 0, - needed = 0, - dataHash = hash, - ) - - val dataPacket = - DataPacket( - to = toNodeNum.toString(), - bytes = ackPacket.toByteString(), - dataType = PortNum.ATAK_FORWARDER.value, - ) - commandSender.sendData(dataPacket) - Logger.d { "Sent fountain ACK for transfer $transferId" } - } - - private suspend fun handleIncomingAck(payload: ByteArray, senderNodeNum: Int) { - val ack = fountainCodec.parseAck(payload) ?: return - Logger.d { "Received fountain ACK: xferId=${ack.transferId}, type=${ack.type}, from $senderNodeNum" } - - pendingTransfersMutex.withLock { - cleanupStalePendingTransfersLocked() - val pending = pendingTransfers[ack.transferId] - if (pending != null) { - if (ack.type == FountainConstants.ACK_TYPE_COMPLETE) { - if (ack.dataHash.contentEquals(pending.dataHash)) { - Logger.i { "Fountain transfer ${ack.transferId} acknowledged by node $senderNodeNum" } - } else { - Logger.w { "Fountain ACK hash mismatch for transfer ${ack.transferId}" } - } - pendingTransfers.remove(ack.transferId) - } - } - } - } - - /** Must be called inside [pendingTransfersMutex]. */ - private fun cleanupStalePendingTransfersLocked() { - val now = Clock.System.now().toEpochMilliseconds() - val stale = pendingTransfers.filter { (_, v) -> now - v.startTime > PENDING_TRANSFER_TTL_MS }.keys - stale.forEach { id -> - pendingTransfers.remove(id) - Logger.d { "Evicted stale outbound pending transfer: $id" } - } - } -} diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTDetailStripperTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTDetailStripperTest.kt new file mode 100644 index 000000000..4e8ecfe6c --- /dev/null +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTDetailStripperTest.kt @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Covers the allowed/stripped element contract documented on [CoTDetailStripper]. If + * a test here starts failing because a new element type was added to the strip list, + * update the strip-list KDoc in [CoTDetailStripper] in the same change. + */ +class CoTDetailStripperTest { + + @Test + fun empty_input_returns_empty() { + assertEquals("", CoTDetailStripper.strip("")) + } + + @Test + fun preserves_contact_group_status_track() { + val input = """ + + <__group name="Cyan" role="Team Member"/> + + + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + assertTrue(stripped.contains(" + + + + + + + + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + assertTrue(stripped.contains(" is the biggest single bloat contributor for u-d-c-c events — it + // contains an and usually a styling child. Make sure the + // entire subtree goes, not just the opening tag. + val input = """ + + + + + + + + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + assertTrue(stripped.contains(" inside is also gone because we strip the whole subtree. + assertFalse(stripped.contains(" + + + + <__video url="rtsp://example.com/stream"/> + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + assertTrue(stripped.contains(" + + + + <__serverdestination destinations="0.0.0.0:4242:tcp:abc-123"/> + hello world + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + assertTrue(stripped.contains("<__chat"), "__chat must survive stripping") + assertTrue(stripped.contains(" + + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + // No leading/trailing whitespace. + assertEquals(stripped, stripped.trim()) + // No line breaks / indentation between elements. + assertFalse(stripped.contains("\n"), "output must not contain newlines: $stripped") + // Elements should be directly concatenated. + assertTrue( + stripped.contains("/><"), + "adjacent elements must be directly concatenated: $stripped", + ) + } + + @Test + fun handles_interleaved_strip_and_keep_elements() { + val input = """ + + + <__group name="Cyan" role="Team Member"/> + + + + + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + // All four keep-elements survive in order. + val contactIdx = stripped.indexOf("= 0, "contact missing") + assertTrue(groupIdx >= 0, "group missing") + assertTrue(statusIdx >= 0, "status missing") + assertTrue(trackIdx >= 0, "track missing") + assertTrue(contactIdx < groupIdx, "contact must come before group") + assertTrue(groupIdx < statusIdx, "group must come before status") + assertTrue(statusIdx < trackIdx, "status must come before track") + // None of the stripped elements linger. + assertFalse(stripped.contains("color"), "color stripped") + assertFalse(stripped.contains("shape"), "shape stripped") + assertFalse(stripped.contains("ellipse"), "ellipse stripped") + assertFalse(stripped.contains("labels_on"), "labels_on stripped") + } + + @Test + fun strips_tog_and_flow_tags() { + // is the rectangle "toggle" flag ATAK emits; <_flow-tags_> is TAK + // Server routing metadata. Both are pure bloat over the mesh. These are + // specifically tested because their names contain regex-special characters + // (`-`, `_`) and it's easy to typo the strip-list pattern. + val input = """ + + + <_flow-tags_ marti1="2014-10-28T22:40:15.341Z"/> + """.trimIndent() + val stripped = CoTDetailStripper.strip(input) + assertTrue(stripped.contains("<__group name='Cyan' role='Team Member'/>""" + + """""" + + """""" + + """""" + + """""" + + """""" + + """""" + + """""" + + """<__video url='rtsp://10.0.0.1:8554/stream'/>""" + val stripped = CoTDetailStripper.strip(realistic) + val before = realistic.length + val after = stripped.length + // Should shrink by at least 60% — most of the bytes were bloat. + assertTrue( + after < before * 0.4, + "expected >60% reduction; before=$before after=$after stripped='$stripped'", + ) + // Only the three "essential" elements survive. + assertTrue(stripped.contains(", , + // and bloat is stripped by CoTDetailStripper so the packet has any + // chance of fitting in a LoRa MTU. + val shapeXml = + """ + + + + + + + + + + + + + """ + .trimIndent() + + val result = CoTXmlParser(shapeXml).parse() + assertTrue(result.isSuccess) + val message = result.getOrNull()!! + + assertEquals("u-d-c-c", message.type) + val detail = message.parsedDetailXml + assertTrue(detail != null, "parsedDetailXml must be populated for unmapped types") + // Preserved: anything the stripper doesn't explicitly match, including contact. + assertTrue(detail.contains(" + + + + + + """ + .trimIndent() + val message = CoTXmlParser(xml).parse().getOrNull()!! + // sourceEventXml is used for diagnostic logging only — it must be the exact + // bytes we received so operators can see what ATAK actually sent. + assertEquals(xml, message.sourceEventXml) + // And it MUST still contain the stripped elements (since it is untouched). + assertTrue(message.sourceEventXml!!.contains(""), "sourceEventXml must be verbatim") + } + + @Test + fun `parsedDetailXml is null for self-closed detail element`() { + val xml = + """ + + + + + """ + .trimIndent() + val message = CoTXmlParser(xml).parse().getOrNull()!! + assertEquals(null, message.parsedDetailXml) + } } diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTXmlTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTXmlTest.kt index 7b6aa0ecd..a3cbda525 100644 --- a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTXmlTest.kt +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/CoTXmlTest.kt @@ -108,9 +108,14 @@ class CoTXmlTest { // ── Structure ───────────────────────────────────────────────────────────── @Test - fun `toXml includes XML declaration`() { + fun `toXml does not include XML declaration - CoT stream protocol`() { + // The CoT TCP streaming protocol requires a concatenated sequence of elements + // with NO XML declaration. A mid-stream tag breaks ATAK's parser and + // causes the client to disconnect as soon as the first real event arrives. val message = CoTMessage.pli(uid = "!1234", callsign = "X", latitude = 0.0, longitude = 0.0) - assertTrue(message.toXml().startsWith(". - */ -package org.meshtastic.core.takserver - -import org.meshtastic.core.takserver.TAKPacketConversion.toCoTMessage -import org.meshtastic.core.takserver.TAKPacketConversion.toTAKPacket -import org.meshtastic.proto.Contact -import org.meshtastic.proto.GeoChat -import org.meshtastic.proto.Group -import org.meshtastic.proto.MemberRole -import org.meshtastic.proto.PLI -import org.meshtastic.proto.Status -import org.meshtastic.proto.TAKPacket -import org.meshtastic.proto.Team -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class TAKPacketConversionTest { - - @Test - fun testCoTToTAKPacketPLI() { - val cot = - CoTMessage.pli( - uid = "!1234", - callsign = "Bob", - latitude = 45.0, - longitude = -90.0, - altitude = 100.0, - speed = 15.0, - course = 180.0, - team = "Blue", - role = "Team Member", - battery = 90, - ) - - val takPacket = cot.toTAKPacket() - assertNotNull(takPacket) - - assertEquals(false, takPacket.is_compressed) - assertEquals("Bob", takPacket.contact?.callsign) - assertEquals("!1234", takPacket.contact?.device_callsign) - assertEquals(Team.Blue, takPacket.group?.team) - assertEquals(MemberRole.TeamMember, takPacket.group?.role) - assertEquals(90, takPacket.status?.battery) - - assertNotNull(takPacket.pli) - assertEquals(450000000, takPacket.pli?.latitude_i) - assertEquals(-900000000, takPacket.pli?.longitude_i) - assertEquals(100, takPacket.pli?.altitude) - assertEquals(15, takPacket.pli?.speed) - assertEquals(180, takPacket.pli?.course) - } - - @Test - fun testTAKPacketToCoTMessagePLI() { - val takPacket = - TAKPacket( - is_compressed = false, - contact = Contact(callsign = "Alice", device_callsign = "!5678"), - group = Group(team = Team.Cyan, role = MemberRole.HQ), - status = Status(battery = 85), - pli = PLI(latitude_i = 300000000, longitude_i = -800000000, altitude = 50, speed = 5, course = 90), - ) - - val cot = takPacket.toCoTMessage() - assertNotNull(cot) - - assertEquals("!5678", cot.uid) - assertEquals("a-f-G-U-C", cot.type) - assertEquals(30.0, cot.latitude, 0.0001) - assertEquals(-80.0, cot.longitude, 0.0001) - assertEquals(50.0, cot.hae, 0.0001) - - assertEquals("Alice", cot.contact?.callsign) - assertEquals("Cyan", cot.group?.name) - assertEquals("HQ", cot.group?.role) - assertEquals(85, cot.status?.battery) - - assertNotNull(cot.track) - assertEquals(5.0, cot.track.speed) - assertEquals(90.0, cot.track.course) - } - - @Test - fun testCoTToTAKPacketChat() { - val cot = - CoTMessage.chat( - senderUid = "!1234", - senderCallsign = "Bob", - message = "Hello World", - chatroom = "All Chat Rooms", - ) - - val takPacket = cot.toTAKPacket() - assertNotNull(takPacket) - - assertNotNull(takPacket.chat) - assertEquals("Hello World", takPacket.chat?.message) - assertEquals("All Chat Rooms", takPacket.chat?.to) - } - - @Test - fun testChatSmugglesMessageId() { - val cot = - CoTMessage.chat( - senderUid = "my-device-123", - senderCallsign = "Bob", - message = "Hello World", - chatroom = "All Chat Rooms", - ) - - val msgId = cot.uid.split(".").last() - - val takPacket = cot.toTAKPacket() - assertNotNull(takPacket) - - val expectedDeviceCallsign = "my-device-123|$msgId" - assertEquals(expectedDeviceCallsign, takPacket.contact?.device_callsign) - assertEquals("Bob", takPacket.contact?.callsign) - assertEquals("Hello World", takPacket.chat?.message) - } - - @Test - fun testParseSmuggledMessageId() { - val takPacket = - TAKPacket( - is_compressed = false, - contact = Contact(callsign = "Alice", device_callsign = "alice-device-456|msg-789"), - chat = GeoChat(message = "Hi Bob", to = "Bob"), - ) - - val cot = takPacket.toCoTMessage() - assertNotNull(cot) - - assertEquals("GeoChat.alice-device-456.Bob.msg-789", cot.uid) - assertEquals("Alice", cot.chat?.senderCallsign) - assertEquals("Hi Bob", cot.chat?.message) - assertEquals("Bob", cot.chat?.chatroom) - } -} diff --git a/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketV2RawDetailTest.kt b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketV2RawDetailTest.kt new file mode 100644 index 000000000..65be83ca7 --- /dev/null +++ b/core/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKPacketV2RawDetailTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +import org.meshtastic.core.takserver.TAKPacketV2Conversion.toCoTMessage +import org.meshtastic.core.takserver.TAKPacketV2Conversion.toTAKPacketV2 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Verifies the `raw_detail` fallback round-trip for CoT types that don't fit any structured + * [org.meshtastic.proto.TAKPacketV2] payload (PLI, GeoChat, Aircraft). + * + * Prior to this, ATAK user-drawn elements like `u-d-c-c` would be silently dropped by + * [TAKPacketV2Conversion.toTAKPacketV2] with `"Cannot convert CoT to TAKPacketV2 for type ..."`. + */ +class TAKPacketV2RawDetailTest { + + @Test + fun udcc_round_trips_via_raw_detail() { + // Note: `` / `` / `` in the input are deliberately + // stripped by [CoTDetailStripper] before being placed in raw_detail, because + // they blow up the wire size beyond the LoRa MTU. We keep `` here so + // we have something non-trivial to verify round-tripped. + val shapeXml = """ + + + + + + + + + + + + """.trimIndent() + + // Parse → convert to TAKPacketV2 + val cotMessage = CoTXmlParser(shapeXml).parse().getOrNull() + assertNotNull(cotMessage, "CoT XML must parse successfully") + val takPacketV2 = cotMessage.toTAKPacketV2() + assertNotNull(takPacketV2, "u-d-c-c must convert to TAKPacketV2 (not drop)") + + // raw_detail must be populated; structured payloads must be null. + assertNotNull(takPacketV2.raw_detail, "raw_detail must hold the detail bytes") + assertNull(takPacketV2.pli, "PLI payload must not be set for u-d-c-c") + assertNull(takPacketV2.chat, "chat payload must not be set for u-d-c-c") + assertEquals("u-d-c-c", takPacketV2.cot_type_str.ifEmpty { "u-d-c-c" }) + // Stripping must have fired: the raw_detail bytes must NOT contain the + // shape/labels_on fragments we put in the input. + val rawDetailBytes = takPacketV2.raw_detail!!.utf8() + assertFalse(rawDetailBytes.contains("shape"), "shape must be stripped from raw_detail: $rawDetailBytes") + assertFalse(rawDetailBytes.contains("labels_on"), "labels_on must be stripped: $rawDetailBytes") + assertTrue(rawDetailBytes.contains("contact"), "contact must survive: $rawDetailBytes") + + // Convert back to CoTMessage + val roundTripped = takPacketV2.toCoTMessage() + assertNotNull(roundTripped, "TAKPacketV2 with raw_detail must convert back to CoTMessage") + assertEquals("u-d-c-c", roundTripped.type) + assertEquals(45.5, roundTripped.latitude, 0.0001) + assertEquals(-90.25, roundTripped.longitude, 0.0001) + + // Serialize to XML; the surviving (stripped) content must be present. + val xmlOut = roundTripped.toXml() + assertTrue(xmlOut.contains("type='u-d-c-c'"), "type must survive: $xmlOut") + assertTrue(xmlOut.contains("ALPHA01"), "contact callsign must survive: $xmlOut") + assertFalse(xmlOut.contains(" + + + + <__group name="Red" role="Team Member"/> + + + + """.trimIndent() + + val cotMessage = CoTXmlParser(xml).parse().getOrNull()!! + val takPacketV2 = cotMessage.toTAKPacketV2()!! + val roundTripped = takPacketV2.toCoTMessage()!! + + assertNull(roundTripped.contact, "contact must be null on raw_detail path (lives inside rawDetailXml)") + assertNull(roundTripped.group, "group must be null on raw_detail path") + assertNull(roundTripped.status, "status must be null on raw_detail path") + + val xmlOut = roundTripped.toXml() + // Exactly one (from the round-tripped raw detail), not two. + assertEquals(1, xmlOut.split(". - */ -package org.meshtastic.core.takserver.fountain - -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class FountainCodecTest { - - private fun createCodec() = FountainCodec() - - @Test - fun `test encode and decode small payload`() { - val codec = createCodec() - val originalData = "Hello, TAK! This is a test payload.".encodeToByteArray() - // Use a fixed transfer ID for deterministic peeling decode - val transferId = 42 - - val packets = codec.encode(originalData, transferId) - assertTrue(packets.isNotEmpty(), "Encoding should produce packets") - - var decodedResult: Pair? = null - for (packet in packets) { - val result = codec.handleIncomingPacket(packet) - if (result != null) { - decodedResult = result - break - } - } - - assertNotNull(decodedResult, "Should successfully decode payload") - assertEquals(transferId, decodedResult.second, "Transfer ID should match") - assertContentEquals(originalData, decodedResult.first, "Decoded data should match original") - } - - @Test - fun `test encode and decode larger payload with packet loss`() { - val codec = createCodec() - // Create a payload larger than BLOCK_SIZE (220 bytes) - val originalData = ByteArray(1024) { (it % 256).toByte() } - // Use a fixed transfer ID for deterministic peeling decode. - // Random transfer IDs cause ~14% flake rate because the robust soliton - // distribution with k=5 and 50% overhead doesn't always produce a - // decodable set of encoded blocks via the peeling algorithm. - val transferId = 42 - - val packets = codec.encode(originalData, transferId) - assertTrue(packets.size > 4, "Should have multiple packets for large payload") - - var decodedResult: Pair? = null - - // Process all packets - fountain codes are designed to handle packet loss - // by receiving enough encoded packets to reconstruct the original data - for (packet in packets) { - val result = codec.handleIncomingPacket(packet) - if (result != null) { - decodedResult = result - break - } - } - - assertNotNull(decodedResult, "Should successfully decode payload with sufficient packets") - assertEquals(transferId, decodedResult.second, "Transfer ID should match") - assertContentEquals(originalData, decodedResult.first, "Decoded data should match original") - } - - @Test - fun `test build and parse ACK`() { - val codec = createCodec() - val transferId = 123456 - val type = FountainConstants.ACK_TYPE_COMPLETE - val received = 5 - val needed = 0 - val dataHash = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8) - - val ackPacket = codec.buildAck(transferId, type, received, needed, dataHash) - assertTrue(codec.isFountainPacket(ackPacket), "ACK should be recognized as a Fountain packet") - - val parsedAck = codec.parseAck(ackPacket) - assertNotNull(parsedAck, "ACK should be parseable") - assertEquals(transferId, parsedAck.transferId) - assertEquals(type, parsedAck.type) - assertEquals(received, parsedAck.received) - assertEquals(needed, parsedAck.needed) - assertContentEquals(dataHash, parsedAck.dataHash) - } - - @Test - fun `test invalid packet handling`() { - val codec = createCodec() - val invalidPacket = byteArrayOf(0x00, 0x01, 0x02, 0x03) - assertFalse(codec.isFountainPacket(invalidPacket), "Should reject invalid magic bytes") - assertNull(codec.parseDataHeader(invalidPacket), "Should not parse invalid header") - assertNull(codec.handleIncomingPacket(invalidPacket), "Should handle invalid packet gracefully") - } -} diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/CodecExpect.kt b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt similarity index 72% rename from core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/CodecExpect.kt rename to core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt index 65d7077f9..9feb78cca 100644 --- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/fountain/CodecExpect.kt +++ b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt @@ -14,14 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.takserver.fountain +package org.meshtastic.core.takserver -internal expect object ZlibCodec { - fun compress(data: ByteArray): ByteArray? - - fun decompress(data: ByteArray): ByteArray? -} - -internal expect object CryptoCodec { - fun sha256Prefix8(data: ByteArray): ByteArray +/** iOS no-op — iTAK accepts routes via TCP streaming, no data package needed. */ +internal actual object AtakFileWriter { + actual fun writeToImportDir(fileName: String, zipBytes: ByteArray): Boolean = false } diff --git a/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TAKServerIos.kt b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TAKServerIos.kt new file mode 100644 index 000000000..0a48450a3 --- /dev/null +++ b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TAKServerIos.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.meshtastic.core.di.CoroutineDispatchers + +/** + * iOS KMP stub. The real iOS TAK server lives in Meshtastic-Apple + * (`Meshtastic/Helpers/TAK/TAKServerManager.swift`) and uses Apple's + * `Network.framework` / `NWListener` + mTLS directly, not this KMP module. + * + * We provide a no-op implementation here so that the shared `core:takserver` + * module still compiles for the iOS KMP targets. Any iOS-side consumer of this + * module would never actually call into this path — iOS bypasses the KMP + * `TAKServer` interface entirely. + */ +private class NoopTAKServer : TAKServer { + private val _connectionCount = MutableStateFlow(0) + override val connectionCount: StateFlow = _connectionCount.asStateFlow() + override var onMessage: ((CoTMessage) -> Unit)? = null + + override suspend fun start(scope: CoroutineScope): Result = Result.success(Unit) + override fun stop() = Unit + override suspend fun broadcast(cotMessage: CoTMessage) = Unit + override suspend fun hasConnections(): Boolean = false +} + +actual fun createTAKServer(dispatchers: CoroutineDispatchers, port: Int): TAKServer = NoopTAKServer() diff --git a/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt new file mode 100644 index 000000000..9e246777b --- /dev/null +++ b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + */ + +package org.meshtastic.core.takserver + +import org.meshtastic.proto.TAKPacketV2 + +/** + * iOS stub for TakV2Compressor. + * TODO: Replace with Swift SDK integration via interop. + */ +internal actual object TakV2Compressor { + + actual val MAX_DECOMPRESSED_SIZE: Int = 4096 + actual val DICT_ID_NON_AIRCRAFT: Int = 0 + actual val DICT_ID_AIRCRAFT: Int = 1 + actual val DICT_ID_UNCOMPRESSED: Int = 0xFF + + actual fun compress(packet: TAKPacketV2): ByteArray { + // iOS: Send uncompressed for now (TAK_TRACKER mode) + val protobufBytes = TAKPacketV2.ADAPTER.encode(packet) + val wirePayload = ByteArray(1 + protobufBytes.size) + wirePayload[0] = DICT_ID_UNCOMPRESSED.toByte() + protobufBytes.copyInto(wirePayload, 1) + return wirePayload + } + + actual fun decompressToXml(wirePayload: ByteArray): String { + // iOS stub: decompress and convert via toCoTMessage().toXml() as fallback + val packet = decompress(wirePayload) + return packet.toString() // placeholder — iOS uses Swift SDK directly + } + + actual fun decompress(wirePayload: ByteArray): TAKPacketV2 { + require(wirePayload.size >= 2) { "Wire payload too short: ${wirePayload.size} bytes" } + + val flagsByte = wirePayload[0].toInt() and 0xFF + val payloadBytes = wirePayload.copyOfRange(1, wirePayload.size) + + // iOS stub: only support uncompressed (0xFF) payloads + if (flagsByte != DICT_ID_UNCOMPRESSED) { + throw UnsupportedOperationException( + "iOS zstd decompression not yet implemented. Received dict ID: ${flagsByte and 0x3F}" + ) + } + + require(payloadBytes.size <= MAX_DECOMPRESSED_SIZE) { + "Payload size ${payloadBytes.size} exceeds limit $MAX_DECOMPRESSED_SIZE" + } + + return TAKPacketV2.ADAPTER.decode(payloadBytes) + } +} diff --git a/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt b/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt deleted file mode 100644 index 4473fc521..000000000 --- a/core/takserver/src/iosMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.takserver.fountain - -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value -import platform.CoreCrypto.CC_SHA256 -import platform.CoreCrypto.CC_SHA256_DIGEST_LENGTH -import platform.zlib.Z_BUF_ERROR -import platform.zlib.Z_OK -import platform.zlib.compress -import platform.zlib.compressBound -import platform.zlib.uncompress - -internal actual object ZlibCodec { - @OptIn(ExperimentalForeignApi::class) - actual fun compress(data: ByteArray): ByteArray? { - if (data.isEmpty()) return ByteArray(0) - - return memScoped { - val destLen = alloc() - destLen.value = compressBound(data.size.toULong()) - - val destBuffer = ByteArray(destLen.value.toInt()) - - val result = - destBuffer.usePinned { destPin -> - data.usePinned { srcPin -> - compress( - destPin.addressOf(0).reinterpret(), - destLen.ptr, - srcPin.addressOf(0).reinterpret(), - data.size.toULong(), - ) - } - } - - if (result == Z_OK) { - destBuffer.copyOf(destLen.value.toInt()) - } else { - null - } - } - } - - @OptIn(ExperimentalForeignApi::class) - actual fun decompress(data: ByteArray): ByteArray? { - if (data.isEmpty()) return ByteArray(0) - - var currentSize = data.size * 4 - var maxAttempts = 5 - - while (maxAttempts > 0) { - val success = memScoped { - val destLen = alloc() - destLen.value = currentSize.toULong() - - val destBuffer = ByteArray(currentSize) - - val result = - destBuffer.usePinned { destPin -> - data.usePinned { srcPin -> - uncompress( - destPin.addressOf(0).reinterpret(), - destLen.ptr, - srcPin.addressOf(0).reinterpret(), - data.size.toULong(), - ) - } - } - - if (result == Z_OK) { - return@memScoped destBuffer.copyOf(destLen.value.toInt()) - } else if (result == Z_BUF_ERROR) { - currentSize *= 2 - maxAttempts-- - null - } else { - maxAttempts = 0 - null - } - } - if (success != null) return success - } - return null - } -} - -internal actual object CryptoCodec { - @OptIn(ExperimentalForeignApi::class) - actual fun sha256Prefix8(data: ByteArray): ByteArray { - val digest = ByteArray(CC_SHA256_DIGEST_LENGTH) - if (data.isNotEmpty()) { - data.usePinned { dataPin -> - digest.usePinned { digestPin -> - CC_SHA256(dataPin.addressOf(0), data.size.toUInt(), digestPin.addressOf(0).reinterpret()) - } - } - } else { - digest.usePinned { digestPin -> CC_SHA256(null, 0u, digestPin.addressOf(0).reinterpret()) } - } - return digest.copyOf(8) - } -} diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt new file mode 100644 index 000000000..4f001de8b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/AtakFileWriter.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.takserver + +import co.touchlab.kermit.Logger +import java.io.File + +/** + * Android implementation — writes route data packages to ATAK's monitored + * auto-import directory. Tries multiple locations in order of preference: + * 1. `/sdcard/atak/tools/datapackage/` (ATAK monitors this) + * 2. `/sdcard/Download/` (user can manually import from here) + */ +@Suppress("TooGenericExceptionCaught") +internal actual object AtakFileWriter { + + actual fun writeToImportDir(fileName: String, zipBytes: ByteArray): Boolean { + // Use hardcoded paths — on Android /sdcard/ maps to external storage. + // On JVM desktop these paths don't exist and the fallback returns false. + val targets = listOf( + File("/sdcard/atak/tools/datapackage"), + File("/sdcard/Download"), + ) + + for (dir in targets) { + try { + if (!dir.exists()) dir.mkdirs() + val target = File(dir, fileName) + target.writeBytes(zipBytes) + Logger.i { "Route data package written: $fileName (${zipBytes.size} bytes) → ${target.absolutePath}" } + return true + } catch (e: Exception) { + Logger.d { "Cannot write to ${dir.absolutePath}: ${e.message}" } + } + } + + Logger.w { "Failed to write route data package to any ATAK import directory" } + return false + } +} diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt new file mode 100644 index 000000000..075deb5ac --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKClientConnection.kt @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +@file:Suppress("TooManyFunctions", "TooGenericExceptionCaught") + +package org.meshtastic.core.takserver + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.io.BufferedOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.net.Socket +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.Volatile +import kotlin.random.Random +import kotlin.time.Clock +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Instant +import kotlinx.coroutines.isActive as coroutineIsActive + +/** + * Per-client state machine for a connected TAK client (ATAK / iTAK / WinTAK). + * + * This is the jvmAndroidMain implementation, using plain `java.net.Socket` (which is also + * the base class of [javax.net.ssl.SSLSocket] from [TAKServerJvm]) with blocking + * `InputStream`/`OutputStream` I/O wrapped in [Dispatchers.IO] coroutines. + * + * Responsibilities: + * - TAK protocol negotiation handshake (`t-x-takp-v` / `-q` / `-r`) + * - Read loop that frames `` elements off the stream via [CoTXmlFrameBuffer] + * - Keepalive loop that emits a `t-x-d-d` event every [TAK_KEEPALIVE_INTERVAL_MS] + * - Serializing writes under a mutex so interleaved broadcasts never corrupt the XML stream + * - Lifecycle reporting up to [TAKServerJvm] via [onEvent] (`Connected`, `Disconnected`, + * `Error`, `ClientInfoUpdated`, `Message`) + */ +internal class TAKClientConnection( + private val socket: Socket, + val clientInfo: TAKClientInfo, + private val onEvent: (TAKConnectionEvent) -> Unit, + private val scope: CoroutineScope, + private val ioDispatcher: CoroutineDispatcher, +) { + private var currentClientInfo = clientInfo + private val frameBuffer = CoTXmlFrameBuffer() + + private val inputStream: InputStream = socket.getInputStream() + // Wrap the OutputStream in a BufferedOutputStream so that multiple small writes + // (we emit a full XML event per write) coalesce into one syscall; flush() after + // each event to push the bytes through TLS immediately. + private val outputStream: OutputStream = BufferedOutputStream(socket.getOutputStream()) + private val writeMutex = Mutex() + + /** + * Per-connection child scope. Every coroutine this class launches — the read loop, + * the keepalive loop, and every single send — is attached to [connectionScope] so + * that [emitDisconnected] can tear the whole connection down with one + * `connectionScope.cancel()`. + * + * Why this is critical: [broadcast] in [TAKServerJvm] fires `connection.send()` on + * **every** connected client for **every** CoT event coming off the mesh (and with + * a 56-node nodeDB each `nodeDBbyNum` emission fans out to ~56 broadcasts). If + * [sendXml] launched those writes on the server-level [scope] — as the previous + * implementation did — a single dead connection could accumulate hundreds of + * in-flight write coroutines before it was removed from [TAKServerJvm.connections], + * and every one of them would spin up, hit the closed TLS socket, and log + * `SocketException: Socket closed` from `BufferedOutputStream.flush()`. Scoping + * writes to [connectionScope] means cancelling the scope wipes the entire backlog. + * + * Uses a [SupervisorJob] child of [scope]'s job so a single write failure doesn't + * cascade-cancel other connections on the same server. + */ + private val connectionScope: CoroutineScope = + CoroutineScope(SupervisorJob(scope.coroutineContext[Job]) + ioDispatcher) + + /** Guards against emitting [TAKConnectionEvent.Disconnected] more than once. */ + private val disconnectedEmitted = AtomicBoolean(false) + + /** + * Fail-fast flag checked at the top of [sendXml] so racing broadcasts against a + * dead connection don't even allocate a coroutine. + */ + @Volatile private var closed = false + + fun start() { + onEvent(TAKConnectionEvent.Connected(currentClientInfo)) + sendProtocolSupport() + + connectionScope.launch { readLoop() } + connectionScope.launch { keepaliveLoop() } + } + + private fun sendProtocolSupport() { + val serverUid = "Meshtastic-TAK-Server-${Random.nextInt().toString(TAK_HEX_RADIX)}" + val now = Clock.System.now() + val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds + val detail = + """ + + + + """ + .trimIndent() + sendXmlInternal(buildEventXml(uid = serverUid, type = "t-x-takp-v", now = now, stale = stale, detail = detail)) + } + + private suspend fun readLoop() { + try { + val buffer = ByteArray(TAK_XML_READ_BUFFER_SIZE) + while (connectionScope.coroutineIsActive && !closed && !socket.isClosed) { + // Blocking read off the TLS input stream — must run on the IO dispatcher. + val bytesRead = withContext(ioDispatcher) { inputStream.read(buffer) } + if (bytesRead > 0) { + processReceivedData(buffer.copyOfRange(0, bytesRead)) + } else if (bytesRead == -1) { + break // EOF: remote peer closed the connection cleanly + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + if (!closed) { + Logger.w(e) { "TAK client read error: ${currentClientInfo.id}" } + emitDisconnected(TAKConnectionEvent.Error(e)) + } + return + } + emitDisconnected(TAKConnectionEvent.Disconnected) + } + + private suspend fun keepaliveLoop() { + while (connectionScope.coroutineIsActive && !closed && !socket.isClosed) { + kotlinx.coroutines.delay(TAK_KEEPALIVE_INTERVAL_MS) + if (closed) break + sendKeepalive() + } + } + + private fun sendKeepalive() { + val now = Clock.System.now() + val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds + sendXmlInternal(buildEventXml(uid = "takPong", type = "t-x-d-d", now = now, stale = stale, detail = "")) + } + + /** Respond to ATAK's `t-x-c-t` ping with a pong to reset its RX timeout. */ + private fun sendPong() { + val now = Clock.System.now() + val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds + sendXmlInternal(buildEventXml(uid = "takPong", type = "t-x-c-t-r", now = now, stale = stale, detail = "")) + } + + private fun processReceivedData(newData: ByteArray) { + frameBuffer.append(newData).forEach { xmlString -> parseAndHandleMessage(xmlString) } + } + + private fun parseAndHandleMessage(xmlString: String) { + // Fast-path: detect keepalive pings before full XML parsing to avoid + // both the parse overhead and the noisy RAW CoT IN log line every 4.5s. + if (xmlString.contains("t-x-c-t") || xmlString.contains("uid=\"ping\"")) { + sendPong() + return + } + + // Full raw CoT XML from the ATAK client, before any parsing happens. + // Emitted at debug level so it's always available in logcat for field + // debugging without needing a release rebuild. Not truncated — the + // reader of this log needs the complete event to reproduce issues. + Logger.d { "RAW CoT IN (TCP ${currentClientInfo.id}): $xmlString" } + + val parser = CoTXmlParser(xmlString) + val result = parser.parse() + + result.onSuccess { cotMessage -> + when { + cotMessage.type.startsWith("t-x-takp") -> { + handleProtocolControl(cotMessage.type, xmlString) + return + } + else -> { + cotMessage.contact?.let { contact -> + val updatedClientInfo = + currentClientInfo.copy( + callsign = currentClientInfo.callsign ?: contact.callsign, + uid = currentClientInfo.uid ?: cotMessage.uid, + ) + if (updatedClientInfo != currentClientInfo) { + currentClientInfo = updatedClientInfo + onEvent(TAKConnectionEvent.ClientInfoUpdated(updatedClientInfo)) + } + } + onEvent(TAKConnectionEvent.Message(cotMessage, currentClientInfo)) + } + } + } + } + + private fun handleProtocolControl(type: String, xmlString: String) { + if (type == "t-x-takp-q") { + sendProtocolResponse() + } else { + Logger.d { "Unhandled protocol control type: $type (raw=$xmlString)" } + } + } + + private fun sendProtocolResponse() { + val serverUid = "Meshtastic-TAK-Server-${Random.nextInt().toString(TAK_HEX_RADIX)}" + val now = Clock.System.now() + val stale = now + TAK_KEEPALIVE_INTERVAL_MS.milliseconds + val detail = + """ + + + + """ + .trimIndent() + sendXmlInternal(buildEventXml(uid = serverUid, type = "t-x-takp-r", now = now, stale = stale, detail = detail)) + } + + fun send(cotMessage: CoTMessage) { + if (closed) return + val xml = cotMessage.toXml() + // Full raw CoT XML being shipped out to the ATAK client, after the + // CoTMessage → XML round trip. This is the exact bytes the client + // will receive, so logging here closes the debugging loop with the + // matching RAW CoT IN line on the receiver. + Logger.d { "RAW CoT OUT (TCP ${currentClientInfo.id}): $xml" } + sendXmlInternal(xml) + } + + private fun buildEventXml(uid: String, type: String, now: Instant, stale: Instant, detail: String): String { + val detailContent = if (detail.isBlank()) "" else "$detail" + val point = """""" + return """""" + + point + + detailContent + + "" + } + + /** Send raw XML directly to this client. Used for mesh-originated messages + * that bypass CoTMessage parsing to preserve shape detail elements. */ + fun sendRawXml(xml: String) { + Logger.d { "RAW CoT OUT (TCP ${currentClientInfo.id}): [raw] $xml" } + sendXmlInternal(xml) + } + + private fun sendXmlInternal(xml: String) { + // Fail-fast synchronous check BEFORE allocating a coroutine. This is the hot path + // for broadcasts — see the scope doc above for why it matters. + if (closed) return + connectionScope.launch { + // Re-check inside the coroutine: we may have been cancelled or marked closed + // between the launch and the dispatcher picking this up. + if (closed) return@launch + try { + writeMutex.withLock { + if (closed || socket.isClosed) return@withLock + val bytes = xml.toByteArray(Charsets.UTF_8) + // Blocking write on TLS output must run on the IO dispatcher + withContext(ioDispatcher) { + outputStream.write(bytes) + outputStream.flush() + } + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + // Don't spam on writes that raced a disconnect we already observed. + if (!closed) { + Logger.w(e) { "TAK client send error: ${currentClientInfo.id}" } + emitDisconnected(TAKConnectionEvent.Error(e)) + } + } + } + } + + fun close() { + frameBuffer.clear() + emitDisconnected(TAKConnectionEvent.Disconnected) + } + + /** + * Emits [event] (expected to be [TAKConnectionEvent.Disconnected] or [TAKConnectionEvent.Error]) at most once + * across all code paths, then tears down the per-connection coroutines and socket. + * + * This is the ONLY place the connection's entire coroutine scope — keepalive loop, + * read loop, and any in-flight send coroutines — gets cancelled when the *remote* + * peer closes the TLS stream. Without this, Java's [Socket.isClosed] only reports + * whether *our* side called close(), so the keepalive loop's `!socket.isClosed` + * guard never fires, the broadcast fanout keeps launching writes onto the dead + * socket via [sendXml], and every iteration logs `SSLOutputStream / Socket closed`. + * Before [closed] + [connectionScope.cancel] were added, a single session with a + * few reconnects accumulated hundreds of zombie write coroutines each spamming + * errors in parallel. + * + * Idempotent via [AtomicBoolean.compareAndSet], so racing calls from [readLoop], + * [keepaliveLoop], and [sendXml] all converge on a single teardown. + */ + private fun emitDisconnected(event: TAKConnectionEvent) { + if (disconnectedEmitted.compareAndSet(false, true)) { + // Set the fail-fast flag BEFORE emitting the event. [TAKServerJvm] will + // schedule an async map removal on receipt, and any broadcast racing the + // removal must see `closed = true` when it hits [send] / [sendXml]. + closed = true + onEvent(event) + // Cancel the whole scope — readLoop, keepaliveLoop, and every queued or + // in-flight sendXml coroutine. Any write blocked in the syscall will throw + // on the next iteration because we close the socket next. + connectionScope.cancel() + runCatching { socket.close() } + } + } +} diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt new file mode 100644 index 000000000..161b2d450 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TAKServerJvm.kt @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +@file:Suppress("TooGenericExceptionCaught") + +package org.meshtastic.core.takserver + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.meshtastic.core.di.CoroutineDispatchers +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import javax.net.ssl.SSLServerSocket +import kotlin.random.Random +import kotlinx.coroutines.isActive as coroutineIsActive + +/** + * JSSE-backed TLS TAK server. Matches the Meshtastic-Apple (iOS) implementation: + * + * - Binds `127.0.0.1:8089` (loopback only — no remote device can reach the server) + * - TLS 1.2+ with the bundled server.p12 identity + * - Mutual TLS: clients MUST present a certificate chaining to the bundled ca.pem + * - `SO_REUSEADDR` on the listen socket so an app restart doesn't hit + * `BindException: Address already in use` while the previous socket is in + * `TIME_WAIT` + * - Per-connection [TAKClientConnection] running on [CoroutineDispatchers.io] + * + * If the bundled certificates fail to load (e.g. packaging regression), the server + * refuses to start rather than silently falling back to plain TCP — that failure mode + * would produce exactly the symptom the user was debugging ("ATAK never connects"). + */ +internal class TAKServerJvm( + private val dispatchers: CoroutineDispatchers, + private val port: Int = DEFAULT_TAK_PORT, +) : TAKServer { + + private var serverSocket: ServerSocket? = null + private var running = false + private var serverScope: CoroutineScope? = null + private var acceptJob: Job? = null + private val connectionsMutex = Mutex() + private val connections = mutableMapOf() + + private val _connectionCount = MutableStateFlow(0) + override val connectionCount: StateFlow = _connectionCount.asStateFlow() + + override var onMessage: ((CoTMessage, TAKClientInfo?) -> Unit)? = null + override var onClientConnected: (() -> Unit)? = null + + override suspend fun start(scope: CoroutineScope): Result { + if (running) { + Logger.w { "TAK Server already running on port $port" } + return Result.success(Unit) + } + + val sslContext = TakCertLoader.getServerSslContext() + ?: return Result.failure( + IllegalStateException( + "TAK Server: bundled TLS certificates could not be loaded; refusing to start", + ) + ) + + return try { + serverScope = scope + + // Bind on the IO dispatcher — bind() can briefly block. + val boundSocket = withContext(dispatchers.io) { + val factory = sslContext.serverSocketFactory + // Use the address-specific overload so we bind to loopback only. + val loopback = InetAddress.getByName("127.0.0.1") + // backlog of 4 is plenty for local TAK clients + val tls = factory.createServerSocket(port, 4, loopback) as SSLServerSocket + configureTlsServerSocket(tls) + tls + } + serverSocket = boundSocket + running = true + Logger.i { "TAK Server listening on 127.0.0.1:$port (TLS, mTLS enforced)" } + + acceptJob = scope.launch(dispatchers.io) { acceptLoop() } + Result.success(Unit) + } catch (e: Exception) { + Logger.e(e) { "Failed to bind TAK Server to 127.0.0.1:$port" } + running = false + serverSocket?.runCatching { close() } + serverSocket = null + Result.failure(e) + } + } + + private fun configureTlsServerSocket(tls: SSLServerSocket) { + // Minimum TLS 1.2 — matches iOS. + val protocols = tls.supportedProtocols.filter { it == "TLSv1.2" || it == "TLSv1.3" } + if (protocols.isNotEmpty()) { + tls.enabledProtocols = protocols.toTypedArray() + } + // Require client certificate (mTLS) — matches + // `sec_protocol_options_set_peer_authentication_required` on iOS. + tls.needClientAuth = true + // Enable address reuse so restart doesn't hit TIME_WAIT on the port. + tls.reuseAddress = true + } + + private suspend fun acceptLoop() { + val scope = serverScope ?: return + while (running && scope.coroutineIsActive) { + try { + val clientSocket = withContext(dispatchers.io) { + serverSocket?.accept() + } + if (clientSocket != null) { + handleConnection(clientSocket) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + // Bind was lost or the socket was closed under us — back off, then retry. + if (running) { + Logger.w(e) { "TAK server accept loop iteration failed: ${e.message}" } + } + delay(TAK_ACCEPT_LOOP_DELAY_MS) + } + } + } + + private fun handleConnection(clientSocket: Socket) { + val scope = serverScope ?: return + val endpoint = clientSocket.remoteSocketAddress?.toString() ?: "unknown" + + if (clientSocket.inetAddress?.isLoopbackAddress != true) { + Logger.w { "TAK server rejected non-loopback connection from $endpoint" } + runCatching { clientSocket.close() } + return + } + + val connectionId = Random.nextInt().toString(TAK_HEX_RADIX) + val clientInfo = TAKClientInfo(id = connectionId, endpoint = endpoint) + Logger.i { "TAK client connected: id=$connectionId endpoint=$endpoint" } + + val connection = + TAKClientConnection( + socket = clientSocket, + clientInfo = clientInfo, + onEvent = { event -> handleConnectionEvent(connectionId, event) }, + scope = scope, + ioDispatcher = dispatchers.io, + ) + + // Launch on IO so socket reads/writes don't queue behind CPU work on Default + scope.launch(dispatchers.io) { + connectionsMutex.withLock { + connections[connectionId] = connection + _connectionCount.value = connections.size + Logger.i { "TAK connection count now ${connections.size}" } + } + connection.start() + } + } + + private fun handleConnectionEvent(connectionId: String, event: TAKConnectionEvent) { + when (event) { + is TAKConnectionEvent.Message -> { + onMessage?.invoke(event.cotMessage, event.clientInfo) + } + is TAKConnectionEvent.Disconnected -> { + Logger.i { "TAK client disconnected: id=$connectionId" } + serverScope?.launch(dispatchers.io) { + connectionsMutex.withLock { + connections.remove(connectionId) + _connectionCount.value = connections.size + Logger.i { "TAK connection count now ${connections.size}" } + } + } + } + is TAKConnectionEvent.Error -> { + Logger.w(event.error) { "TAK client connection error: $connectionId" } + serverScope?.launch(dispatchers.io) { + connectionsMutex.withLock { + connections.remove(connectionId) + _connectionCount.value = connections.size + Logger.i { "TAK connection count now ${connections.size}" } + } + } + } + is TAKConnectionEvent.Connected -> { + onClientConnected?.invoke() + } + is TAKConnectionEvent.ClientInfoUpdated -> { + /* no-op: TAKClientConnection tracks updated info locally */ + } + } + } + + override fun stop() { + running = false + acceptJob?.cancel() + acceptJob = null + + val toClose: List + // Non-suspending stop path — best-effort copy; any connection added concurrently + // will get closed when its socket is torn down by accept() returning null. + toClose = connections.values.toList() + connections.clear() + _connectionCount.value = 0 + toClose.forEach { it.close() } + + serverSocket?.runCatching { close() } + serverSocket = null + serverScope = null + Logger.i { "TAK Server stopped" } + } + + override suspend fun broadcast(cotMessage: CoTMessage) { + val currentConnections = connectionsMutex.withLock { connections.values.toList() } + if (currentConnections.isEmpty()) { + Logger.d { "broadcast ${cotMessage.type}: no TAK clients connected, dropping" } + return + } + Logger.d { "broadcast ${cotMessage.type} to ${currentConnections.size} TAK client(s)" } + currentConnections.forEach { connection -> + try { + connection.send(cotMessage) + } catch (e: Exception) { + Logger.w(e) { "Failed to broadcast CoT to TAK client ${connection.clientInfo.id}" } + connection.close() + } + } + } + + override suspend fun broadcastRawXml(xml: String) { + val currentConnections = connectionsMutex.withLock { connections.values.toList() } + if (currentConnections.isEmpty()) return + Logger.d { "broadcastRawXml to ${currentConnections.size} TAK client(s)" } + currentConnections.forEach { connection -> + try { + connection.sendRawXml(xml) + } catch (e: Exception) { + Logger.w(e) { "Failed to broadcast raw XML to TAK client ${connection.clientInfo.id}" } + connection.close() + } + } + } + + override suspend fun hasConnections(): Boolean = + connectionsMutex.withLock { connections.isNotEmpty() } +} + +/** + * `actual` factory for the KMP `expect fun createTAKServer` declared in `commonMain`. + * Both the Desktop JVM target and the Android target share this source set, so both + * run the same JSSE-based TLS listener. + * + * Also wires [TAKDataPackageGenerator]'s bundled-cert provider so that the exported + * `.zip` data package contains the real `server.p12` / `client.p12` bytes from the + * classpath rather than an empty fallback. + */ +actual fun createTAKServer(dispatchers: CoroutineDispatchers, port: Int): TAKServer { + TAKDataPackageGenerator.bundledCertBytesProvider = TakCertBundledBytesProvider + return TAKServerJvm(dispatchers = dispatchers, port = port) +} + +/** Bridges [TakCertLoader] bytes into [TAKDataPackageGenerator] via the commonMain interface. */ +private object TakCertBundledBytesProvider : BundledCertBytesProvider { + override fun serverP12Bytes(): ByteArray? = TakCertLoader.getServerP12Bytes() + override fun clientP12Bytes(): ByteArray? = TakCertLoader.getClientP12Bytes() +} diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakCertLoader.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakCertLoader.kt new file mode 100644 index 000000000..250ecc342 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakCertLoader.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +@file:Suppress("TooGenericExceptionCaught") + +package org.meshtastic.core.takserver + +import co.touchlab.kermit.Logger +import java.io.ByteArrayInputStream +import java.security.KeyStore +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory + +/** + * Loads the bundled TAK server certificates from the classpath and builds an [SSLContext] + * suitable for running a TLS TAK server with mutual TLS (mTLS). + * + * Bundled resources (under `tak_certs/` on the module classpath): + * - `server.p12` — PKCS#12 containing the server's identity (cert + private key). + * Used as the server's identity during the TLS handshake. + * - `client.p12` — PKCS#12 containing an example client identity, included in the + * exported data package so ATAK / iTAK have a certificate it can present. + * - `ca.pem` — PEM-encoded CA certificate used to validate the presented client + * certificate during mTLS. Only clients whose certificate chains back to this CA + * are accepted. + * + * All files are the same bytes as the iOS Meshtastic-Apple bundle, so the same + * exported data package works for both platforms with no re-import. + */ +internal object TakCertLoader { + + private const val RESOURCE_SERVER_P12 = "tak_certs/server.p12" + private const val RESOURCE_CLIENT_P12 = "tak_certs/client.p12" + private const val RESOURCE_CA_PEM = "tak_certs/ca.pem" + + @Volatile private var cachedSslContext: SSLContext? = null + @Volatile private var cachedServerP12: ByteArray? = null + @Volatile private var cachedClientP12: ByteArray? = null + @Volatile private var cachedCaPem: ByteArray? = null + + /** + * Build (and cache) an [SSLContext] for the TAK server. + * + * The context uses the bundled `server.p12` for its identity and the bundled + * `ca.pem` to validate client certificates during mTLS. If anything fails to + * load (missing resources, bad password, corrupt keystore) this returns `null` + * and callers should fall back to a non-TLS listener or refuse to start. + */ + @Synchronized + fun getServerSslContext(): SSLContext? { + cachedSslContext?.let { return it } + return try { + val serverP12 = loadResourceBytes(RESOURCE_SERVER_P12) + ?: error("Bundled $RESOURCE_SERVER_P12 not found on classpath") + val caPem = loadResourceBytes(RESOURCE_CA_PEM) + ?: error("Bundled $RESOURCE_CA_PEM not found on classpath") + + // Load the server identity (cert + private key). + val serverKeyStore = KeyStore.getInstance("PKCS12").apply { + ByteArrayInputStream(serverP12).use { input -> + load(input, TAK_BUNDLED_CERT_PASSWORD.toCharArray()) + } + } + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { + init(serverKeyStore, TAK_BUNDLED_CERT_PASSWORD.toCharArray()) + } + + // Load the CA certificate(s) used to verify incoming client certs. + val caCerts = parsePemCertificates(caPem) + if (caCerts.isEmpty()) error("No certificates found inside $RESOURCE_CA_PEM") + val trustKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { + load(null, null) + caCerts.forEachIndexed { index, cert -> + setCertificateEntry("tak-client-ca-$index", cert) + } + } + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { + init(trustKeyStore) + } + + val sslContext = SSLContext.getInstance("TLSv1.2").apply { + init(kmf.keyManagers, tmf.trustManagers, null) + } + + Logger.i { "TAK: loaded bundled TLS server identity and ${caCerts.size} CA certificate(s)" } + cachedSslContext = sslContext + sslContext + } catch (e: Throwable) { + Logger.e(e) { "TAK: failed to build SSLContext from bundled certificates: ${e.message}" } + null + } + } + + /** Returns the raw bytes of the bundled `server.p12`. Used by the data package generator. */ + fun getServerP12Bytes(): ByteArray? { + cachedServerP12?.let { return it } + val bytes = loadResourceBytes(RESOURCE_SERVER_P12) + cachedServerP12 = bytes + return bytes + } + + /** Returns the raw bytes of the bundled `client.p12`. Used by the data package generator. */ + fun getClientP12Bytes(): ByteArray? { + cachedClientP12?.let { return it } + val bytes = loadResourceBytes(RESOURCE_CLIENT_P12) + cachedClientP12 = bytes + return bytes + } + + /** Returns the raw bytes of the bundled `ca.pem`. */ + fun getCaPemBytes(): ByteArray? { + cachedCaPem?.let { return it } + val bytes = loadResourceBytes(RESOURCE_CA_PEM) + cachedCaPem = bytes + return bytes + } + + private fun loadResourceBytes(name: String): ByteArray? { + val stream = TakCertLoader::class.java.classLoader?.getResourceAsStream(name) + ?: return null + return stream.use { it.readBytes() } + } + + /** + * Parse every `-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----` block in the + * given PEM bytes into [X509Certificate]s. Tolerates multiple certs in one file. + */ + private fun parsePemCertificates(pem: ByteArray): List { + val factory = CertificateFactory.getInstance("X.509") + // CertificateFactory.generateCertificates handles PEM bundles directly on all + // standard Java providers, so we don't need to split ourselves. + return ByteArrayInputStream(pem).use { input -> + factory.generateCertificates(input).filterIsInstance() + } + } +} diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt index c7be69ffc..97abe3553 100644 --- a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt +++ b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/TakV2Compressor.kt @@ -71,6 +71,17 @@ internal actual object TakV2Compressor { return sdkDataToWire(data) } + /** + * Decompress a V2 wire payload and reconstruct CoT XML via the SDK's + * CotXmlBuilder. This handles ALL payload types (DrawnShape, Marker, + * Route, etc.) without going through the Wire proto intermediate, + * avoiding the gap where `toCoTMessage()` only handles PLI/GeoChat. + */ + actual fun decompressToXml(wirePayload: ByteArray): String { + val data = getSdkCompressor().decompress(wirePayload) + return org.meshtastic.tak.CotXmlBuilder().build(data) + } + /** * Convert Wire-generated TAKPacketV2 → SDK's TakPacketV2Data. */ diff --git a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt b/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt deleted file mode 100644 index 9db28ac66..000000000 --- a/core/takserver/src/jvmAndroidMain/kotlin/org/meshtastic/core/takserver/fountain/CodecActual.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.takserver.fountain - -import java.io.ByteArrayOutputStream -import java.security.MessageDigest -import java.util.zip.Deflater -import java.util.zip.Inflater - -internal actual object ZlibCodec { - actual fun compress(data: ByteArray): ByteArray? { - val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, false) - return try { - deflater.setInput(data) - deflater.finish() - - val outputStream = ByteArrayOutputStream(data.size) - val buffer = ByteArray(1024) - while (!deflater.finished()) { - val count = deflater.deflate(buffer) - outputStream.write(buffer, 0, count) - } - outputStream.close() - outputStream.toByteArray() - } catch (e: Exception) { - null - } finally { - deflater.end() - } - } - - actual fun decompress(data: ByteArray): ByteArray? { - val inflater = Inflater(false) - return try { - inflater.setInput(data) - - val outputStream = ByteArrayOutputStream(data.size * 2) - val buffer = ByteArray(1024) - while (!inflater.finished()) { - val count = inflater.inflate(buffer) - if (count == 0 && inflater.needsInput()) { - break - } - outputStream.write(buffer, 0, count) - } - outputStream.close() - outputStream.toByteArray() - } catch (e: Exception) { - null - } finally { - inflater.end() - } - } -} - -internal actual object CryptoCodec { - actual fun sha256Prefix8(data: ByteArray): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(data).copyOf(8) - } -} diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_certs/ca.pem b/core/takserver/src/jvmAndroidMain/resources/tak_certs/ca.pem new file mode 100644 index 000000000..1dc6e36f6 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_certs/ca.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID4zCCAsugAwIBAgIUeM9XhqZCtta+QorYNjZSdAk3gkMwDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQKDApNZXNodGFzdGljMRMwEQYDVQQLDApU +QUsgU2VydmVyMRowGAYDVQQDDBFNZXNodGFzdGljIFRBSyBDQTAeFw0yNTEyMzEx +OTQwMDJaFw0yODA0MDQxOTQwMDJaMIGAMQswCQYDVQQGEwJVUzETMBEGA1UECAwK +Q2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwKTWVz +aHRhc3RpYzETMBEGA1UECwwKVEFLIFNlcnZlcjEaMBgGA1UEAwwRTWVzaHRhc3Rp +YyBUQUsgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2F6/n1CI2 +4dGtLt0irkfiU+PRmqkkuE7m49i7/FeH+38SEn9+0B4egW0kYRoRXmYdPzRsVttu +23LZ3RLjwB6fFI3tiA27mxD58AuEMfwVR7J29oHqFwuVhuqDyjkNpUPFUomKwzvK +SPJvoiHGkbQwWTMNP6T06tCg9llSE7SIgJWjzikQ+JsI37SqVGZ8K2evs7LTuyQh +ssJfYVB7aE1kNNyi8YFHLoCWQMB7h8qJ3hRd7QGFG9gfWuNrWtim61iiHgBAPTRw +gMn+YSIZiV9/iOytBKxFppNTxffEowF/iKBvgXwd9KHxYkk1Nvtcz5NJynSL75PT +8B7XiHCGhcgzAgMBAAGjUzBRMB0GA1UdDgQWBBRRe/o9Raj93Fq22ArNSNrpsye3 +AzAfBgNVHSMEGDAWgBRRe/o9Raj93Fq22ArNSNrpsye3AzAPBgNVHRMBAf8EBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAsuSQ+j/1Bm7HbZWzN5qChH554vucWoqI0 +sVRHThvCASC6+wSosWZlx/Ag5KnRmBVsYA6CX5ztoF5keiSRy5G7qyRQVjITOq1o +4XUAHBtGxKdRCEzS84GnsW9qeWX7t/xxf2fFr9gPZ7Z4nuyNg7QyX5FM01BtAlZC +HbBhXvJyHRqJkMe7keYU7GmiAs1RZa+7593uEQ8DQ/kRvCzU0XswFSguJrd4Fnpi +PGesGOk0NHFQY9pIu9oshgPgMA9dEWnhhvAF3PZ3sLRn9sSuslj5oumFsTYboByE +aOKQshFe5xEX/4O7DI+wsD1Pt5gdT75nAuG7GEAIFKKGjQtUUYfH +-----END CERTIFICATE----- diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_certs/client.p12 b/core/takserver/src/jvmAndroidMain/resources/tak_certs/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..2f27bff2d6a5e102bb7f6c343c6a1bacd530a01f GIT binary patch literal 3827 zcmai%Ra6uV*M(siy1OKXZf59~lvYv(kZ$RghCv!6hejG1X(T13q)S2=QV9`|9vVKs z_5bUAFTabk&f0rloV&fA1BMYD0njnPFrsoST%KsP=u1L$Z1f@+Q6>;Zl=jzt2ZrI? z{2O5v!EpBfTDt&rw7+ZP-vk|O@sGeH1nYqz|4ITd8<6B>YXf4p8|-+3jMm>|GPjeHRQ?95lw%h$R&(@2y#h=y^xPoE3(OBv+Y2 zD0|sQwbe&|WtF?V*63pki3&NW6zs6_NyalKY_BSDTLK}&&Cd=uJ?qJED9On0zr-;Z zSL=#op>@>1l3EJ&)F{HD%Lv>0Gr7#R{ARcX^dt3xmh%{^xOdXJ03p#)ik;&mbmM0w zw_RX=O@dRWT_w)X6vyUColxL6;`puz)f_Q5QB?yUj2+a>Rmt?QPZ@ll-%zgpv-J?o z0sE<=u|O*p(6Z@$&)ze1xrdATQ^SLlvoi_G=Gu&}_V=GDPbIMjys!Ij{Do z(@yPqYxj7N^m7+@@*g#|O;YA{L|kEyUWGvcMp5foof+3`O^U8iDF4CBI#9*FjSpTK z(kV;RlA;rXRbWFF7|Eljyx(zSoj7{sAI7MnGDMujI9(0p>zjZxu>EdPcO}n?VaC5x z-9iNPB9BygVFRC;Zjs!L5KsBSmCbd>v=)U4nR%Sa$sIb)qS{R4Hz=zOYolC}#)wij zd;{U&2&sXWs_pL5V~EOH|9Yl5takQmjiqebepv;l__d^Hq9y%h)=mOV zzm$_k9ZSD#Lh^N$AVxV(IsiH*sTaNXkE7X+-~krY8iq52t{_#I`JS3fYi∈>}Zi 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 z + + + <_radio rssi="-19.4" gps="true"/>000001 ICAO: 000001 REG: NTEST1 Flight: TST100 Type: A321 Squawk: 3456 DO-260B Category: A3 #adsbreceiver<_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T17:45:00Z"/> + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/aircraft_hostile.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/aircraft_hostile.xml new file mode 100644 index 000000000..226586fcb --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/aircraft_hostile.xml @@ -0,0 +1,5 @@ + + + + TST200 NTEST2 000002 Cat:A6 Type:HAWK sim-host@example.test<_aircot_ flight="TST200" reg="NTEST2" cat="A6" icao="000002" cot_host_id="sim-host@example.test" type="HAWK"/><_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T18:20:00Z"/> + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/alert_tic.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/alert_tic.xml new file mode 100644 index 000000000..00fc76f12 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/alert_tic.xml @@ -0,0 +1,8 @@ + + + + + + Troops in contact, requesting support at grid reference + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac.xml new file mode 100644 index 000000000..9bcbae011 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac.xml @@ -0,0 +1,10 @@ + + + + + + + 2 urgent surgical, 1 priority. LZ marked with green smoke. No enemy activity. + <_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T20:00:00Z"/> + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac_medline.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac_medline.xml new file mode 100644 index 000000000..0c169f2b7 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/casevac_medline.xml @@ -0,0 +1,10 @@ + + + + + + <_medevac_ precedence="Urgent" hoist="true" extraction_equipment="true" ventilator="false" blood="false" litter="2" ambulatory="1" security="N" hlz_marking="Smoke" zone_prot_marker="Green smoke" us_military="2" us_civilian="0" non_us_military="1" non_us_civilian="0" epw="0" child="0" terrain_slope="true" terrain_rough="false" terrain_loose="true" terrain_trees="false" terrain_wires="false" terrain_other="false" freq="38.90"/> + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_delivered.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_delivered.xml new file mode 100644 index 000000000..d66d9c3cd --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_delivered.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_read.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_read.xml new file mode 100644 index 000000000..86d8bfb90 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/chat_receipt_read.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/delete_event.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/delete_event.xml new file mode 100644 index 000000000..4c43a27ea --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/delete_event.xml @@ -0,0 +1,5 @@ + + + + <_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T19:30:00Z"/> + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle.xml new file mode 100644 index 000000000..a94353b0f --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle_large.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle_large.xml new file mode 100644 index 000000000..d155be57d --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_circle_large.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_ellipse.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_ellipse.xml new file mode 100644 index 000000000..232877e83 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_ellipse.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_freeform.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_freeform.xml new file mode 100644 index 000000000..9787f0741 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_freeform.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_polygon.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_polygon.xml new file mode 100644 index 000000000..6efcd9ee0 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_polygon.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle.xml new file mode 100644 index 000000000..cb22fca8d --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle_itak.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle_itak.xml new file mode 100644 index 000000000..0197969d2 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_rectangle_itak.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_telestration.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_telestration.xml new file mode 100644 index 000000000..ca9b1f22b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/drawing_telestration.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_911.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_911.xml new file mode 100644 index 000000000..88225bc63 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_911.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_cancel.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_cancel.xml new file mode 100644 index 000000000..6f4f0257d --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/emergency_cancel.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_broadcast.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_broadcast.xml new file mode 100644 index 000000000..30872f9ff --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_broadcast.xml @@ -0,0 +1,12 @@ + + + + + <__chat parent="RootContactGroup" groupOwner="false" messageId="a1b2c3d4" chatroom="All Chat Rooms" id="All Chat Rooms" senderCallsign="ETHEL"> + + + + <__serverdestination destinations="0.0.0.0:4242:tcp:ANDROID-0000000000000003"/> + at breach + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_dm.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_dm.xml new file mode 100644 index 000000000..f3fcd1828 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_dm.xml @@ -0,0 +1,12 @@ + + + + + <__chat parent="RootContactGroup" groupOwner="false" messageId="e5f6a7b8" chatroom="ANDROID-0000000000000004" id="ANDROID-0000000000000004" senderCallsign="ETHEL"> + + + + <__serverdestination destinations="0.0.0.0:4242:tcp:ANDROID-0000000000000003"/> + cover by fire + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_simple.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_simple.xml new file mode 100644 index 000000000..6fdbf123e --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/geochat_simple.xml @@ -0,0 +1,12 @@ + + + + + <__chat senderCallsign="TESTNODE-01" chatRoom="All Chat Rooms" id="All Chat Rooms" parent="RootContactGroup"> + + + + Roger that, moving to rally point + <__serverdestination destinations="0.0.0.0:4242:tcp:ANDROID-0000000000000002"/> + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_2525.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_2525.xml new file mode 100644 index 000000000..da28fee7e --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_2525.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto.xml new file mode 100644 index 000000000..7cc637331 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto_itak.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto_itak.xml new file mode 100644 index 000000000..e1d4548c3 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_goto_itak.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_icon_set.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_icon_set.xml new file mode 100644 index 000000000..76739bd71 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_icon_set.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_spot.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_spot.xml new file mode 100644 index 000000000..2f3499c4b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_spot.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_tank.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_tank.xml new file mode 100644 index 000000000..530ef51f6 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/marker_tank.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_basic.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_basic.xml new file mode 100644 index 000000000..51435cf5a --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_basic.xml @@ -0,0 +1,5 @@ + + + + <_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T14:22:10Z"/> + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_full.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_full.xml new file mode 100644 index 000000000..9283cf94b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_full.xml @@ -0,0 +1,5 @@ + + + + <__group role="Team Member" name="Cyan"/><_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T15:30:00Z"/> + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_itak.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_itak.xml new file mode 100644 index 000000000..cf86918d8 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_itak.xml @@ -0,0 +1,11 @@ + + + + + + <__group name="Cyan" role="Team Member"/> + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_stationary.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_stationary.xml new file mode 100644 index 000000000..6b4da149b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_stationary.xml @@ -0,0 +1,12 @@ + + + + + + + <__group role="Team Member" name="Cyan"/> + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_takaware.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_takaware.xml new file mode 100644 index 000000000..14992583a --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_takaware.xml @@ -0,0 +1,11 @@ + + + + + + <__group role="Team Member" name="Cyan"/> + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_webtak.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_webtak.xml new file mode 100644 index 000000000..ea13b008b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/pli_webtak.xml @@ -0,0 +1,5 @@ + + + + <__group name="Cyan" role="Team Member"/><_flow-tags_ TAK-Server-00000000000000000000000000000001="2026-03-15T16:10:00Z"/> + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_bullseye.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_bullseye.xml new file mode 100644 index 000000000..e23bf3fb6 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_bullseye.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_circle.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_circle.xml new file mode 100644 index 000000000..544b33a7b --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_circle.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_line.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_line.xml new file mode 100644 index 000000000..93d170565 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/ranging_line.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_3wp.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_3wp.xml new file mode 100644 index 000000000..ede8bed8c --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_3wp.xml @@ -0,0 +1,16 @@ + + + + + <__routeinfo/> + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_itak_3wp.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_itak_3wp.xml new file mode 100644 index 000000000..9be1f6169 --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/route_itak_3wp.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/task_engage.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/task_engage.xml new file mode 100644 index 000000000..602ae5cbf --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/task_engage.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/waypoint.xml b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/waypoint.xml new file mode 100644 index 000000000..3f41333cc --- /dev/null +++ b/core/takserver/src/jvmAndroidMain/resources/tak_test_fixtures/waypoint.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt index faf2f792e..2563fca77 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/ModuleConfigurationScreen.kt @@ -49,12 +49,13 @@ fun ModuleConfigurationScreen( val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() + val deviceRole = state.radioConfig.device?.role val modules = - remember(state.metadata, excludedModulesUnlocked) { + remember(state.metadata, deviceRole, excludedModulesUnlocked) { if (excludedModulesUnlocked) { ModuleRoute.entries } else { - ModuleRoute.filterExcludedFrom(state.metadata, state.userConfig.role) + ModuleRoute.filterExcludedFrom(state.metadata, deviceRole) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt index ac713ae7e..c60cfd1a8 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/navigation/SettingsNavigation.kt @@ -64,6 +64,7 @@ import org.meshtastic.feature.settings.radio.component.SerialConfigScreen import org.meshtastic.feature.settings.radio.component.StatusMessageConfigScreen import org.meshtastic.feature.settings.radio.component.StoreForwardConfigScreen import org.meshtastic.feature.settings.radio.component.TAKConfigScreen +import org.meshtastic.feature.settings.radio.component.TakServerScreen import org.meshtastic.feature.settings.radio.component.TelemetryConfigScreen import org.meshtastic.feature.settings.radio.component.TrafficManagementConfigScreen import org.meshtastic.feature.settings.radio.component.UserConfigScreen @@ -185,6 +186,10 @@ fun EntryProviderScope.settingsGraph(backStack: NavBackStack) { } } + entry { + TakServerScreen(onBack = { backStack.removeLastOrNull() }) + } + entry { val viewModel: DebugViewModel = koinViewModel() DebugScreen(viewModel = viewModel, onNavigateUp = { backStack.removeLastOrNull() }) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt index 0ff5326fc..6700e2359 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfig.kt @@ -202,6 +202,13 @@ private fun AdvancedSection(isManaged: Boolean, isOtaCapable: Boolean, enabled: onClick = { onNavigate(SettingsRoutes.CleanNodeDb) }, ) + ListItem( + text = "TAK Server", + leadingIcon = Icons.Rounded.Settings, + enabled = enabled, + onClick = { onNavigate(SettingsRoutes.TakServer) }, + ) + ListItem( text = stringResource(Res.string.debug_panel), leadingIcon = Icons.Rounded.BugReport, diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt index 714513e7d..46f01dee3 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt @@ -16,20 +16,42 @@ */ package org.meshtastic.feature.settings.radio.component +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject +import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.model.getColorFrom import org.meshtastic.core.model.getStringResFrom +import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.TakPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.tak @@ -39,27 +61,77 @@ import org.meshtastic.core.resources.tak_server_enabled import org.meshtastic.core.resources.tak_server_enabled_desc import org.meshtastic.core.resources.tak_team import org.meshtastic.core.takserver.TAKDataPackageGenerator +import org.meshtastic.core.takserver.TakMeshTestRunner import org.meshtastic.core.ui.component.DropDownPreference import org.meshtastic.core.ui.component.SwitchPreference import org.meshtastic.core.ui.component.TitledCard import org.meshtastic.feature.settings.radio.RadioConfigViewModel +import org.meshtastic.feature.settings.radio.ResponseState import org.meshtastic.feature.settings.tak.TakPermissionHandler import org.meshtastic.feature.settings.tak.rememberDataPackageExporter import org.meshtastic.proto.ModuleConfig +// ── TAK Config Screen (Module Settings) ───────────────────────────────────── +// Shows only the firmware module config: team and role dropdowns. + @Composable fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val takConfig = state.moduleConfig.tak ?: ModuleConfig.TAKConfig() val formState = rememberConfigState(initialValue = takConfig) + LaunchedEffect(takConfig) { formState.value = takConfig } + + val effectiveResponseState = when (state.responseState) { + is ResponseState.Loading -> ResponseState.Empty + else -> state.responseState + } + + RadioConfigScreenList( + title = stringResource(Res.string.tak), + onBack = onBack, + configState = formState, + enabled = state.connected, + responseState = effectiveResponseState, + onDismissPacketResponse = viewModel::clearPacketResponse, + onSave = { + val config = ModuleConfig(tak = it) + viewModel.setModuleConfig(config) + }, + ) { + item { + TitledCard(title = stringResource(Res.string.tak_config)) { + DropDownPreference( + title = stringResource(Res.string.tak_team), + enabled = state.connected, + selectedItem = formState.value.team, + itemLabel = { stringResource(getStringResFrom(it)) }, + itemColor = { Color(getColorFrom(it)) }, + onItemSelected = { formState.value = formState.value.copy(team = it) }, + ) + HorizontalDivider() + DropDownPreference( + title = stringResource(Res.string.tak_role), + enabled = state.connected, + selectedItem = formState.value.role, + itemLabel = { stringResource(getStringResFrom(it)) }, + onItemSelected = { formState.value = formState.value.copy(role = it) }, + ) + } + } + } +} + +// ── TAK Server Screen (Settings → Advanced) ───────────────────────────────── +// App-local TAK server controls: enable/disable, export data package, debug test harness. + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TakServerScreen(onBack: () -> Unit) { val takPrefs: TakPrefs = koinInject() val isTakServerEnabled by takPrefs.isTakServerEnabled.collectAsStateWithLifecycle() - val exportLauncher = rememberDataPackageExporter { TAKDataPackageGenerator.generateDataPackage() } - LaunchedEffect(takConfig) { formState.value = takConfig } - TakPermissionHandler( isTakServerEnabled = isTakServerEnabled, onPermissionResult = { granted -> @@ -69,65 +141,135 @@ fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { }, ) - RadioConfigScreenList( - title = stringResource(Res.string.tak), - onBack = onBack, - actions = { - IconButton(onClick = { exportLauncher("Meshtastic_TAK_Server.zip") }) { - Icon(imageVector = Icons.Default.Share, contentDescription = "Export TAK Data Package") - } - }, - configState = formState, - enabled = state.connected, - responseState = state.responseState, - onDismissPacketResponse = viewModel::clearPacketResponse, - onSave = { - val config = ModuleConfig(tak = it) - viewModel.setModuleConfig(config) - }, - ) { - item { - TAKConfigCard( - formState = formState, - isTakServerEnabled = isTakServerEnabled, - isConnected = state.connected, - onTakServerEnabledChange = { takPrefs.setTakServerEnabled(it) }, + Scaffold( + topBar = { + TopAppBar( + title = { Text("TAK Server") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + actions = { + if (isTakServerEnabled) { + IconButton(onClick = { exportLauncher("Meshtastic_TAK_Server.zip") }) { + Icon(imageVector = Icons.Default.Share, contentDescription = "Export TAK Data Package") + } + } + }, ) + }, + ) { padding -> + Column(modifier = Modifier.padding(padding).padding(horizontal = 16.dp)) { + TitledCard(title = "Server") { + SwitchPreference( + title = stringResource(Res.string.tak_server_enabled), + summary = stringResource(Res.string.tak_server_enabled_desc), + checked = isTakServerEnabled, + enabled = true, + onCheckedChange = { takPrefs.setTakServerEnabled(it) }, + ) + if (isTakServerEnabled) { + HorizontalDivider() + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Export Data Package", + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = "Generate .zip for ATAK/iTAK to connect to this server", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = { exportLauncher("Meshtastic_TAK_Server.zip") }) { + Icon(imageVector = Icons.Default.Share, contentDescription = "Export TAK Data Package") + } + } + } + } + + // Debug-only test harness + TakMeshTestCard() } } } +// ── Debug-only TAK Mesh Test Card ──────────────────────────────────────────── + @Composable -private fun TAKConfigCard( - formState: ConfigState, - isTakServerEnabled: Boolean, - isConnected: Boolean, - onTakServerEnabledChange: (Boolean) -> Unit, -) { - TitledCard(title = stringResource(Res.string.tak_config)) { - SwitchPreference( - title = stringResource(Res.string.tak_server_enabled), - summary = stringResource(Res.string.tak_server_enabled_desc), - checked = isTakServerEnabled, - enabled = true, - onCheckedChange = onTakServerEnabledChange, - ) - HorizontalDivider() - DropDownPreference( - title = stringResource(Res.string.tak_team), - enabled = isConnected, - selectedItem = formState.value.team, - itemLabel = { stringResource(getStringResFrom(it)) }, - itemColor = { Color(getColorFrom(it)) }, - onItemSelected = { formState.value = formState.value.copy(team = it) }, - ) - HorizontalDivider() - DropDownPreference( - title = stringResource(Res.string.tak_role), - enabled = isConnected, - selectedItem = formState.value.role, - itemLabel = { stringResource(getStringResFrom(it)) }, - onItemSelected = { formState.value = formState.value.copy(role = it) }, - ) +private fun TakMeshTestCard() { + val buildConfig: BuildConfigProvider = koinInject() + if (!buildConfig.isDebug) return + + val commandSender: CommandSender = koinInject() + val testRunner = remember { TakMeshTestRunner(commandSender) } + val results by testRunner.results.collectAsStateWithLifecycle() + val isRunning by testRunner.isRunning.collectAsStateWithLifecycle() + val currentFixture by testRunner.currentFixture.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() + + val passed = results.count { it.passed } + val failed = results.count { !it.passed } + + TitledCard(title = "TAK Mesh Test (Debug)") { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (isRunning) "Running: ${currentFixture ?: "..."}" else "Send all ${TakMeshTestRunner.FIXTURE_NAMES.size} test fixtures to mesh", + style = MaterialTheme.typography.bodyLarge, + ) + if (results.isNotEmpty()) { + Text( + text = "$passed passed, $failed failed of ${results.size}/${TakMeshTestRunner.FIXTURE_NAMES.size}", + style = MaterialTheme.typography.bodySmall, + color = if (failed > 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + if (isRunning) { + CircularProgressIndicator() + } else { + Button(onClick = { scope.launch { testRunner.runAll() } }) { + Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null) + Text("Run") + } + } + } + + // Results list + for (result in results) { + HorizontalDivider() + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = result.fixtureName.removeSuffix(".xml"), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + ) + Text( + text = if (result.passed) "${result.compressedBytes}B ✓" else result.error ?: "✗", + style = MaterialTheme.typography.bodySmall, + color = if (result.passed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, + ) + } + } } }