Co-authored-by: James Rich <james.a.rich@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
11 KiB
:core:takserver
Overview
The :core:takserver module implements the Meshtastic ↔ TAK (Team Awareness Kit) bridge. It embeds an mTLS TCP server (port 8089) compatible with ATAK (Android), iTAK (iOS), and WinTAK clients, enabling mesh-networked position sharing and GeoChat with TAK-enabled devices.
Targets: Android · JVM (Desktop) · iOS — fully multiplatform with expect/actual splits for compression, file I/O, and the TCP server itself.
Key Responsibilities
- Serve an mTLS TCP listener (port 8089) compatible with the CoT (Cursor-on-Target) protocol
- Convert Meshtastic protobuf packets (
TAKPacketV2) to CoT XML events and vice versa - Generate ATAK Data Package
.zipexports (team contacts, map overlays) - Compress CoT payloads using Zstd (TAK SDK format) with
expect/actualplatform implementations - Buffer up to 50 CoT messages for 5 minutes when no TAK clients are connected; drain on reconnect
- Provide Crowdin-localised TAK preference XML for ATAK client provisioning
Source Structure
src/
├── commonMain/kotlin/org/meshtastic/core/takserver/
│ ├── TAKServer.kt ← interface + expect createTAKServer()
│ ├── TAKServerManager.kt ← interface + TAKServerManagerImpl (offline queue)
│ ├── TAKMeshIntegration.kt ← bridges mesh service ↔ TAK server
│ ├── CoTConversion.kt ← Position/User → CoTMessage extension fns
│ ├── CoTXml.kt / CoTXmlParser.kt / CoTXmlFrameBuffer.kt
│ ├── CoTXmlDataClasses.kt
│ ├── CoTDetailStripper.kt
│ ├── TAKModels.kt ← CoTMessage, TAKClientInfo, TAKConnectionEvent
│ ├── TAKPacketConversion.kt
│ ├── TAKPacketV2Conversion.kt
│ ├── TAKDefaults.kt
│ ├── TAKDataPackageGenerator.kt
│ ├── RouteDataPackageGenerator.kt
│ ├── TAKPrefXmlDataClasses.kt
│ ├── TakV2TypeMapper.kt
│ ├── TakConversionHelpers.kt
│ ├── XmlUtils.kt
│ ├── AtakFileWriter.kt ← expect
│ ├── TakSdkCompressor.kt ← expect (Zstd TAK-SDK frame)
│ ├── TakV2Compressor.kt ← expect (Zstd TAKPacketV2 frame)
│ ├── ZipArchiver.kt ← expect
│ ├── TakFixtureLoader.kt ← expect (test fixtures)
│ ├── TakMeshTestRunner.kt
│ └── di/
│ └── CoreTakServerModule.kt
├── jvmAndroidMain/kotlin/ ← actual TAKServerJvm, TAKClientConnection, TakCertLoader
├── androidMain/kotlin/ ← actual AtakFileWriter (Android)
├── jvmMain/kotlin/ ← actual AtakFileWriter (Desktop), XML pull-parser
└── iosMain/kotlin/ ← actual TAKServerIos, actual compression impls
Notable APIs
TAKServer (interface)
interface TAKServer {
val connectionCount: StateFlow<Int>
var onMessage: ((CoTMessage, TAKClientInfo?) -> Unit)?
var onClientConnected: (() -> Unit)?
suspend fun start(scope: CoroutineScope): Result<Unit>
fun stop()
suspend fun broadcast(cotMessage: CoTMessage)
suspend fun broadcastRawXml(xml: String)
suspend fun hasConnections(): Boolean
}
The mTLS listener binds on port 8089 using a bundled server.p12 / ca.pem identity, compatible with the ATAK Data Package provisioning flow.
TAKServerManager (interface)
interface TAKServerManager {
val isRunning: StateFlow<Boolean>
val connectionCount: StateFlow<Int>
val inboundMessages: SharedFlow<InboundCoTMessage>
suspend fun start(scope: CoroutineScope)
fun stop()
suspend fun broadcast(cotMessage: CoTMessage)
suspend fun broadcastRawXml(xml: String)
}
TAKServerManagerImpl adds an offline queue: buffers up to 50 CoT messages for 5 minutes when no clients are connected and drains them automatically on the next onClientConnected callback.
CoTMessage
@Serializable
data class CoTMessage(
val uid: String,
val type: String, // e.g. "a-f-G-U-C" (friendly ground unit)
val time: Instant,
val lat: Double, val lon: Double, val hae: Double,
val contact: CoTContact?,
val group: CoTGroup?,
val track: CoTTrack?,
val chat: CoTChat?,
val remarks: String?,
// ...
)
// Factory helpers
CoTMessage.pli(uid, callsign, lat, lon, ...) // Position Location Information
CoTMessage.chat(senderUid, callsign, message, chatroom)
CoT Conversion
// Meshtastic proto → CoT
org.meshtastic.proto.Position.toCoTMessage(uid, callsign, team, role, battery): CoTMessage
org.meshtastic.proto.User.toCoTMessage(position, team, role, battery): CoTMessage
Dependency Graph
core:takserver
├── api → core:repository (exported)
├── core:common, core:di, core:model, core:proto
├── okio, kotlinx.serialization.json
├── xmlutil-core, xmlutil-serialization
├── ktor-client-core, ktor-network (TCP socket)
└── zstd-jni (jvmAndroid/jvm), kotlinx.datetime
Local TAK Server Feature
The Local TAK Server can be enabled from the app's Settings screen. When running, ATAK/iTAK clients on the same network can connect to <device-ip>:8089 and their position reports are automatically bridged onto the mesh. Mesh node positions are broadcast to all connected TAK clients in real time.
TAKPacket-SDK consumer & version-bump playbook
This module consumes the external TAKPacket-SDK (org.meshtastic:takpacket-sdk-jvm) for the V2 wire format. The SDK does CoT-XML ↔ TAKPacketV2 ↔ zstd-compressed bytes; it owns the dictionaries and the schema.
Two V2 wire paths — keep both in mind when the SDK changes:
- Path A (primary, SDK-delegated):
TakSdkCompressor/TakV2Compressorcall the SDK'sCotXmlParser/CotXmlBuilder/TakCompressor. This path is insulated from proto field renames as long as the SDK artifact and the proto submodule are bumped together. - Path B (fallback, Android-local Wire proto):
TAKPacketV2Conversion.ktbuilds and reads the Wire-generatedTAKPacketV2directly (used on the SDK-failure send fallback and as the iOS receive stub). It references proto fields by name, so it breaks at compile time on any schema change and must be updated in lockstep.
The proto submodule (core/proto/src/main/proto) must track the SDK's proto. It is the same meshtastic/protobufs repo the SDK generates from. :core:proto's build.gradle.kts prunes the atak messages owned by the SDK's own Wire codegen.
When bumping to a new (wire-breaking) SDK version:
gradle/libs.versions.toml→takpacket-sdk = "<new>".- Bump the
core/proto/src/main/protosubmodule to the matching protobufs commit (push it upstream first, thengit -C core/proto/src/main/proto checkout <sha>and commit the gitlink). For local testing before the protobufs commit is pushed, syncing justmeshtastic/atak.proto's content works (Wire regenerates from the working tree). - Keep
core/proto/build.gradle.kts'sprune(...)list complete. The SDK jar ships its own Wire codegen oforg.meshtastic.proto.*for the WHOLE atak.proto, so:core:protomustprune()EVERY atak message/enum the SDK ships — otherwise both define the same class and the release R8 build fails withType org.meshtastic.proto.X is defined multiple times(it compiles + passesjvmTestfine; only R8/dexing catches it). When the schema gains a message (e.g.Martiin v0.3.2 — which was missed and broke R8), add a matchingprune("meshtastic.X")line (+ one per nested proto type — prune does NOT cascade). Verify with:unzip -l <sdk jar> | grep org/meshtastic/proto/vs the prune list. (Exception: types the SDK does NOT ship — e.g.Team,MemberRole— must stay UNpruned, or the bridge loses them.) - Update Path B (
TAKPacketV2Conversion.kt) and the bridge (TakV2Compressor.kt) for any renamed/removed/added wire fields. - Test against the local SDK: publish it (
cd TAKPacket-SDK/kotlin && ./gradlew publishToMavenLocal), then./gradlew :core:takserver:jvmTest -PuseMavenLocal(needs JDK 21;-PuseMavenLocalis gated insettings.gradle.kts). Against a published version, drop-PuseMavenLocaland add--refresh-dependenciesto pull from Maven Central. - Verify the release R8 build (
./gradlew :androidApp:minifyFdroidReleaseWithR8) — catches prune-list gaps (above) AND confirms thezstd-jni:…@aarnative lib survives minification (core/takserver/build.gradle.ktswires the@aarfor androidMain, plain jar +xpp3for jvmMain).
v0.4.0 wire facts (so you don't re-introduce phantom changes): PLI is implicit — the bool pli oneof arm was removed; a packet with no payload variant + an a-f-* cot type is a PLI. DrawnShape vertices are two packed repeated sint32 columns (vertex_lat_deltas / vertex_lon_deltas, deltas from the envelope point), not repeated CotGeoPoint. course stays deg×100, uid stays a string, stale_seconds stays tag 16 — these were evaluated and deliberately NOT changed; do not "fix" the ×100 scaling in TAKPacketV2Conversion.kt.
Debug "Send Test CoTs": TakMeshTestRunner sends the bundled tak_test_fixtures/*.xml through the SDK path (parse → strip → compress → send). taktalk_sanity.xml is intentionally first. The fixtures need no edits across SDK wire breaks because they ride the SDK path; they ARE the regression surface (drawing_* exercise packed vertices, pli_* exercise implicit PLI).
Dependency Graph
graph TB
:core:takserver[takserver]:::kmp-library
:core:takserver --> :core:repository
:core:takserver -.-> :core:common
:core:takserver -.-> :core:di
:core:takserver -.-> :core:model
:core:takserver -.-> :core:proto
:core:takserver -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;