Skip to content

Commit ade5aff

Browse files
committed
CotXmlBuilder to support route payloads and remarks in multiple languages
1 parent 0f0c9a9 commit ade5aff

9 files changed

Lines changed: 300 additions & 25 deletions

File tree

TESTING_PUNCHLIST.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# TAK Mesh Integration — Testing Punchlist
2+
3+
Manual testing checklist for ATAK ↔ iTAK over Meshtastic mesh. Covers all payload types supported by the TAKPacket-SDK v2 protocol.
4+
5+
**Setup:** ATAK on Android tablet connected to Meshtastic radio via the Meshtastic app's built-in TAK server. iTAK on iPad connected to a separate Meshtastic radio via the Meshtastic Apple app's built-in TAK server. Both radios on the same mesh channel.
6+
7+
---
8+
9+
## 1. PLI — Position Location Info
10+
11+
| # | Test | Direction | Steps | Expected | Pass |
12+
|---|------|-----------|-------|----------|------|
13+
| 1.1 | Basic PLI | ATAK → iTAK | ATAK moves on map | iTAK shows ATAK's position icon updating ||
14+
| 1.2 | Basic PLI | iTAK → ATAK | iTAK moves on map | ATAK shows iTAK's position icon updating ||
15+
| 1.3 | Callsign | Both | Set callsign on each client | Correct callsign displayed on the other ||
16+
| 1.4 | Team color | Both | Set team color (e.g. Cyan, Red) | Correct team color on the other side ||
17+
| 1.5 | Battery | Both | Check battery level shown | Battery % matches sender's device ||
18+
19+
## 2. GeoChat — Text Messages
20+
21+
| # | Test | Direction | Steps | Expected | Pass |
22+
|---|------|-----------|-------|----------|------|
23+
| 2.1 | Broadcast chat | ATAK → iTAK | Send message to "All Chat Rooms" | Message appears in iTAK chat with correct text and sender ||
24+
| 2.2 | Broadcast chat | iTAK → ATAK | Send message to "All Chat Rooms" | Message appears in ATAK chat with correct text and sender ||
25+
| 2.3 | Message text preserved | Both | Send "Hello from mesh!" | Exact text displayed, no empty messages ||
26+
| 2.4 | Special characters | Both | Send message with `& < > "` | Characters displayed correctly (XML escaping works) ||
27+
| 2.5 | Long message | Both | Send a ~150 character message | Message delivered (may be truncated if exceeds MTU) ||
28+
29+
## 3. Drawn Shapes — Geometry
30+
31+
| # | Test | Direction | Steps | Expected | Pass |
32+
|---|------|-----------|-------|----------|------|
33+
| 3.1 | Circle | ATAK → iTAK | Draw a circle on map | iTAK shows circle at same location with same size ||
34+
| 3.2 | Circle | iTAK → ATAK | Draw a circle on map | ATAK shows circle at same location with same size ||
35+
| 3.3 | Rectangle | ATAK → iTAK | Draw a rectangle | iTAK shows rectangle with correct vertices ||
36+
| 3.4 | Rectangle | iTAK → ATAK | Draw a rectangle | ATAK shows rectangle with correct vertices ||
37+
| 3.5 | Freeform polyline | ATAK → iTAK | Draw a freeform line | iTAK shows polyline with correct shape ||
38+
| 3.6 | Freeform polyline | iTAK → ATAK | Draw a freeform line | ATAK shows polyline with correct shape ||
39+
| 3.7 | Polygon | Either | Draw a closed polygon | Other side shows correct polygon shape ||
40+
| 3.8 | Stroke color | Both | Draw shape with non-default color (e.g. Red) | Color preserved on the other side ||
41+
| 3.9 | Fill color | Both | Draw shape with fill (semi-transparent) | Fill color and opacity preserved ||
42+
| 3.10 | Shape with remarks | Both | Add description text to a shape | Remarks text preserved on the other side (if fits under MTU) ||
43+
| 3.11 | Ellipse | Either | Draw an ellipse (major ≠ minor) | Correct ellipse proportions on other side ||
44+
45+
## 4. Markers — Points of Interest
46+
47+
| # | Test | Direction | Steps | Expected | Pass |
48+
|---|------|-----------|-------|----------|------|
49+
| 4.1 | Spot marker | ATAK → iTAK | Place a spot marker | iTAK shows marker at correct location with callsign ||
50+
| 4.2 | Spot marker | iTAK → ATAK | Place a spot marker | ATAK shows marker at correct location with callsign ||
51+
| 4.3 | Waypoint | Either | Place a waypoint | Other side shows waypoint marker ||
52+
| 4.4 | Checkpoint | Either | Place a checkpoint | Other side shows checkpoint marker ||
53+
| 4.5 | Marker color | Both | Set marker color (non-default) | Color preserved on the other side ||
54+
| 4.6 | Custom icon marker | Either | Place marker with custom icon | Icon type/category preserved (iconset path) ||
55+
| 4.7 | 2525 symbol | ATAK → iTAK | Place a 2525B military symbol | iTAK shows correct symbology ||
56+
| 4.8 | Marker with remarks | Both | Add description to marker | Remarks preserved (if fits under MTU) ||
57+
58+
## 5. Routes — Waypoint Sequences
59+
60+
| # | Test | Direction | Steps | Expected | Pass |
61+
|---|------|-----------|-------|----------|------|
62+
| 5.1 | 3-waypoint route | iTAK → ATAK | Create route with 3 waypoints | ATAK imports route via data package, visible in Route Manager ||
63+
| 5.2 | 3-waypoint route | ATAK → iTAK | Create route with 3 waypoints | iTAK shows route on map with waypoints ||
64+
| 5.3 | Route name | Both | Name the route | Route name (callsign) preserved on the other side ||
65+
| 5.4 | Walking method | Either | Create Walking route | Travel method preserved ||
66+
| 5.5 | Infil/Exfil direction | Either | Set route direction | Direction preserved ||
67+
| 5.6 | 5+ waypoint route | Either | Create route with 5-6 waypoints | Route fits under 225B MTU (UID stripping saves space) ||
68+
| 5.7 | Route with remarks | Either | Add description to route | Remarks text preserved (if fits) ||
69+
70+
## 6. Range and Bearing
71+
72+
| # | Test | Direction | Steps | Expected | Pass |
73+
|---|------|-----------|-------|----------|------|
74+
| 6.1 | RAB line | ATAK → iTAK | Create range/bearing measurement | iTAK shows line with distance and bearing ||
75+
| 6.2 | RAB line | iTAK → ATAK | Create range/bearing measurement | ATAK shows line with distance and bearing ||
76+
77+
## 7. Emergency Alerts
78+
79+
| # | Test | Direction | Steps | Expected | Pass |
80+
|---|------|-----------|-------|----------|------|
81+
| 7.1 | 911 alert | Either | Trigger 911 emergency | Other side shows emergency alert ||
82+
| 7.2 | Cancel alert | Either | Cancel the emergency | Other side removes/cancels the alert ||
83+
84+
## 8. CASEVAC / MEDEVAC (9-Line)
85+
86+
| # | Test | Direction | Steps | Expected | Pass |
87+
|---|------|-----------|-------|----------|------|
88+
| 8.1 | Basic CASEVAC | ATAK → iTAK | Create 9-line MEDEVAC request | iTAK receives with precedence, patient counts ||
89+
| 8.2 | Full 9-line | Either | Fill all 9 lines | All fields preserved: precedence, equipment, HLZ marking, security, patients, terrain ||
90+
91+
## 9. Tasking
92+
93+
| # | Test | Direction | Steps | Expected | Pass |
94+
|---|------|-----------|-------|----------|------|
95+
| 9.1 | Task request | Either | Create engage/observe task | Other side receives task with type, target, priority ||
96+
97+
## 10. Delete Events
98+
99+
| # | Test | Direction | Steps | Expected | Pass |
100+
|---|------|-----------|-------|----------|------|
101+
| 10.1 | Delete marker | Either | Delete a previously sent marker | Other side removes the marker from the map ||
102+
| 10.2 | Delete shape | Either | Delete a previously sent shape | Other side removes the shape ||
103+
104+
---
105+
106+
## Feature-Level Verification
107+
108+
### Stale Time Extension
109+
110+
| # | Test | Steps | Expected | Pass |
111+
|---|------|-------|----------|------|
112+
| F.1 | Route stale | Send route from iTAK (2-min default stale) | Route still renders on ATAK (stale extended to 15 min) ||
113+
| F.2 | Shape stale | Send shape from iTAK | Shape still renders on the other side (stale extended) ||
114+
| F.3 | PLI stale unchanged | Send PLI | Stale NOT extended (dynamic position, short stale is correct) ||
115+
116+
### Remarks Preservation (new feature)
117+
118+
| # | Test | Steps | Expected | Pass |
119+
|---|------|-------|----------|------|
120+
| F.4 | Shape with short remarks | Add "Enemy OP spotted" to shape | Remarks visible on the other side ||
121+
| F.5 | Marker with short remarks | Add "Resupply point" to marker | Remarks visible on the other side ||
122+
| F.6 | Shape with very long remarks | Add 500+ character description | Remarks auto-stripped to fit MTU; shape still arrives ||
123+
| F.7 | Route with remarks | Add description to route | Remarks preserved if fits; route still works without them ||
124+
125+
### Route Data Package (ATAK-specific)
126+
127+
| # | Test | Steps | Expected | Pass |
128+
|---|------|-------|----------|------|
129+
| F.8 | Route appears in Route Manager | Send route from iTAK → ATAK | ATAK auto-imports KML data package; route in Route Manager ||
130+
| F.9 | Route waypoints match | Compare waypoint positions | Coordinates match between sender and receiver ||
131+
132+
### GeoChat Remarks Fix (regression check)
133+
134+
| # | Test | Steps | Expected | Pass |
135+
|---|------|-------|----------|------|
136+
| F.10 | iTAK→ATAK chat text | Send "Test message" from iTAK | ATAK shows "Test message" (not empty) ||
137+
| F.11 | ATAK→iTAK chat text | Send "Test message" from ATAK | iTAK shows "Test message" (not empty) ||
138+
139+
### Node Broadcast Removed (regression check)
140+
141+
| # | Test | Steps | Expected | Pass |
142+
|---|------|-------|----------|------|
143+
| F.12 | No phantom nodes | Connect ATAK to mesh with 50+ nodes | ATAK does NOT show all mesh nodes as TAK contacts — only actual TAK users ||
144+
145+
---
146+
147+
## Compression Sanity Checks
148+
149+
| # | Test | Steps | Expected | Pass |
150+
|---|------|-------|----------|------|
151+
| C.1 | PLI under MTU | Send PLI, check Android logs | Compressed size ~70B (well under 225B limit) ||
152+
| C.2 | GeoChat under MTU | Send chat, check logs | Compressed size ~80-120B ||
153+
| C.3 | Shape under MTU | Send rectangle, check logs | Compressed size ~100B ||
154+
| C.4 | Route under MTU | Send 3-waypoint route, check logs | Compressed size ~80-135B ||
155+
| C.5 | No dropped packets | Monitor logs during all tests | No "Dropping oversized" warnings ||
156+
157+
---
158+
159+
## Test Pass Summary
160+
161+
| Category | Total | Passed | Failed | Blocked |
162+
|----------|-------|--------|--------|---------|
163+
| PLI | 5 | | | |
164+
| GeoChat | 5 | | | |
165+
| Shapes | 11 | | | |
166+
| Markers | 8 | | | |
167+
| Routes | 7 | | | |
168+
| Range & Bearing | 2 | | | |
169+
| Emergency | 2 | | | |
170+
| CASEVAC | 2 | | | |
171+
| Tasking | 1 | | | |
172+
| Delete | 2 | | | |
173+
| Features | 12 | | | |
174+
| Compression | 5 | | | |
175+
| **Total** | **62** | | | |

