Every TAK deployment we scope eventually arrives at the same question. The server is up, the field teams have ATAK on their phones, the map works — and then someone asks why the gate cameras, the boat tracker and the drone aren’t on it.
They aren’t on it because none of them speak TAK. A TAK map only understands Cursor on Target (CoT) events, and almost nothing outside the TAK ecosystem emits CoT natively. An NVR sends plate reads over its own webhook. An AIS receiver outputs NMEA sentences. A drone ground station publishes MAVLink or a vendor API. Something has to sit in the middle, translate each source into CoT, and do it in a way that keeps the map useful rather than drowning it. That something is a CoT bridge, and it is most of the actual engineering in a sensor-heavy TAK deployment.
Our earlier posts covered how TAK works, the idea of pushing Wazuh alerts onto a TAK map, and the hub-and-spoke architecture for multi-site OpenTAKServer deployments. This one goes a layer down: what a production bridge has to decide, how it should connect, and a working example you can run.
1. What a bridge actually does
A bridge has four jobs, in this order:
flowchart TD
SRC["Source feed such as NVR webhook AIS or drone telemetry"] --> PARSE["Parse and validate the raw payload"]
PARSE --> FILTER["Filter low confidence and out of area data"]
FILTER --> MAP["Map to a CoT event with UID type and stale time"]
MAP --> RATE["Rate control with dedupe and latest wins per UID"]
RATE --> TLS["Send over mutual TLS to the TAK server"]
TLS --> CLIENTS["ATAK WinTAK and iTAK clients"]
RATE --> BUF["Local buffer while the server link is down"]
BUF --> TLS
Parse the source’s own format. Filter what nobody should see — low-confidence detections, positions outside the area of interest, test traffic. Map what’s left into a CoT event, which is where most of the design decisions live. Control the rate, because sensors report far more often than a human can usefully read a map. Then send it over an authenticated connection, and buffer when that connection is down.
Most failed bridges skip the filter and rate stages. They translate faithfully, send everything, and a week later the operators turn the layer off because the map is unreadable.
2. The anatomy of a CoT event
A CoT event is a small XML document. Here is the one our example bridge produces for a single plate read:
<event version="2.0" uid="anpr-gate-north-1KB1234" type="a-u-G-E-V" how="m-p"
time="2026-10-01T04:14:14Z" start="2026-10-01T04:14:14Z" stale="2026-10-01T04:24:14Z">
<point lat="13.7563" lon="100.5018" hae="0" ce="15.0" le="9999999"/>
<detail>
<contact callsign="Plate 1KB1234"/>
<remarks>ANPR gate-north confidence 0.93</remarks>
</detail>
</event>
| Field | What it means | What the bridge must decide |
|---|---|---|
uid |
Identity of the thing being reported; persists across reports | How to build a stable ID for each real-world entity |
type |
Hierarchical class: a for a physical thing, then affiliation (f friendly, h hostile, n neutral, u unknown), then dimension (G ground, A air, S sea surface, U subsurface), then more specific function |
Which icon and affiliation the sensor is honestly allowed to claim |
how |
How the report was produced — starts with m for machine-generated or h for human-entered |
Machine for sensor feeds, almost always |
time / start / stale |
When the event was generated, when the state became valid, and when it should stop being treated as current | How long each source’s reports stay true |
point |
Latitude, longitude, height above the WGS84 ellipsoid (hae), and horizontal and vertical 1-sigma error in metres (ce, le) |
How precise the position really is |
detail |
Open-ended extension element: contact, track, remarks and anything else clients understand |
What the operator needs to see when they tap the marker |
The convention for "unknown" error is a very large number; 9999999 is what PyTAK itself emits when it has no real value. Using it is better than inventing precision.
3. Four decisions every bridge makes
UID: one real-world thing, one UID, forever
The UID is how TAK knows that a new report is an update to an existing marker rather than a new marker. Get it wrong and you get one of two failures: a new icon on every report (a trail of hundreds of duplicate boats), or several real-world things collapsing onto one icon.
Build UIDs from the most stable identifier the source has. For AIS that is the vessel’s MMSI. For a drone it is the airframe serial or remote ID, not the flight number. For ANPR it depends on what you want the map to show: camera + plate gives one marker per vehicle per gate; plate alone gives one marker that jumps between gates. Both are defensible — but decide deliberately, and prefix UIDs by source (ais-, anpr-, uas-) so two sources can never collide.
Plates in non-Latin scripts — Thai, Japanese, Chinese — need one more step. XML carries them fine, but UIDs are safest as plain ASCII across every client and tool. Map the plate to an agreed ASCII code or a hash for the UID, and show the real plate in the callsign, which is what operators actually read.
Type: claim only what the sensor actually knows
A plate camera knows there is a vehicle. It does not know whether that vehicle is friendly or hostile, so the honest affiliation is u — unknown — and the type is a ground vehicle (a-u-G-E-V). The same discipline applies everywhere: an AIS contact is a sea-surface track of unknown affiliation until someone with authority says otherwise; your own drone is friendly air. Bridges that mark everything hostile to make it stand out train operators to ignore red.
Stale time: how long is this report true?
Stale time is the most under-designed field in most bridges, and it is the one that most affects what operators see. When the stale time passes, clients stop treating the marker as current. Set it per source, from how often that source reports and how fast the thing moves:
| Source | Typical update cadence | A reasonable stale time |
|---|---|---|
| Drone telemetry | About once per second | Tens of seconds — a drone that stops reporting is news |
| AIS vessel | Seconds to minutes, depending on speed and class | A few minutes, longer for anchored vessels |
| ANPR plate read | One event per pass | Minutes — it records a sighting, not a live track |
| Fixed sensor alarm | On state change | Until the alarm is cleared, refreshed by a heartbeat |
A stale time that is too long leaves ghosts on the map. One that is too short makes markers flicker in and out between updates. Neither is visible in a lab with one test device, which is why it gets missed.
Position error: report the uncertainty you have
A plate read is located at the camera, not at the car, so its ce should be the camera’s coverage radius, not zero. An AIS position has its own accuracy; an estimated position from a human report has much more. Clients can use ce to show uncertainty, but only if the bridge fills it in honestly.
4. Rate control: the part that keeps the map readable
A single busy gate camera can produce hundreds of plate reads an hour, many of them the same vehicle idling in front of the lens. A drone publishes telemetry several times a second. TAK clients on a 4G link do not need either at that rate.
Three techniques, usually combined:
- Drop at the edge. Discard detections below a confidence threshold and anything outside the area of interest before they cost bandwidth.
- Deduplicate per UID. If the same entity was sent less than N seconds ago and hasn’t moved meaningfully, skip it.
- Latest wins. When the outbound queue backs up, keep only the newest event per UID rather than sending a backlog of stale positions in order. PyTAK’s own default behaviour points in this direction: its outbound queue holds 100 events and drops the oldest when full.
5. Transport: mutual TLS, not plaintext, not HTTP
On OpenTAKServer the standard deployment exposes plaintext CoT streaming on port 8088, mutual-TLS CoT streaming on 8089, and automatic certificate enrollment on 8446. A production bridge should connect on 8089 with its own client certificate.
That matters for more than encryption. With mutual TLS, the server knows which bridge sent each event, you can revoke a single bridge’s certificate if its host is compromised, and you can put each bridge in a TAK group so a camera feed from one site isn’t broadcast to every team in the country. A bridge on plaintext 8088 is anonymous, and anything that can reach the port can inject markers onto your operators’ maps.
Two related details:
- XML or protobuf. TAK clients and servers can negotiate an upgrade from the original XML encoding ("version 0") to a Protocol Buffers encoding ("version 1"), but every client must still decode XML. A bridge can send XML and let the server handle the rest; switch to protobuf only if bandwidth on the bridge’s own link is genuinely tight.
- Buffer on disconnect. Edge sites lose their uplink. A bridge should keep a bounded local buffer — applying latest-wins per UID — and flush it on reconnect, instead of losing everything or replaying an hour of positions in order. This is the bridge-level version of the federation question we discussed in the hub-and-spoke post.
6. A working example: ANPR plate reads to CoT
Here is a complete, minimal bridge that takes plate reads from an NVR, filters and deduplicates them, and sends CoT using PyTAK, the open-source Python TAK library. We tested it against a local listener: of three sample reads — one good read, the same plate again immediately after, and a low-confidence read — exactly one CoT event went out.
"""Minimal ANPR-to-CoT bridge: NVR plate reads in, CoT events out to a TAK server."""
import asyncio
import time
import xml.etree.ElementTree as ET
from configparser import ConfigParser
import pytak
# Fixed camera positions come from the site survey, not from the NVR payload.
CAMERAS = {
"gate-north": {"lat": 13.7563, "lon": 100.5018, "ce": 15.0},
}
MIN_INTERVAL_S = 30 # same plate at the same camera: at most one update per 30 s
STALE_S = 600 # a plate sighting stops being "current" after 10 minutes
def anpr_to_cot(read: dict) -> bytes:
"""Map one NVR plate read to a CoT event."""
cam = CAMERAS[read["camera_id"]]
plate = read["plate"].replace(" ", "").upper()
event = ET.Element(
"event",
version="2.0",
uid=f"anpr-{read['camera_id']}-{plate}", # stable per plate and camera
type="a-u-G-E-V", # unknown ground vehicle
how="m-p", # machine-generated, relayed by the bridge
time=pytak.cot_time(),
start=pytak.cot_time(),
stale=pytak.cot_time(STALE_S),
)
ET.SubElement(event, "point", lat=str(cam["lat"]), lon=str(cam["lon"]),
hae="0", ce=str(cam["ce"]), le="9999999")
detail = ET.SubElement(event, "detail")
ET.SubElement(detail, "contact", callsign=f"Plate {plate}")
ET.SubElement(detail, "remarks").text = (
f"ANPR {read['camera_id']} confidence {read['confidence']:.2f}"
)
return ET.tostring(event)
class AnprSender(pytak.QueueWorker):
"""Reads plate events from a local queue, rate-limits per UID, sends CoT."""
def __init__(self, tx_queue, config, source: asyncio.Queue):
super().__init__(tx_queue, config)
self.source = source
self.last_sent: dict[str, float] = {}
async def handle_data(self, data: bytes) -> None:
await self.put_queue(data) # hand the CoT event to pytak's TX worker
async def run(self, number_of_iterations=-1):
while True:
read = await self.source.get()
if read["confidence"] < 0.80: # drop low-confidence reads at the edge
continue
key = f"{read['camera_id']}-{read['plate'].replace(' ', '').upper()}"
now = time.monotonic()
if now - self.last_sent.get(key, 0) < MIN_INTERVAL_S:
continue # duplicate within the window
self.last_sent[key] = now
await self.handle_data(anpr_to_cot(read))
async def main(cot_url: str, reads: list[dict]):
config = ConfigParser()
config["bridge"] = {"COT_URL": cot_url}
# For mutual TLS on port 8089, also set PYTAK_TLS_CLIENT_CERT,
# PYTAK_TLS_CLIENT_KEY and PYTAK_TLS_CLIENT_CAFILE here.
source: asyncio.Queue = asyncio.Queue()
for r in reads:
source.put_nowait(r)
clitool = pytak.CLITool(config["bridge"])
await clitool.setup()
clitool.add_tasks({AnprSender(clitool.tx_queue, config["bridge"], source)})
await clitool.run()
Three things worth noticing:
- Positions come from the site survey, not the NVR. The camera’s coordinates and coverage radius live in the bridge’s configuration, because most NVRs don’t know where they are.
handle_datamust be implemented. In PyTAK’sQueueWorkerit is a placeholder; it has to put the event on the transmit queue. We found this the honest way — our first test run sent only PyTAK’s own connection ping.- TLS is configuration, not code. Set
COT_URLtotls://your-server:8089and pointPYTAK_TLS_CLIENT_CERT,PYTAK_TLS_CLIENT_KEYandPYTAK_TLS_CLIENT_CAFILEat the bridge’s certificate, key and the server’s CA. LeavePYTAK_TLS_DONT_VERIFYat its default of0.
A production version adds a webhook listener in place of the sample list, a persistent buffer, health metrics, and a heartbeat event so operators can see the bridge itself go stale when it stops reporting.
7. Operating a bridge
A bridge is a service that people will stop trusting the first time it lies to them, so it needs the same care as any production service:
- Make the bridge visible. Publish a heartbeat CoT for the bridge itself. When a site’s bridge dies, operators should see its marker go stale — not silently lose a layer.
- Log what you dropped, not just what you sent. When someone asks why a vehicle wasn’t on the map, the answer is usually a filter. You want to be able to show which one.
- Version the mappings. Type codes, stale times and thresholds are operational decisions. Keep them in configuration under version control, so a change is reviewable and reversible.
- One certificate per bridge. Never share client certificates across bridges or sites; revocation only works if each certificate maps to exactly one thing.
8. What Simplico builds, and what is scoped per mission
Bridge builds are a fixed-scope engagement on our TAK Integration page: a bridge for a defined set of sources, with the source code delivered and a handover included. Sources we regularly bridge include drone and UAV telemetry, CCTV/NVR and ANPR events, GPS trackers, AIS, ADS-B, perimeter sensors and SCADA alarms.
Scoped per mission: custom ATAK or WinTAK plugins (for example, video tiles or mission-specific dialogs), live video routing, protobuf transport, and the two-way link between a SOC and TAK, where cyber detections become map events and field incidents become cases. Each of these is a separate piece of work, priced once we understand the sources and the operating environment.
Out of scope: the sensors themselves. We integrate the cameras, receivers and drones you already own or procure; we don’t supply or mark up hardware.
FAQ
Can’t we just POST events to the TAK server over HTTP?
For a prototype, yes. For production, a streaming connection over mutual TLS is the better default: it identifies each bridge, supports group-based routing and revocation, and avoids the per-request overhead of HTTP for high-rate sources.
Should a bridge run at the edge site or centrally?
At the edge whenever the source is at the edge. Filtering and deduplication close to the sensor save the uplink, and the bridge can buffer through a link outage. Central bridges suit sources that are already centralised, such as an AIS aggregator feed.
Which TAK server does this work with?
The bridge speaks standard CoT over TCP or TLS, so it works with OpenTAKServer, FreeTAKServer and the official TAK Server. Ports and enrollment details differ by server; the ones in this post are OpenTAKServer’s.
Does every sensor need its own bridge?
One bridge process per source type is usually cleanest — one for ANPR, one for AIS, one for drones — because each has a different rate, stale profile and failure mode. They can share a codebase and a deployment.
What about personal data in plate reads?
A plate is personal data in most jurisdictions. Decide who is allowed to see the ANPR layer, put it in a restricted TAK group, and keep the stale time short so the map is a live picture rather than a movement history.
If you have sensors that should be on a TAK map and aren’t, our two-week readiness assessment maps each source, its rate and its connectivity before anything is built. Contact hello@simplico.net, phone (+66) 97 496 6397, WhatsApp (+66) 83 001 0222, or LINE iiitum1984.
Sources:
- TAK Integration Services — Simplico
- PyTAK — GitHub
- PyTAK configuration reference
- TAK Protocol (takproto) README — AndroidTacticalAssaultKit-CIV
- Cursor on Target format: developer reference — Corvus Intelligence
- OpenTAKServer certificate enrollment
- OpenTAKServer Helm chart (port reference)
Latest Posts
- Can an LLM Predict a Flood? What AI Actually Does in Flood Forecasting, and Where Drones and Gauge Cameras Fit September 27, 2026
- Running Flood Response Like a SOC: A Detection-and-Response Blueprint for Thailand’s Water Crises September 26, 2026
- From Whiteboard to Dashboard: Vehicle Load Planning Meets Live GPS Tracking September 22, 2026
- From Paper to Pipeline: Digitizing Multi-Factory Precast Slab Production, Warehouse, and Delivery September 20, 2026
- What Simplico Builds: A Product Portfolio Overview and What Each One Actually Gets You September 12, 2026
- Inside simpliMES: How One Django Core Runs Discrete and Batch Manufacturing on the Same Engine September 7, 2026