Node-RED¶
FastTAK includes Node-RED with node-red-contrib-postgresql pre-installed for querying the PostGIS database.
Pre-configured Config Nodes¶
Two config nodes are available out of the box — no credentials or certificates to set up:
| Config Node | Type | Use With |
|---|---|---|
| FastTAK PostGIS | PostgreSQL | Any postgresql node — select it as the server |
| FastTAK Server TLS | TLS | Any tcp in or tcp out node that talks to TAK Server |
Sending / Receiving CoT¶
To connect to TAK Server, add a tcp out (send) or tcp in (receive) node and:
- Set Host to
tak-server - Set Port to
8089 - Enable TLS and select FastTAK Server TLS
For receiving, set the tcp in node to split on </event> so each CoT event arrives
as a complete message.
Note
A nodered LDAP user is automatically created in the tak_ROLE_ADMIN group,
so CoT messages sent from Node-RED flows reach all connected TAK clients.
GeoJSON to CoT¶
The flow below converts any GeoJSON source into Cursor on Target (CoT) XML and pushes it to TAK Server. It uses a configurable function node — instead of hardcoding CoT parameters in the code, configuration is passed on the message via a change node. This means you can reuse the same function node for completely different data sources, each with its own CoT type, icon, stale time, and property mappings.
How the Config Pattern Works¶
Each data source gets its own config by wiring a change node before the function:
[Data Source A] → [Config A] ──→ [GeoJSON to CoT] → [TAK Server]
[Data Source B] → [Config B] ──↗
The config rides on the message (msg.cotType, msg.uidPrefix, etc.), so when
Data Source A's message arrives, it carries Config A's settings. When Data Source B's
message arrives, it carries Config B's settings. The function node reads config from
whichever message it receives.
This is useful when you have a single GeoJSON feed that contains different event types. For example, a sensor API might return both temperature alerts and motion detections. You can split the feed with a switch node, apply different configs, and push both to TAK with different icons:
┌→ [Temperature config] ──→ [GeoJSON to CoT] → [TAK Server]
[Sensor API] → [Switch]
└→ [Motion config] ──↗
Temperature alerts show as one icon type, motion detections as another — all from the same API call, through the same converter, to the same TAK Server connection.
Configuration Properties¶
Set these on the message via a change node wired before the function:
| msg property | Default | Description |
|---|---|---|
msg.cotType |
a-f-G-U-C |
CoT event type (determines TAK icon) |
msg.cotHow |
m-g |
How the event was generated (m-g = machine-generated) |
msg.uidPrefix |
geojson |
Prefix for unique IDs |
msg.staleMinutes |
60 |
Minutes until marker expires on TAK clients |
msg.callsignProperty |
name |
GeoJSON property to use as the marker callsign |
msg.remarksProperty |
remarks |
GeoJSON property to use as the marker description |
msg.altitudeProperty |
GeoJSON property for altitude override (empty = use geometry z) |
Common CoT Types¶
The cotType determines the icon displayed on TAK clients. Types follow
MIL-STD-2525:
first letter is a (atom/entity) or b (bits/point), second is affiliation
(f=friendly, h=hostile, n=neutral, u=unknown), then hierarchy
(G=ground, A=air, S=sea).
| Type | Description |
|---|---|
a-f-G-U-C |
Ground unit |
a-f-G-E-S |
Sensor |
a-f-A-M-H-Q |
Drone/UAV (rotary wing/quadcopter) |
a-f-A-M-F-Q |
Drone/uav (fixed wing) |
a-f-g-i-i-e-icp |
incident command post |
a-f-g-i-i-m |
emergency medical services |
b-m-p-s-m |
seismic event |
Tutorial: USGS Earthquakes on the TAK Map¶
This tutorial walks through building a flow that polls the USGS earthquake feed every five minutes and pushes each event to TAK Server as a seismic event marker. It demonstrates the configurable GeoJSON-to-CoT pattern with a real data source.
Step 1 — Create the data source¶
- Create a new flow tab — double-click the tab and name it
USGS Earthquakes to TAK - Drag an inject node onto the canvas
- Double-click the inject node to configure it:
- Set Name to
Every 5 Min - Delete all message payload properties (click the X next to each one)
- Check Inject once after 0 seconds so it fires on deploy
- Set Repeat to interval, every
5minutes - Click Done
- Drag an http request node and wire it to the inject
- Set Method to GET
- Set URL to
https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson - Set Return to a parsed JSON object
Step 2 — Add the config¶
- Drag a change node and wire it to the http request output
- Add rules to set these
msgproperties: msg.cotType→b-m-p-s-m(seismic event)msg.uidPrefix→usgsmsg.staleMinutes→60(set type to number)msg.callsignProperty→titlemsg.remarksProperty→place
Step 3 — Add the converter¶
- Drag a function node and wire it to the change node
- Set Name to
GeoJSON to CoT - Paste the following code into the function body:
// Read config from the message (set by the change node in Step 2)
const cotType = msg.cotType || "a-f-G-U-C"; // CoT event type
const cotHow = msg.cotHow || "m-g"; // m-g = machine-generated
const uidPrefix = msg.uidPrefix || "geojson"; // prefix for marker UIDs
const staleMinutes = msg.staleMinutes || 60; // marker expiry time
const callsignProp = msg.callsignProperty || "name"; // GeoJSON prop → callsign
const remarksProp = msg.remarksProperty || "remarks"; // GeoJSON prop → remarks
const altProp = msg.altitudeProperty || ""; // GeoJSON prop → altitude
// Accept a FeatureCollection or a single Feature
const features = msg.payload.features || [msg.payload];
const messages = [];
for (const f of features) {
if (!f.geometry || !f.geometry.coordinates) continue;
const coords = f.geometry.coordinates;
const lon = coords[0];
const lat = coords[1];
// Use altitude property if specified, otherwise fall back to geometry z
const alt =
altProp && f.properties[altProp] != null
? f.properties[altProp]
: coords[2] || 0;
const props = f.properties || {};
const uid = `${uidPrefix}-${f.id || Date.now()}`;
const callsign = props[callsignProp] || uid;
const remarks = props[remarksProp] || "";
// Build timestamps — stale controls how long the marker lives on the map
const now = new Date().toISOString();
const stale = new Date(Date.now() + staleMinutes * 60000).toISOString();
// Build CoT XML event
const cot = `<?xml version="1.0" encoding="UTF-8"?>
<event version="2.0"
uid="${uid}"
type="${cotType}"
how="${cotHow}"
time="${now}"
start="${now}"
stale="${stale}">
<point lat="${lat}" lon="${lon}" hae="${alt}" ce="9999999" le="9999999"/>
<detail>
<remarks>${remarks}</remarks>
<contact callsign="${callsign}"/>
</detail>
</event>`;
messages.push({ payload: cot });
}
// Return all messages at once — Node-RED sends them in sequence
return [messages];
Step 4 — Add the TAK Server output¶
- Drag a tcp out node and wire it to the function output
- Set Host to
tak-server, Port to8089 - Enable TLS and select FastTAK Server TLS
Step 5 — Deploy and verify¶
Click Deploy. The inject node fires immediately, fetches the latest earthquakes, and pushes them to TAK Server. Open ATAK, WinTAK, or iTAK to see the markers.
Import This Flow¶
Or skip the manual setup — copy the JSON below, then in Node-RED go to Menu → Import → Clipboard and paste:
[
{
"id": "geojson-cot-tab",
"type": "tab",
"label": "GeoJSON to CoT",
"info": "Configurable GeoJSON to CoT converter. Each data source carries its own config on the message."
},
{
"id": "geojson-cot-comment",
"type": "comment",
"z": "geojson-cot-tab",
"name": "Data Source → Config → GeoJSON to CoT → TAK Server",
"info": "Wire a change node before the function to set msg.cotType, msg.uidPrefix, etc.\nEach source can have its own config. Duplicate the source chain to add more feeds.",
"x": 280,
"y": 40,
"wires": []
},
{
"id": "geojson-cot-usgs-inject",
"type": "inject",
"z": "geojson-cot-tab",
"name": "Every 5 min",
"props": [],
"repeat": "300",
"once": true,
"onceDelay": "0",
"topic": "",
"x": 130,
"y": 120,
"wires": [["geojson-cot-usgs-http"]]
},
{
"id": "geojson-cot-usgs-http",
"type": "http request",
"z": "geojson-cot-tab",
"name": "USGS Feed",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 300,
"y": 120,
"wires": [["geojson-cot-usgs-config"]]
},
{
"id": "geojson-cot-usgs-config",
"type": "change",
"z": "geojson-cot-tab",
"name": "Earthquake config",
"rules": [
{
"t": "set",
"p": "cotType",
"pt": "msg",
"to": "b-m-p-s-m",
"tot": "str"
},
{ "t": "set", "p": "uidPrefix", "pt": "msg", "to": "usgs", "tot": "str" },
{
"t": "set",
"p": "staleMinutes",
"pt": "msg",
"to": "60",
"tot": "num"
},
{
"t": "set",
"p": "callsignProperty",
"pt": "msg",
"to": "title",
"tot": "str"
},
{
"t": "set",
"p": "remarksProperty",
"pt": "msg",
"to": "place",
"tot": "str"
}
],
"x": 510,
"y": 120,
"wires": [["geojson-cot-function"]]
},
{
"id": "geojson-cot-function",
"type": "function",
"z": "geojson-cot-tab",
"name": "GeoJSON to CoT",
"func": "const cotType = msg.cotType || 'a-f-G-U-C';\nconst cotHow = msg.cotHow || 'm-g';\nconst uidPrefix = msg.uidPrefix || 'geojson';\nconst staleMinutes = msg.staleMinutes || 60;\nconst callsignProp = msg.callsignProperty || 'name';\nconst remarksProp = msg.remarksProperty || 'remarks';\nconst altProp = msg.altitudeProperty || '';\n\nconst features = msg.payload.features || [msg.payload];\nconst messages = [];\n\nfor (const f of features) {\n if (!f.geometry || !f.geometry.coordinates) continue;\n\n const coords = f.geometry.coordinates;\n const lon = coords[0];\n const lat = coords[1];\n const alt = altProp && f.properties[altProp] != null\n ? f.properties[altProp]\n : (coords[2] || 0);\n\n const props = f.properties || {};\n const uid = `${uidPrefix}-${f.id || Date.now()}`;\n const callsign = props[callsignProp] || uid;\n const remarks = props[remarksProp] || '';\n\n const now = new Date().toISOString();\n const stale = new Date(Date.now() + staleMinutes * 60000).toISOString();\n\n const cot = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<event version=\"2.0\"\n uid=\"${uid}\"\n type=\"${cotType}\"\n how=\"${cotHow}\"\n time=\"${now}\"\n start=\"${now}\"\n stale=\"${stale}\">\n <point lat=\"${lat}\" lon=\"${lon}\" hae=\"${alt}\" ce=\"9999999\" le=\"9999999\"/>\n <detail>\n <remarks>${remarks}</remarks>\n <contact callsign=\"${callsign}\"/>\n </detail>\n</event>`;\n\n messages.push({ payload: cot });\n}\n\nreturn [messages];",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 720,
"y": 180,
"wires": [["geojson-cot-tcp"]]
},
{
"id": "geojson-cot-tcp",
"type": "tcp out",
"z": "geojson-cot-tab",
"name": "TAK Server",
"host": "tak-server",
"port": "8089",
"beserver": "client",
"base64": false,
"end": false,
"tls": "fastak-tls",
"close": false,
"x": 920,
"y": 180,
"wires": []
}
]
After importing, click Deploy. The inject node fires immediately, fetches the latest earthquakes from USGS, and pushes them to TAK Server as seismic event markers.
Adding Another Data Source¶
To add a second GeoJSON feed to the same flow:
- Drag an inject node (set your poll interval)
- Drag an http request node (set your GeoJSON URL)
- Drag a change node and set the config properties you want (different
cotType,uidPrefix, etc.) - Wire: inject → http request → change → the existing GeoJSON to CoT function node
Both feeds share the same function node and TAK Server connection. Each arrives with its own config on the message, so they render as different icons on the TAK map.