csharp/src/Meshtastic.TAK/CotXmlBuilder.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,22 @@ public string Build(Meshtastic.Protobufs.TAKPacketV2 pkt)
129129
var how = CotTypeMapper.HowToString((int)pkt.How) ?? "m-g";
130130
var lat = pkt.LatitudeI / 1e7;
131131
var lon = pkt.LongitudeI / 1e7;
132+
if (pkt.PayloadVariantCase == TAKPacketV2.PayloadVariantOneofCase.Route
133+
&& pkt.LatitudeI == 0 && pkt.LongitudeI == 0
134+
&& pkt.Route.Links.Count > 0)
135+
{
136+
lat = (pkt.Route.Links[0].Point?.LatDeltaI ?? 0) / 1e7;
137+
lon = (pkt.Route.Links[0].Point?.LonDeltaI ?? 0) / 1e7;
138+
}
132139

133140
var sb = new StringBuilder();
134141
sb.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
135142
sb.AppendLine($"<event version=\"2.0\" uid=\"{Esc(pkt.Uid)}\" type=\"{Esc(cotType)}\" how=\"{Esc(how)}\" time=\"{timeStr}\" start=\"{timeStr}\" stale=\"{staleStr}\">");
136143
sb.AppendLine($" <point lat=\"{lat}\" lon=\"{lon}\" hae=\"{pkt.Altitude}\" ce=\"9999999\" le=\"9999999\"/>");
137144
sb.AppendLine(" <detail>");
138145

139-
if (!string.IsNullOrEmpty(pkt.Callsign))
146+
var isRoute = pkt.PayloadVariantCase == TAKPacketV2.PayloadVariantOneofCase.Route;
147+
if (!string.IsNullOrEmpty(pkt.Callsign) && !isRoute)
140148
{
141149
var ep = string.IsNullOrEmpty(pkt.Endpoint) ? "0.0.0.0:4242:tcp" : pkt.Endpoint;
142150
var tag = $" <contact callsign=\"{Esc(pkt.Callsign)}\" endpoint=\"{Esc(ep)}\"";
@@ -252,7 +260,7 @@ public string Build(Meshtastic.Protobufs.TAKPacketV2 pkt)
252260
EmitRab(sb, pkt.Rab, pkt.LatitudeI, pkt.LongitudeI);
253261
break;
254262
case TAKPacketV2.PayloadVariantOneofCase.Route:
255-
EmitRoute(sb, pkt.Route, pkt.LatitudeI, pkt.LongitudeI, pkt.Uid, pkt.Remarks);
263+
EmitRoute(sb, pkt.Route, pkt.LatitudeI, pkt.LongitudeI, pkt.Uid, pkt.Remarks, pkt.Callsign);
256264
break;
257265
case TAKPacketV2.PayloadVariantOneofCase.Casevac:
258266
EmitCasevac(sb, pkt.Casevac);
@@ -414,7 +422,7 @@ private void EmitRab(StringBuilder sb, RangeAndBearing rab, int eventLatI, int e
414422
sb.AppendLine($" <strokeWeight value=\"{F(rab.StrokeWeightX10 / 10.0)}\"/>");
415423
}
416424

417-
private void EmitRoute(StringBuilder sb, Route route, int eventLatI, int eventLonI, string eventUid = "", string remarks = "")
425+
private void EmitRoute(StringBuilder sb, Route route, int eventLatI, int eventLonI, string eventUid = "", string remarks = "", string callsign = "")
418426
{
419427
// Emit <link> elements BEFORE <link_attr> (ATAK expects waypoints first)
420428
for (var idx = 0; idx < route.Links.Count; idx++)
@@ -452,6 +460,14 @@ private void EmitRoute(StringBuilder sb, Route route, int eventLatI, int eventLo
452460
sb.AppendLine(" <remarks/>");
453461
// routeinfo with navcues child (after link_attr)
454462
sb.AppendLine(" <__routeinfo><__navcues/></__routeinfo>");
463+
sb.AppendLine(" <strokeColor value=\"-1\"/>");
464+
var strokeW = route.StrokeWeightX10 > 0 ? F(route.StrokeWeightX10 / 10.0) : "3";
465+
sb.AppendLine($" <strokeWeight value=\"{strokeW}\"/>");
466+
sb.AppendLine(" <strokeStyle value=\"solid\"/>");
467+
if (!string.IsNullOrEmpty(callsign))
468+
sb.AppendLine($" <contact callsign=\"{Esc(callsign)}\"/>");
469+
sb.AppendLine(" <labels_on value=\"false\"/>");
470+
sb.AppendLine(" <color value=\"-1\"/>");
455471
}
456472

457473
private void EmitCasevac(StringBuilder sb, CasevacReport c)

kotlin/src/main/kotlin/org/meshtastic/tak/CotXmlBuilder.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,19 @@ class CotXmlBuilder {
128128
val cotType = packet.cotTypeString()
129129
val how = packet.howString().ifEmpty { "m-g" }
130130

131-
val lat = packet.latitudeI / 1e7
132-
val lon = packet.longitudeI / 1e7
131+
var lat = packet.latitudeI / 1e7
132+
var lon = packet.longitudeI / 1e7
133133
val hae = packet.altitude
134134

135+
// Routes from ATAK use 0,0 as the event anchor. Use the first
136+
// waypoint's coordinates so the receiving TAK client can locate
137+
// the route on the map.
138+
val routePayload = packet.payload as? TakPacketV2Data.Payload.Route
139+
if (routePayload != null && packet.latitudeI == 0 && packet.longitudeI == 0 && routePayload.links.isNotEmpty()) {
140+
lat = routePayload.links.first().latI / 1e7
141+
lon = routePayload.links.first().lonI / 1e7
142+
}
143+
135144
sb.append("""<?xml version="1.0" encoding="UTF-8"?>""")
136145
sb.append("\n")
137146
sb.append("""<event version="2.0" uid="${esc(packet.uid)}" type="${esc(cotType)}" how="${esc(how)}" """)

protobufs

python/src/meshtastic_tak/cot_xml_builder.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def build(self, packet: atak_pb2.TAKPacketV2) -> str:
7272
how = CotTypeMapper.how_to_string(packet.how) or "m-g"
7373
lat = packet.latitude_i / 1e7
7474
lon = packet.longitude_i / 1e7
75+
if (packet.WhichOneof("payload_variant") == "route"
76+
and packet.latitude_i == 0 and packet.longitude_i == 0
77+
and len(packet.route.links) > 0):
78+
lat = packet.route.links[0].point.lat_delta_i / 1e7
79+
lon = packet.route.links[0].point.lon_delta_i / 1e7
7580

7681
lines = [
7782
'<?xml version="1.0" encoding="UTF-8"?>',
@@ -81,7 +86,8 @@ def build(self, packet: atak_pb2.TAKPacketV2) -> str:
8186
' <detail>',
8287
]
8388

84-
if packet.callsign:
89+
is_route = packet.WhichOneof("payload_variant") == "route"
90+
if packet.callsign and not is_route:
8591
ep = packet.endpoint or "0.0.0.0:4242:tcp"
8692
parts = [f'callsign="{escape(packet.callsign)}"', f'endpoint="{escape(ep)}"']
8793
if packet.phone: parts.append(f'phone="{escape(packet.phone)}"')
@@ -189,7 +195,7 @@ def build(self, packet: atak_pb2.TAKPacketV2) -> str:
189195
elif which == "rab":
190196
self._emit_rab(lines, packet.rab, packet.latitude_i, packet.longitude_i)
191197
elif which == "route":
192-
self._emit_route(lines, packet.route, packet.latitude_i, packet.longitude_i, packet.uid, packet.remarks)
198+
self._emit_route(lines, packet.route, packet.latitude_i, packet.longitude_i, packet.uid, packet.remarks, packet.callsign)
193199
elif which == "casevac":
194200
self._emit_casevac(lines, packet.casevac)
195201
elif which == "emergency":
@@ -337,7 +343,7 @@ def _emit_rab(self, lines: list, rab, event_lat_i: int, event_lon_i: int) -> Non
337343
w = rab.stroke_weight_x10 / 10.0
338344
lines.append(f' <strokeWeight value="{w}"/>')
339345

340-
def _emit_route(self, lines: list, route, event_lat_i: int, event_lon_i: int, event_uid: str = "", remarks: str = "") -> None:
346+
def _emit_route(self, lines: list, route, event_lat_i: int, event_lon_i: int, event_uid: str = "", remarks: str = "", callsign: str = "") -> None:
341347
# Emit <link> elements BEFORE <link_attr> (ATAK expects waypoints first)
342348
for idx, link in enumerate(route.links):
343349
llat = (event_lat_i + link.point.lat_delta_i) / 1e7
@@ -375,6 +381,14 @@ def _emit_route(self, lines: list, route, event_lat_i: int, event_lon_i: int, ev
375381
lines.append(' <remarks/>')
376382
# routeinfo with navcues child (after link_attr)
377383
lines.append(' <__routeinfo><__navcues/></__routeinfo>')
384+
lines.append(' <strokeColor value="-1"/>')
385+
sw = route.stroke_weight_x10 / 10.0 if route.stroke_weight_x10 > 0 else 3
386+
lines.append(f' <strokeWeight value="{sw}"/>')
387+
lines.append(' <strokeStyle value="solid"/>')
388+
if callsign:
389+
lines.append(f' <contact callsign="{escape(callsign)}"/>')
390+
lines.append(' <labels_on value="false"/>')
391+
lines.append(' <color value="-1"/>')
378392

379393
# --- CasevacReport / EmergencyAlert / TaskRequest reverse lookups ----
380394

0 commit comments

Comments
 (0)