ZYRNTOPO Documentation
How the ZYRN apps work. ZYRNTOPO is the offline topographic map for Android and the browser, and ZYRNMAPS is its native Android sibling. ZYRN Workstation is the desktop app for Windows, macOS and Linux, and the ZYRN TAK server links all of them with ATAK, WinTAK and iTAK on hardware you control. The apps share one account, one Pro license, your map items and the same plugins.
The core apps are free. ZYRNTOPO and ZYRN Workstation ask you to sign in with a free account, and ZYRNMAPS does not. Your map, your tracks and your markers stay on your device apart from the items you sync to your own account, which you can turn off.
Getting Started
Install, first launch, and finding your way around.
Map Interface
Base layers, overlays, 3D terrain and controls.
Offline Maps
Download an area before you lose signal.
Team Sync
Internet, TAK, Meshtastic and drones.
Search & Rescue
Periods, team members, clues and gear.
Import & Export
GPX, KML, GeoJSON, CSV and PDF.
ZYRNMAPS
The native Android client.
ZYRN Workstation
The desktop app for Windows, macOS and Linux.
TAK Server
Run your own for the whole team.
Plugins
Add tools to every app, write your own and share them.
Installation
ZYRNTOPO runs on Android and in the browser. Use whichever fits the device in your hand; your account and Pro carry across both.
Android
- Install from Google Play. Updates arrive automatically.
- On a Fire tablet, or any device without Google Play, install from the Amazon Appstore instead. It handles installation and updates for you.
- Open the app. A short setup asks how you will use it, your units and your starting map; then it is ready, with no sign-in.
The points-of-interest dataset (peaks, water, trailheads, shelters and more, about 1.4 GB) is not part of the download from the store. The app fetches it once in the background the first time it runs, so open the app for the first time on Wi-Fi; after that, POI search needs no connection. When a newer dataset is published, the saved copy updates itself over Wi-Fi.
Web / PWA
Open app.zyrntopo.com in any modern browser. For offline use, install it as an app: in Chrome or Edge, click Install app in the address bar; in Safari, use Share → Add to Home Screen. See Web / PWA for what the browser version can and cannot do.
Desktop and server
On Windows, macOS and Linux, the desktop app is ZYRN Workstation, installed with one command: see ZYRN Workstation. To run your own TAK server for your team, see ZYRN TAK server. Both install commands check what they download before running it.
First Launch
The first time you open ZYRNTOPO, a short setup asks three things: how you will use it (recreation, search and rescue, and so on), your theme and units, and which map to start on. Without Pro, a last step shows what Pro adds; you can skip it. Everything you choose can be changed later in Settings.
The app then explains why it wants your location and asks with the system dialog. Location is used for your position on the map, tracks, route following and nearby field information. It is foreground-only except while you record a track: the first time you tap Record Track, the app asks separately for background location so the recording continues with the screen off. The microphone is requested the first time you record a voice note, the camera the first time you scan a QR code, and Bluetooth the first time you connect a radio or receiver.
The default map is VersaTiles Eclipse, a detailed street and terrain map. It is hosted, so it needs a connection until you download an area. Your base layer, map position and zoom are remembered and restored every time you open the app.
Account & Pro
ZYRNTOPO and ZYRN Workstation ask you to sign in with a free account when they open. If you are offline or the account server cannot be reached, they let you skip it and sign in later. ZYRNMAPS does not ask. Your account gives you two things:
- Your map items on all your devices. Markers, lines, areas and tracks sync between the devices you are signed in on, in real time. Turn it off with Sync map items on the Account page.
- Pro, if you buy it. The license is checked on your device and keeps working offline; it refreshes in the background when you are online.
Sign in with an email address and password, or with Google, Microsoft or Yahoo. The same account works in ZYRNTOPO, ZYRNMAPS, ZYRN Workstation, the forum and the plugin submissions on this site. Manage it from Menu → Account or on zyrntopo.com/account, where you can:
- set your display name (your callsign) and picture;
- see every app and browser signed in to the account, sign one out, or sign out all the others at once;
- add a password next to a Google, Microsoft or Yahoo sign-in, change your email address, and read the last 25 sign-ins and account changes;
- download everything the account holds as JSON: profile, sign-in methods, license and payments, devices, synced map items, friends, plugin submissions and forum posts;
- close the account, which erases your license, sessions, picture, profile, synced map items and friends, and withdraws any plugins you published. Forum posts stay and show as "Former member".
Pro
Pro is $79.99 once, or $2.99 a month until you cancel. It belongs to the account, not to a device: sign in anywhere and Pro unlocks there, and the license keeps working offline. A monthly plan keeps Pro to the end of the period you paid for after you cancel. Pro can also come from a trial, which ends on its date and leaves the app working as free, or from a seat in an organization's plan. Transfer Pro on the account page moves a license to another person's existing account.
Base Layers
Tap Layers in the bottom bar. The current base map is the top row; tap it to open the picker, grouped by category with a preview of each layer. Twenty base maps are built in and free to use, 54 more work with your own key from a map provider (see Maps with your own key), and a catalog adds more than 1,300 regional and specialist imagery sources.
| Group | Layers | Good for |
|---|---|---|
| Topo & terrain | USGS Topo, USGS Imagery + Topo, OpenTopoMap, NatGeo World, Esri World Topo, Esri USA Topo (Historical) | Contours, trails and terrain; historical USGS quadrangles |
| Imagery | Esri Satellite, VersaTiles Satellite, USGS Imagery, Esri Firefly Imagery, Sentinel-2 Cloudless | Aerial and satellite views; Firefly is muted so your markers stand out |
| Aeronautical | FAA VFR Sectional | Airspace and aviation planning in the US |
| Water | Esri Ocean Basemap, EMODnet Bathymetry, NOAA Nautical Charts (ENC) | Depths, coastlines and water search |
| Street & general | OpenStreetMap, Esri World Street, OSM Humanitarian, CyclOSM | Roads, towns and cycling |
| Vector | VersaTiles Eclipse (default) | A detailed dark map that stays sharp at every zoom |
Maps with your own key
Some map providers only serve their maps to their own customers. If you have an account with one, add your key and its maps join the picker: Google (roadmap, satellite, hybrid and terrain), Bing, Azure Maps, Cesium ion (Bing and Google imagery), Mapbox, MapTiler, HERE, TomTom, Stadia Maps (including the Stamen styles), Thunderforest and Ordnance Survey for Great Britain. Add keys under Map Data → Map provider keys. Each provider's row links to the page where you get a key and says what to switch on there, and the app checks a key when you save it.
Keys stay on your device. Maps from a key stream online only and are never saved in an offline download, so use one of the built-in maps for an area you need offline.
Map Styling
Map Styling tweaks in the Layers panel restyle any base map, including satellite and your downloaded maps: all roads, highways with route shields and names, train tracks, borders with names, national and state parks, power lines, contour lines with a choice of interval, water and rivers, trails, offroad and gravel, campgrounds and viewpoints, live OpenStreetMap peaks, fishing spots and boat launches, and business labels. They stay on when you switch base maps and work offline once the area is saved. Map Legend, beside the Map Styling title, explains every symbol.
Stacking maps and quick setups
Add Base Layer stacks a second base map or downloaded map over the first, each with its own opacity, for example USGS Topo contours over satellite. The Catalog (Customize) lets you choose which layers the menu offers, with Reset To Defaults one tap away.
One-tap presets set up a sensible base map and overlays for an activity: Hike, Snow / Ski (hillshade, slope angle and avalanche danger), SAR and Off-road. The same panel has a brightness slider and the night mode switch.
Imagery Catalog
The catalog holds over 1,300 worldwide sources from the OpenStreetMap Editor Layer Index: regional aerial photos, government topo maps, historical photos and maps, and elevation layers. Open Layers → Custom WMS / WMTS → Browse Imagery Catalog and filter by name, type, category or country. Add turns an entry into one of your own map sources, which then appears in the picker with the built-in layers.
To add any other map service, tap Add WMS/WMTS Source at the bottom of the Layers panel and enter its address. Your own WMS, WMTS and WFS services are free to use.
Overlays
Overlays draw on top of any base map. Add one from Layers → Add an overlay; each active overlay gets its own opacity slider, and you can run as many at once as you like. Overlays marked online-only need a connection and hide themselves while you are offline.
Terrain
- Hillshade (worldwide), Shaded relief (USGS, US only) and Elevation tint (USGS 3DEP).
- Slope Angle: terrain colored by steepness, computed on your device. The avalanche-oriented scale keeps gentle ground clear and heats up through the critical band: 27–29° yellow, 30–34° orange, 35–45° red, where most slab avalanches release, 46–50° purple, 51–59° blue, 60° and over gray.
- Slope Aspect: the compass direction each slope faces, in eight colors, for sun exposure, snow loading and avalanche aspect.
- Sun & shade: which slopes are in sun or shade for a time you choose.
- Custom terrain coloring: color the ground by elevation or slope bands you set.
Slope, aspect and the UTM / MGRS grid are calculated on your device from elevation data, so they work offline once the terrain for the area is saved.
Topographic & reference
- UTM / MGRS grid: a labeled reference grid, see Coordinate Display.
- Quad index (USGS 7.5′ quadrangles) and Reference labels for imagery.
- Country and state borders are always drawn and work offline.
Route discovery, water & marine
- Public Lands (BLM) and Ski pistes.
- OpenSeaMap seamarks, Hydrography (USGS NHD), Maritime limits / EEZ (NOAA) and Watersheds (USGS WBD).
Weather & hazards online only
- Wind barbs: standard meteorological barbs across the view, refreshed every 15 minutes. A half barb is 5 kt, a full barb 10, a pennant 50; the barbs point along the true wind and do not turn with the map.
- Weather radar (NEXRAD), refreshed about every 5 minutes.
- Smoke forecast near the surface (NOAA).
- Active wildfires (NIFC perimeters) and Satellite fire detections from the last 48 hours (VIIRS). Tap a perimeter for its name, size and start date.
- Avalanche danger: US forecast zones colored by the current danger rating, with a legend.
- Snow depth (NOAA NOHRSC).
United States field data
- Trails & recreation: trails on national forests (USFS), trails nationwide (USGS), the USFS Motor Vehicle Use Map and USFS recreation sites.
- Access & transport: USFS roads, and roads including 4WD (USGS).
- Land status: wilderness areas, tribal lands and reservations, hunt units on wildlife refuges.
- Hazards: FEMA flood zones and earthquakes from the last 24 hours (USGS).
- Ground cover: land cover (NLCD) and cropland (USDA).
- Survey & names: the PLSS survey grid (BLM), USGS place names, and wetlands (NWI).
3D Terrain View
Tap 3D in the Layers panel to tilt the map into real terrain. Any base layer and your map items drape over it. 3D terrain is part of ZYRNTOPO Pro; the 3D globe beside it is free.
- Drag with two fingers to tilt, twist to rotate; the tilt slider does the same.
- Raise the terrain exaggeration in Settings to make gentle ground easier to read.
- The viewshed tool and the slope overlays use the same elevation data.
3D without a connection
- Frame the area you need.
- Open Layers → Save Terrain and confirm the size estimate.
- From then on, 3D uses the saved terrain for that area, with no connection.
Vector Maps
Besides picture-tile maps, ZYRNTOPO shows vector maps, which stay sharp at every zoom and take up less space. The default VersaTiles Eclipse map is one, and it is ready to use with no import.
- Import any
.pmtilesfile, picture or vector, with Settings → Offline Maps → Import. Archives from Protomaps, OpenFreeMap and BBBike work, as does your own build. - An imported vector map gets a style made for it automatically from the layers it contains, and drapes over 3D terrain like any other map.
- Choose it in the base-layer picker under My Maps (offline).
Controls & Compass
- Zoom buttons (+/−) sit in the bottom-right corner.
- Download map area is in the top bar: tap it, frame the box and download. See Downloading an Area.
- Compass: tap to turn the map back to north-up; long-press for heading-up, where the map turns with you.
- Scale bar: metric and imperial together, so either reading is there at a glance.
- Long-press the map to hide the on-screen controls; long-press again to bring them back.
- Tools in the bottom bar holds every drawing, measuring and field tool, grouped by task.
Night Mode
The night button in the Layers panel cycles the map through Off → Dim → Red light, to protect your night vision without touching the controls. In Settings → Display you can also choose Auto, which follows your device's dark mode. Your choice is remembered.
Coordinate Display
Your position, or the map center when GPS is off, is shown at the bottom of the screen together with bearing and elevation. Choose the format in Settings → Coordinates → Coordinate Format:
| Format | Example |
|---|---|
| Decimal Degrees (DD) | 39.0835° N, 108.5601° W |
| Degrees Minutes Seconds (DMS) | 39°05'00" N, 108°33'36" W |
| Degrees Decimal Minutes (DDM) | 39° 05.010' N, 108° 33.606' W |
| UTM | 12S 741234 4328765 |
| MGRS | 12SVJ4123428765 |
Tap the coordinates to copy them. Use the format the people you work with use: mismatched formats are a classic way to send someone to the wrong place.
UTM / MGRS grid overlay
Add UTM / MGRS grid from Layers → Add an overlay → Topographic / Reference for a labeled reference grid over any map. The spacing follows the zoom (100 km, 10 km, 1 km), it has its own opacity slider, and it works offline.
Markers & Waypoints
Tap Tools → Marker, then tap the map where it goes. To mark exactly where you stand, use the drop-marker button beside the GPS button. The marker's details open straight away.
Marker properties
- Name and notes.
- Icon: dozens of typed symbols, the same on every device, including camp, water, summit, hazard, medical and wildlife. A marker with no icon of its own shows as a red dot.
- Color, folder, and photo: a photo waypoint keeps where it was taken and which way the camera faced.
- SAR properties: mark it as a clue or a find, and assign it to an operational period.
Tap a marker to open its details. In Items, every item has Go To, and point items also have Point Info, Directions, Navigate To and Transform. Tools → Photo waypoint takes a photo and pins it where you stand.
Averaged waypoint
For a more accurate point, use Tools → Averaged Survey Waypoint. Hold the device still; the dialog shows the number of fixes, the running average and an estimated accuracy that tightens as fixes accumulate. Save waypoint drops a marker at the averaged position with its accuracy noted.
Lines & Routes
Tap Tools → Line, then tap the map to place points. The running distance shows as you go; tap Finish when done.
- Each segment shows its length in your units.
- Edit a line to drag, insert or delete points, and to set its name, color and folder.
- Free Draw (Tools → Free Draw): drag on the map to sketch. Each stroke saves as a line you can rename, recolor or move later.
- Bearing Line (Tools → Bearing Line): a line from a point along a bearing you enter.
Elevation profile
Open any line, track or route and tap Elevation for its profile: total distance, gain, loss, lowest and highest point in your units. It needs a connection to fetch the elevation.
Polygons & Areas
Tools → Polygon: tap to place the corners and Finish to close it. Its area shows in acres, hectares, square kilometers or square miles.
- Highlight (Tools → Highlight): shade an area with a see-through fill to mark a search segment, a hazard or an area of interest without hiding the map.
- Circle (Tools → Circle): press at the center and drag out; the radius shows as you size it.
- Buffer (Tools → Buffer): an area a set distance around a point, line or area.
- Sector (Tools → Sector): a pie-wedge search area from a bearing and radius.
- Assignments (Tools → Assignment — Line or Assignment — Area): search assignments that join an operational period.
Range Rings
Tools → Range Ring draws concentric rings around a point. Set the number of rings (1 to 10), the spacing, or your own radius for each ring. The set is one item you can edit or delete from Items.
Folders & Organization
Everything you create is listed under Items in the bottom bar, in folders such as "Day 1 Search", "Water Sources" or "Hazards".
- Create a folder with a name and color, and move any item into it.
- Hide or show a whole folder on the map at once.
- Export one folder on its own.
- Select several items for bulk actions.
Clearing caches never deletes your items: items, downloaded maps and the tile cache are stored separately.
Operational Periods
An operational period is a planning cycle, usually one 12- or 24-hour shift. Create one with Tools → Search Period.
- Give it a name, a start time and a status.
- Assign items to it from their details.
- Hide the items of periods that are not active.
- Export one period on its own.
Saved Maps & Sharing
Menu → Save Map saves what you have drawn (items, folders and search periods) as a named map. Menu → Saved Maps lists them; open one to load it, and Clear Map starts a fresh one.
Share a map by QR code, with no signal
- In Saved Maps, tap Share on the map you want to send.
- The other person taps Scan in their own Saved Maps and points their camera at your screen.
- A small map is one code; a big one cycles through several. Hold steady until it says every part is captured; a missed part is picked up on the next pass.
Everything travels inside the codes, so it needs no internet, no account and no cable. Items, areas and folders come across; downloaded map tiles do not, as they are far too big for a code.
Files and map packages
A saved map can also be exported as a .zyrn file and opened on another device with Saved Maps → Import .zyrn.
To send a whole map in one file, use Settings → Data → Send map package. It writes one .zip in ATAK's data package format: the map itself, a KML copy that ATAK imports, and, if you choose, the downloaded maps that cover the area in view. ZYRNTOPO opens it as a saved map, and ATAK imports it like any other data package.
GPS Modes & Receivers
The GPS button cycles through three states:
- Off: no GPS; the button is gray.
- Follow: the map keeps you centered as you move.
- Heading-up: the map also turns to match the direction you are facing.
A circle around your position shows its accuracy. Under heavy canopy or in a canyon it grows large; check your position against landmarks before trusting it.
External GNSS receivers
A separate receiver, over USB or Bluetooth, can replace the phone's own GPS. Once connected it is the position source for everything: your position, track recording and what you share with your team. Disconnect it and the device GPS takes over again. Connect one from Settings → GPS Receiver.
Receivers are read over NMEA 0183, which nearly every GNSS unit speaks. Sentences that fail their checksum are discarded rather than guessed at.
| Connection | Where it works |
|---|---|
| USB serial | Android app; Chrome and Edge on a computer |
| Bluetooth LE | Android app; Chrome and Edge on a computer |
Track Recording
Tools → Record Track starts recording; your track draws on the map as you move, with live distance, elevation gain, elapsed time and average speed.
On Android the recording continues with the screen off, behind a notification, once you allow background location; on the web, keep the screen on. Tap Stop for a summary (distance, gain, duration and top speed). The track is saved with your items, ready to export as GPX or GeoJSON, with its elevation profile one tap away.
Measurement Tools
Tools → Measure, then tap points on the map:
- Distance between points, with a running total, and the bearing of the last leg.
- Bearing between two points, from true, magnetic and grid north at once, in your bearing units.
- Area: close the shape to get its area, in your area units.
- Elevation profile along the path.
- Navigate: tap the point you want to go to and guidance starts. See Navigate & Route.
- Circular scale: three range rings around the crosshair at round distances for the current zoom, with a compass rose on the inner ring that turns with the map. Tap it again to hide it.
- Viewshed / line of sight: tap an observer point and the ground visible from it is shaded green, computed on your device. Set the observer and target height, a maximum radius, and an optional radio mode that models how radio bends over the horizon, for siting repeaters, lookouts or signal mirrors.
Results stay on the map in a card while you keep working. Tap its × to clear it.
Field Shortcuts
- Bearing From Heading (Tools): a line from where you stand along the way you are facing, at a length you choose (500 m, 1 km, 0.5 mi, 1000 ft and so on).
- Bearing sight (Tools, compass icon): a full-screen compass showing the azimuth you are pointing at, magnetic or true, in degrees and mils, with the inclination angle. Shoot azimuth → map drops a bearing line from your position. It uses the device's compass and works offline.
- Tap to measure from GPS (Settings → Map Behavior): with it on, tapping open map draws a line from your position to that spot with the bearing and distance on it. The line follows you as you move; its label has Navigate and Clear.
- Share Field Location (Tools): share your position, or the map center if GPS is off, as a card with DD, DMS, UTM and MGRS coordinates and a map link.
Search & Point Info
Place search
Tap search and type a place name, an address or coordinates in any supported format (DD, DMS, DDM, UTM or MGRS). Results come from OpenStreetMap; tap one to go there. The points of interest saved on your device are searchable offline.
Point Info
Tools → Point Info, then tap the map. For that spot you get:
- Coordinates in your format, and the elevation (online).
- Weather: current conditions, temperature and wind (online).
- Sun: sunrise, sunset, twilight and daylight left, worked out on your device so it works offline.
- Moon: rise, set and phase, with a drawing of the actual phase.
How Offline Maps Work
A downloaded map is a single .pmtiles file on your device. One file per area is sturdier than thousands of cached pictures: the system does not clear it to make room, it survives restarts, and you can copy it to a teammate like any other file.
Nothing downloads behind your back: you choose the area and the zoom range. The default map is hosted and needs a connection until you save the area you care about. Do it before you leave service, not after.
What you can save
- Every base map except Google Satellite and Google Hybrid, which Google does not allow to be saved.
- The overlays you have on: they are saved with the base map for the same area and zooms.
- Your own map sources, if their servers allow it.
- 3D terrain, with Layers → Save Terrain.
- Slope, aspect and the UTM / MGRS grid, which need nothing extra: they are worked out from the saved terrain.
- Any
.pmtilesor.mbtilesfile you import;.mbtilesare converted on import.
The points-of-interest dataset is saved on your device after the first run, so POI search works offline from then on.
Using a downloaded map
A downloaded map does not switch itself on. Open the base-layer picker and choose it under My Maps (offline). To prove it works, turn off Wi-Fi and mobile data: the map should still draw, straight from the file.
Importing a file
- Open Settings → Offline Maps → Import, or Menu → Map Data → Import.
- Pick a
.pmtilesor.mbtilesfile. Files of many gigabytes are fine. - It appears in your list with its area and zoom range, ready under My Maps (offline).
Archives from Protomaps, OpenFreeMap and BBBike work, as does your own build. Map Data → From link downloads one from an address.
Downloading an Area
- Open Settings → Offline Maps → Select Area on Map, or tap the download button in the top bar. A green box appears.
- Drag the box over the area and pull its corners to size it.
- Choose the lowest and highest zoom. A live estimate shows the size before you commit.
- Tap Download Area. The download runs in the background and carries on after a restart if it is interrupted.
- The map lands in My Maps (offline).
Download a whole region
Menu → Map Data → Download region saves the current base map for a country, continent or mountain range in one go, with the same zoom choice and size estimate. The same screen lists ready-made map packages.
How big will it be?
| Area | Zoom range | Approximate size |
|---|---|---|
| One county (about 50 × 50 km) | 6–14 | about 120 MB |
| One county | 6–16 | about 600 MB |
| One state | 6–14 | about 1 GB |
| One state | 6–16 | about 5 GB |
Each extra zoom level roughly quadruples the size. Areas of sea, forest canopy or empty ground are stored once, so real downloads are often smaller. Download on Wi-Fi.
Viewed Tile Cache
While you are online, map tiles you look at are also kept in a temporary cache, so recently viewed places keep drawing when the signal drops and panning stays smooth. The system may clear this cache when storage runs low, which is why a downloaded map is the dependable way to work offline. Manage it in Settings → Viewed Tile Cache.
Storage
- Settings → Offline Maps lists every downloaded map with its size, area and zoom range. Each has Use and Remove; downloaded maps are usually by far the biggest thing the app stores.
- Clear Tile Cache (in the menu, or Settings → Viewed Tile Cache) drops only the temporary cache and leaves your downloaded maps alone.
Team Sync: Overview
Team Sync puts everyone on the same live map: positions, markers, lines, areas and tracks, plus chat. Open Menu → Team Sync. Team Sync and its links are Pro; the Team Members roster is free.
Pick your name, role and team color. The colors are the TAK team colors (Cyan, White, Yellow, Orange, Magenta, Red, Maroon, Purple, Dark Blue, Blue, Teal and Green), so you show up the same way to ATAK users. Teammates draw as contact balls with their callsign, and a teammate who reports a heading gets a direction cone. GPS switches on when you join, so your position goes out straight away.
| Link | Needs internet | Reaches |
|---|---|---|
| Session code | ✓ | ZYRNTOPO users anywhere |
| Nearby TAK | ✗ | ATAK, WinTAK and iTAK on the same Wi-Fi |
| TAK server | Depends on the server | Everyone on that server, in any TAK app |
| Meshtastic radio | ✗ | Radios on the same LoRa channel, for miles |
| MeshCore radio | ✗ | Radios on the same MeshCore channel, across many repeaters |
| MAVLink drone | ✗ | A drone or vehicle you connect |
The links run side by side: you can be in a session, on a TAK server and on a radio at once, and the app passes what it hears between them.
When a teammate or a TAK contact stops reporting, they stay on the map where they were last seen, faded, with their name and the time, for example Team 3 · last seen 14:05. They come back to normal as soon as they report again. Nothing disappears on its own; Clear in the Team Sync panel removes the ones that stopped reporting.
Team Sync: Over the Internet
- In Team Sync, set your name, role and team color.
- Create a session code, or type the one you were given. Codes are 10 characters of letters, digits and symbols.
- Share it with your team: show the QR code, copy it, or invite friends from your account.
- Tap Join Session. Everyone with the code sees the same map.
Team Sync: Nearby TAK Devices
On the same Wi-Fi as ATAK, WinTAK or iTAK users, the ZYRNTOPO Android app joins the local TAK network by itself. They appear on your map as TAK contacts with their callsigns, and you appear on theirs.
- Markers, lines, areas, circles and range rings go both ways. Markers made in ATAK, iTAK and WinTAK show up in ZYRNTOPO, and deleting one reaches TAK.
- Chat goes both ways too, including direct messages to one TAK user.
- No server and no internet are involved; it is the network the TAK apps already use among themselves.
Team Sync: Drones (MAVLink)
Drones and other vehicles that speak MAVLink (ArduPilot, PX4) appear on the map with their position, heading, altitude, armed state and battery. Open Team Sync → Drone tracking (MAVLink).
| Connection | How |
|---|---|
| Telemetry radio or flight controller over USB | Connect USB / Serial, with automatic port and baud detection (Android app; Chrome and Edge on a computer) |
| DroneBridge ESP32 | ws://192.168.4.1:5760 |
| mavproxy | --out=wsserver:0.0.0.0:5760, then ws://<computer>:5760 |
| Any MAVLink WebSocket | ws://<address>:<port> |
Set the flight controller's telemetry port to MAVLink at the same baud rate as your adapter (commonly 57600, 115200 or 921600). On Android, common USB serial adapters (FTDI, CP210x, CH340, PL2303 and CDC/ACM) work through USB-C or OTG.
Team Sync: TAK Servers
A TAK server links everyone connected to it, whatever app they use: ZYRNTOPO, ATAK, WinTAK or iTAK all see each other's positions, markers and chat.
Connecting
Team Sync → TAK server. It is set to the ZYRNTOPO server, but nothing is sent until you press Connect: joining a server shares your position, markers and chat with everyone on it. It never connects on its own, and does not reconnect by itself the next time you open the app.
To use another server, choose Custom TAK Server and enter its address.
Running your own
The ZYRN TAK server is free and keeps everything on hardware you control. It is one program for Linux, macOS, Windows and Raspberry Pi, with no Docker. One command installs it, creates the certificates and an administrator account, starts it as a service, and tests itself before saying it worked:
curl -fsSL https://zyrntopo.com/tak-server.sh | shOn Windows, in PowerShell:
irm https://zyrntopo.com/tak-server.ps1 | iexIt prints the dashboard address and the administrator password. In ZYRNTOPO, choose Custom TAK Server and enter ws://<server>:8090. Installing, connecting ATAK, WinTAK and iTAK, the dashboard and day-to-day running are covered under ZYRN TAK server.
zyrn-tak setup --tailscale) or behind a reverse proxy with TLS. The ZYRNTOPO web app needs a wss:// address, which either gives you. If a device cannot connect, run zyrn-tak status on the server.Team Sync: Meshtastic Radio
A Meshtastic radio carries positions and chat over LoRa, for miles and with no phone signal. Open Menu → Meshtastic radio and connect your radio over Bluetooth, USB or Wi-Fi (by its address, such as meshtastic.local).
- ZYRNTOPO users and ATAK users with the Meshtastic plugin on the same channel appear on your map.
- Anything the radio hears is passed on to your TAK server and to nearby TAK devices, so one radio can link the people out of signal with everyone else.
Team Sync: MeshCore Radio
MeshCore radios relay messages across many repeaters and keep messages for radios that are out of range for a while, which suits a search spread over a large area. ZYRNTOPO connects to a MeshCore companion radio through the MeshCore plugin: install it from Plugins → Get Plugins, open it, and connect over Bluetooth, USB or the network.
- Everyone on the team types the same channel name and passphrase. The plugin writes the channel into the radio; the passphrase stays on the radio and the phone.
- Positions, team chat, clues and markers travel on that channel. Teammates appear on the map, and every radio heard is listed with its signal strength and when it was last heard.
- Chat also goes out as plain channel text, so teammates using the MeshCore phone app can read it.
- Assignments travel too. When command sets an assignment to Assigned, the team's radio gets it, and the team's Acknowledge comes back. A radio delivery receipt is shown as delivered and never counts as acknowledged.
- The same plugin runs in ZYRNTOPO, ZYRNMAPS and ZYRN Workstation.
Team Sync: Without Internet
When there is no internet at all, three links still work:
- Nearby TAK devices on the same Wi-Fi network or hotspot, see above.
- Your own TAK server on a laptop or Raspberry Pi on the local network: everyone connects to its local address, and nothing leaves the network.
- Meshtastic radios where there is no network at all.
Team Sync: Sensor Detections
Search systems that report detections as Cursor on Target, such as a thermal drone, can send them to ZYRNTOPO through a TAK server or the local TAK network. Each detection appears as a colored marker with its scores, and is removed when its time runs out.
The color comes from the confidence score, against the thresholds the sender includes (by default 85% and 60%):
- Red: at or above the alert threshold. Check it now.
- Orange: between the two thresholds. Needs a look.
- Slate: below the review threshold. Logged, low confidence.
- Friendly: a friendly unit's position rather than a detection.
Color is taken from confidence, not from the CoT affiliation, so a missing person is never drawn as a hostile track.
Reviewing a detection
Tap a detection (right-click it in the TAK Server contact list in ZYRN Workstation) to see its picture, confidence, sensor and time, and choose:
- Accept: keep it. It gets a green ring and accepted by your name on every ZYRN device.
- Dismiss: hide it on this device only.
- Deny: remove it for the whole team.
A dismissed or denied detection stays gone if the sender reports it again. A drone operator can accept the likely ones, and command decides what to send a team to.
For detection systems
Send each detection as a CoT event with a non-friendly type such as a-u-G, a stable uid, and a <sord> block in the detail:
<sord schema="1">
<score type="composite" value="0.91"/>
<thresholds autoalert="0.85" review="0.6"/>
<source sensor="thermal" mode="survey"/>
<media type="image/jpeg" url="https://kit.local/crops/7.jpg"/>
</sord>The composite score is the confidence (0 to 1). Without a <sord> block, a remarks line such as Conf: 91% is read instead. The picture must be an https address or a small inline data:image/jpeg;base64,... image; plain http addresses are not shown. Review results come back as events of type t-x-zyrn-review placed at the detection, with remarks such as zyrn-review v1 uid=DET-7 verdict=denied by=IC, which a detection system can read to stop sending a denied track.
Team Chat
Chat works across every Team Sync link. Open Menu → Team Chat, or tap the chat bubble.
- Text to everyone in the session.
- Voice messages: hold the microphone button to record, release to send.
- TAK chat: on a TAK server or with nearby TAK devices, your messages land in ATAK's chat and theirs in yours.
- An unread badge shows on the chat button, and on Android a notification arrives while the app is in the background.
SAR: Operational Periods
Create a period with Tools → Search Period. Each period is a planning cycle in a search, usually 12 or 24 hours; assign items to it to keep the map organized.
- Name, start time and status.
- Show or hide a period's items with one switch.
- Export a period's items on their own for the incident record.
- Print a period from its print button in Map Items: either a packet with one page and map per assignment, or an ICS 204 assignment list, one table with each assignment's team, resource, radio, status and instructions. In ZYRN Workstation it is Search & Rescue → ICS 204. For an ICS 214 activity log, use the Unit Log plugin's export.
SAR: Assignments
An assignment moves through Draft, Prepared, Assigned, Acknowledged, In Progress and Completed. Command sets Assigned to (a team's callsign or team name) and the status in the assignment's editor.
- The assigned team sees Acknowledge at the top of the assignment's menu (in ZYRN Workstation, at the top of its properties), then Start and Mark complete. Pressing Acknowledge records who acknowledged and when, and every device shows it, for example Acknowledged by Team 3 at 14:05.
- Only a person pressing Acknowledge counts. A message delivered to a radio, or an assignment that simply arrived on a phone, stays Assigned, not acknowledged yet.
- Sending an assignment back to Assigned or earlier clears the old acknowledgement.
- Statuses travel with the assignment over Team Sync, TAK and the MeshCore plugin, between ZYRNTOPO, ZYRNMAPS and ZYRN Workstation.
SAR: Team Members & Roles
Menu → Team Members is a free roster, with no account needed. Each member has:
- Name, role and phone.
- Medical and emergency information: blood type, allergies and an emergency contact.
- Specializations and notes.
Rosters import and export as JSON or CSV for briefings and ICS forms. The roster stays on your device; it is separate from the people in a Team Sync session.
SAR: Gear / Pack List
Menu → Gear / Pack List keeps an inventory of field equipment:
- Item, category, quantity and weight, grouped by category (shelter, navigation, medical, comms and so on).
- Total pack weight.
- Export and import as JSON or CSV, to reuse a list from a previous operation.
SAR: Clue & Find Logging
Tools → Clue Marker drops a clue: a yellow marker that stands apart from ordinary markers. Like any marker it takes a name, notes, a photo and its operational period, so each find records what was found, where, and in which shift.
Clues keep all their details when exported as GeoJSON, and print on PDF map sheets with the rest of the map.
Import & Export: GeoJSON
GeoJSON keeps the most detail and is the best format for moving data in and out of ZYRNTOPO.
Export
Menu → Export Map, then pick the format. Export everything, one folder or one operational period. On Android the file goes to the place you choose or the share sheet; in the browser it downloads.
Import
Menu → Import Map and pick a GeoJSON, GPX, KML, KMZ or .zyrn file. Imported items merge into your map: items you already have are updated and new ones are added. In the web app you can also drag a file onto the window.
Import & Export: KML / KMZ
KML suits Google Earth, CalTopo and ATAK. Exported KML keeps points, lines and areas with their names and colors; KMZ files import too.
Import & Export: GPX
GPX is what handheld GPS units such as Garmin and Suunto expect. Markers export as waypoints and recorded tracks as tracks; importing reads waypoints, tracks and routes.
Import & Export: CSV
CSV writes markers with their coordinates and details as a spreadsheet, for ArcGIS, QGIS or agencies that work in spreadsheets. Team rosters and gear lists also import and export as CSV.
POI Data
Points of interest are the peaks, springs and water, trailheads, passes, caves, fishing spots, boat launches, parking, power lines, parks, helipads and airstrips, and SAR shelters, rescue points and lookouts the app shows and searches.
- Gather POI Data pulls the current OpenStreetMap points for the whole view and keeps them offline. Zoom out until the region you want fills the screen: one gather over a region beats a dozen small ones.
- Settings → Data → Export POI Data writes everything you hold to one file, and Import POI Data merges a file back in. Importing only adds; it never removes what you already have, and a damaged file is refused.
Print to PDF
Printing to PDF is part of ZYRNTOPO Pro. Menu → Print to PDF makes a map sheet you can print at true scale, the way a USGS quad is printed:
- A ground scale such as 1:24,000, a UTM / MGRS or latitude-longitude grid labeled in the margins, and a legend of what is on the map.
- Paper from Letter and A4 up to wall size: Legal, Tabloid, A3, A2, A1, A0, Arch C and Arch D, portrait or landscape.
- 150, 200 or 300 DPI. The box on the map shows exactly what will print, and the panel shows the scale and the ground it covers.
It uses the map tiles you already have, so it works offline. The result is a real PDF file: save it or share it from Android, or download it in the browser.
Settings: Coordinate Formats
Settings → Coordinates → Coordinate Format: DD, DMS, DDM, UTM or MGRS. The choice applies everywhere: the coordinate readout, Point Info, marker details and CSV export.
Settings: Units & Display
Settings opens on a list of pages: Display, Map Display, Coordinates, Map Behavior, Appearance, GPS Receiver, Track Recording, Map Sources, Offline Maps, Viewed Tile Cache, Data, Account and Version.
- Units (Coordinates): distance in imperial or metric, elevation in feet or meters.
- Area units (Coordinates): metric with ares or hectares, imperial with square yards and acres, or nautical. Match distance units keeps acres or km².
- Bearing units (Coordinates): degrees (0–360, ±180 or quadrants such as S 72° W), gradians, or mils (NATO 6400, exact 6283, Swedish 6300, Warsaw Pact 6000).
- Reference north (Coordinates): true, grid (UTM) or magnetic. Every bearing the app shows is measured from it and marked T, G or M.
- Mode (Display): Recreation or SAR, which shows the search and rescue tools.
- Glove mode (Display): bigger buttons for gloves, cold hands or a moving vehicle.
- Color-blind palette (Display): switches the slope and aspect colors to a color-blind-safe scale, with matching legends.
- Map Display: show or hide the coordinate readout, the weather widget, the scale bar, the on-map buttons and the Tools button.
- Map title: the name printed on PDF sheets, also editable at the top of the menu.
Your base layer, map position and zoom, and settings are saved on the device and restored every time you open the app. Settings → Version has a Location diagnostics check for when GPS will not lock.
Settings: Permissions
The app asks for each sensitive permission the first time a feature needs it, and you can say no. To change one later:
- Android: Settings → Apps → ZYRNTOPO → Permissions.
- Chrome or Edge: the site icon in the address bar → Permissions.
| Permission | Used for |
|---|---|
| Location (background only while recording a track) | Your position, tracks and route following, and sharing your position when you choose to |
| Camera | Scanning QR codes of shared maps |
| Microphone | Voice notes and voice messages in Team Chat |
| Bluetooth | Meshtastic radios and Bluetooth GNSS receivers |
| Notifications | The notice while a track records or Team Sync runs, and new chat and items |
| Nearby Wi-Fi traffic | Seeing ATAK, WinTAK and iTAK devices on the same network |
Platforms: Android
Android 7.0 or newer with Android System WebView 80 or newer, on phones and tablets of every kind, from Google Play or the Amazon Appstore.
- No hardware is required: a Wi-Fi-only Fire tablet with no GPS receiver still installs it, and the features that need missing hardware simply stay off.
- Track recording, Team Sync and chat keep working with the screen off, behind a notification.
- USB serial devices (GNSS receivers, drone telemetry radios) plug in through USB-C or an OTG adapter.
- The ATAK-style notification and nearby TAK devices work only in the Android app.
Platforms: Web / PWA
The web version runs in any modern browser at app.zyrntopo.com. Install it as an app for offline use:
- Chrome / Edge: the install icon in the address bar.
- Safari on iPhone and iPad: Share → Add to Home Screen.
- Firefox: Menu → Install as App, where offered.
Once installed, the app itself works offline, along with your downloaded maps.
ZYRNMAPS: the native Android client
ZYRNMAPS is ZYRNTOPO built as a native Android app, drawing the map with the GPU rather than inside a web view. It is a separate app from ZYRNTOPO, with its own store listing and package name (com.zyrntopo.maps), and both can be installed side by side. Your account and Pro work in both.
Requirements
| Item | Requirement |
|---|---|
| Android version | Android 7.0 or newer |
| Graphics | Vulkan 1.0.3 or newer |
| Package name | com.zyrntopo.maps |
ZYRNMAPS: feature reference
ZYRNMAPS covers the same ground as ZYRNTOPO. Where it differs, it says so here.
The map
- The built-in base layers, the imagery catalog, and any
.pmtilesmap you import, with Map Styling on any of them and a choice of contour interval. - Maps with your own key: add keys under Map provider keys in the basemap picker.
- 3D terrain, viewshed and line of sight, worked out on the device.
- The overlays: terrain, reference grids, weather and hazards, and the US field data.
- Map labels follow the device language, and night mode follows the device's dark mode.
Location and GPS
- GPS modes (off, follow and heading-up) and external GNSS receivers, as in ZYRNTOPO.
- Track recording continues with the screen off, behind a notification.
- Averaged waypoints and the bearing sight.
Items, drawing and measuring
- Markers, lines, areas and tracks in folders, with the full SAR properties.
- Distance, area, elevation profile, range rings, buffers, heading lines and bearings.
- Point Info, and identify: tap a map feature to see what it is.
Search and rescue
- Scent cone, routes, clue logging, search periods, photo waypoints, viewshed, gear list and team roster.
- True-scale PDF map sheets.
Offline
- Downloads with a size estimate before you commit, that resume if interrupted, and a list of what you already hold.
- Offline 3D terrain from imported elevation data.
- A downloaded area draws on a cold start in airplane mode.
Sharing and interchange
- Share a saved map by QR code: open the map's card, choose Show code, and the other person scans it from their own Saved Maps. No signal, account or cable needed.
- GeoJSON, GPX, KML/KMZ and CSV in and out, and data packages for moving a whole working set at once.
Team Sync and TAK
- Team Sync sessions, TAK servers and nearby TAK devices in both directions, and MAVLink drones.
- A Meshtastic radio over Bluetooth or Wi-Fi from Menu → Meshtastic radio, and over USB with the Meshtastic plugin.
- Nothing is shared until you press Connect.
Plugins and place data
- The same
.zyrnpluginplugins as every ZYRN app, plus native plugins built as Android apps. See ZYRNMAPS plugins. - The points-of-interest dataset downloads by itself in the background after you install, so open the app on Wi-Fi the first time; place search then works offline.
ZYRNMAPS plugins
ZYRNMAPS runs two kinds of plugin. The first is the same .zyrnplugin file every ZYRN app runs: open Menu → Plugins and use Get plugins, Install file or From a link, or open a .zyrnplugin from Files, Downloads or a chat app. Those work exactly as described under Plugins, including community plugins and Report.
The second is a native plugin: an ordinary Android app that extends ZYRNMAPS. It ships as its own APK, installs the way any app does, and can add layers, panels, map objects, search results and context-menu actions. Building one is covered in Native ZYRNMAPS plugins below; the rest of this section is about running them.
Installing a native plugin
- Install the plugin's APK from a store, with
adb install, or by opening the file on the device. - Open ZYRNMAPS ▸ Menu ▸ Plugins. The plugin appears in the list.
- Switch it on. Nothing runs until you do: a discovered plugin is off until a person enables it, with no allow-list that pre-approves anything.
What the Plugins screen tells you
The list shows every plugin found on the device, including ones ZYRNMAPS has decided not to run, each with its reason. That is deliberate: a plugin that is switched off and a plugin that is missing are different problems, and a screen that showed nothing for both would send you looking in the wrong place.
| What it says | What it means | What to do |
|---|---|---|
| Not enabled | Found, allowed, and waiting for you | Switch it on |
| Needs a newer ZYRNMAPS | Built against a host API this version does not have | Update ZYRNMAPS |
| Built for an older ZYRNMAPS | Built against a host API no longer supported | Update the plugin |
| No entry point | Its manifest names no plugin class, so there is nothing to load | Report it to whoever built it |
| Could not be loaded | The named class is missing, or is not a ZYRNTOPO plugin | Report it, quoting the reason shown |
| Requires Pro | The plugin declares itself Pro and this account has no entitlement | Unlock Pro, or remove the plugin |
Who signed it
Every row shows where the plugin came from, and ZYRNMAPS records the SHA-256 of its signing certificate either way.
| Level | Meaning |
|---|---|
| First-party | Signed with the same certificate as ZYRNMAPS itself: shipped by us, or built by somebody holding our key |
| Third-party | Signed by somebody else. The fingerprint is shown, so you can check it against what the author published |
| Unknown | The signature could not be read at all. Rare, and it means the package manager refused, not that the APK is suspect |
Turning one off
Switch it off in the same screen. Everything the plugin added (layers, sources, map objects, panels, its search provider, tap handlers and subscriptions) is removed by the host, whether or not the plugin cleaned up after itself, and whether or not its own teardown threw. Objects it drew are namespaced to it, so they go and nothing you drew goes with them.
Uninstalling the APK does the same, and also sweeps any objects it left behind.
ZYRN Workstation
ZYRN Workstation is the desktop app for Windows, macOS and Linux. It has its own 3D globe with ZYRNTOPO's maps, terrain analysis and GIS tools, and the same Team Sync and TAK links as the phone app. It signs in to the same account, uses the same Pro license and syncs the same map items, so a plan made at the desk is on the phones in the field.
Installing
Windows, in PowerShell:
irm https://zyrntopo.com/zyrngis.ps1 | iexmacOS or Linux, in a terminal:
curl -fsSL https://zyrntopo.com/zyrngis.sh | shThe command picks the right package for the computer, checks it against its published SHA-256 and stops on any mismatch. On Windows it installs for the current user; to install for every user on the machine with the .msi, for Group Policy or Intune, run $env:ZYRNGIS_METHOD='msi' first. On Linux it uses a .deb, .rpm or pacman package where it can and an AppImage in your home folder everywhere else. On a Mac the app goes to Applications, and Apple silicon Macs get Rosetta 2 if they don't have it. On Linux, ZYRN Workstation is also in the Snap Store; that copy runs sandboxed, so it can't use USB GPS receivers, radios or the system's location service.
It needs Windows 10 or 11 (64-bit), macOS 13.3 or newer, or 64-bit Linux with glibc 2.35 or newer (Ubuntu 22.04, Debian 12, Fedora 36, Linux Mint 21, Arch and later), and a graphics driver with OpenGL 4.1.
Updates
The Windows installer copy and the Linux AppImage update themselves: when a new version is out, ZYRN Workstation downloads it, checks it against the published checksums and installs it when you close the app. Other installs tell you a new version is available. Update those by running the install command again, or with zyrngis update on macOS and Linux. zyrngis version shows the installed and published versions, and zyrngis remove uninstalls it and keeps your projects and settings. The Snap Store updates its copy by itself.
The Workstation window
The map fills the middle. Contents on the right lists the basemap, the layers you added and Map Styling; task panes such as Point Info, Viewshed, Plugins and the TAK server open beside it; and the status bar at the bottom always shows the cursor's position in latitude and longitude, UTM and MGRS. The ribbon across the top groups the tools by job:
| Tab | What is on it |
|---|---|
| Map | Zoom, north up, the 3D view, bookmarks, basemaps, adding data, the imagery catalog, select, identify, Point Info and coordinates |
| Search | Go to a coordinate in any format, and place and address search |
| GPS & Team | A GPS receiver over USB or serial, replaying a recorded log, Team Sync, the TAK server and team chat |
| Server | Signing in to a ZYRN TAK server and running it from here, see Team, TAK and the Server tab |
| UAS | Flying a MAVLink drone: telemetry, flight commands, routes and what its camera sees |
| Draw | Markers, routes, lines, areas and shapes with their style, editing, bearing lines, and field tools: set your location, record a track, photo and averaged waypoints |
| Range & Bearing | Measuring distance, area and bearing, and the elevation profile of a line |
| Search & Rescue | Assignment lines and areas, clues, the operational period, a scent cone and the wind plot |
| Analysis | Viewshed and line of sight, terrain tools such as slope and hillshade, geoprocessing and density |
| Data | Adding files, links and map services (XYZ, WMS, WFS), the attribute table and symbology, selection, export and printing, offline areas and archives, 3D Tiles |
| Plugins | Your plugins' buttons, Get Plugins, Install and New Plugin, see Plugins |
| View | Which panes are open, the sky, the lat/lon and UTM grids, a colour-blind palette, night mode, the map legend and resetting the layout |
Projects (.zyrnproj) save the map with its layers and styling, and print layouts make map sheets as PDF; both are under Data → Export and in the ≡ menu at the left end of the ribbon. Your account and Pro status sit at the top right.
Workstation offline maps
Data → Download Area saves a basemap for an area so it works with no connection. Draw the area, press Use Current View, or pick one of the ready-made regions, then choose the basemap and zoom range. Two extra boxes add the elevation the 3D view, slope and profiles need, and address points for offline address search. Save Offline Maps In sets the folder. A canceled download keeps nothing.
The same group opens map and terrain archives you already have (.pmtiles), and adds OGC 3D Tiles: a tileset.json by address or from disk, or a Cesium ion asset.
Workstation GIS data and tools
Add Data opens GeoJSON, KML and KMZ, GPX, Shapefiles, GeoPackages, CSV and the other common vector formats, and reprojects them to the map. Each layer gets a legend in Contents, an attribute table, and symbology: a single symbol, unique values, or graduated colours in classes by natural breaks, quantile, equal interval or standard deviation. You can select features by attributes or by location, calculate fields, and export a layer to another format.
Geoprocessing holds 41 tools in five groups:
- Overlay: buffer, clip, erase, intersect, union, dissolve, merge, spatial join, count points in areas and near.
- Geometry: convex and concave hulls, centroids, bounding boxes, vertices to points, polygons to lines and back, multipart to singlepart, simplify, densify, repair, and geometry attributes.
- Create: grids, range rings, Delaunay triangles and Thiessen polygons.
- Terrain: contours, hillshade, slope, aspect, ruggedness, topographic position, curvature, elevation tint, elevation statistics and cut and fill.
- Density and selection: kernel density, IDW interpolation and summary statistics.
Tools run on the layers you have open and add their result as a new layer, so nothing you had is changed.
Workstation Team, TAK and the Server tab
GPS & Team connects a GPS receiver over USB or a serial port, or replays a recorded NMEA log. Team Sync works as it does on the phone, with the same session codes, and the TAK Server pane connects to a TAK server so ATAK, WinTAK and iTAK users share the map with you. Team Sync and TAK need Pro, as they do everywhere.
The Server tab signs you in to a ZYRN TAK server, the ZYRNTOPO one or your own, and then does what the server's web dashboard does without a browser: every dashboard page opens in the Server pane, and the dashboard's map is the Workstation globe itself. Two-step sign-in works. Signing in also puts your TAK link under your account, so your position, markers, drawings and chat reach the server as you, in your groups. The Workstation keeps a personal access token instead of your password; it lasts 30 days, renews itself while you use it, and signing out revokes it on the server.
The UAS tab flies an ArduPilot or PX4 aircraft over UDP, TCP or a telemetry radio: arm and take off, go to or circle a point you click, return to launch and land, send it a route, and see what its camera covers on the map, shared with TAK.
ZYRN TAK server
The ZYRN TAK server is a free TAK server in a single program for Linux, macOS, Windows and Raspberry Pi, with nothing else to install: no Docker, no database, no Java. ZYRNTOPO, ZYRNMAPS, ZYRN Workstation, ATAK, WinTAK, iTAK and TAK Aware all connect to it, and positions, chat, markers, data packages, missions, video and voice stay on hardware you control. Its download page is TAK server.
Installing
Linux, Raspberry Pi or macOS:
curl -fsSL https://zyrntopo.com/tak-server.sh | shWindows, in PowerShell:
irm https://zyrntopo.com/tak-server.ps1 | iexThe installer downloads the program for your system, checks its SHA-256 and runs zyrn-tak setup. Setup creates a certificate authority, a server certificate and an administrator account, installs a service that starts at boot and restarts after a failure, opens the ports in the firewall, and sends a test position through every port before it says it is ready. It prints the dashboard address and the administrator password, which also stays in admin-password.txt in the data folder until you change it. Running the command again is safe: that is how you update or repair it, and users, certificates and settings are kept.
Setup takes options after sh -s --, or in ZYRN_TAK_ARGS on Windows. The useful ones: --host sets the address devices use, --name the server name clients show, --tailscale publishes it on your tailnet as a wss:// address (which also works from the ZYRNTOPO web app), --zerotier NETWORK_ID joins a ZeroTier network, and --no-anonymous requires a sign-in on every port.
It needs a computer that stays on: a Raspberry Pi 2 or newer, a spare laptop, a desktop or a small VPS, running Linux, macOS 11 or later, or Windows 10 or 11, with administrator rights for the install. On a VPS, also open TCP 8089, 8090, 8443 and 8446 in the provider's firewall.
Connecting devices to your TAK server
| App | How |
|---|---|
| ZYRNTOPO and ZYRNMAPS | Team Sync → TAK server → Custom TAK Server, address ws://<server>:8090 |
| ZYRN Workstation | The TAK Server pane, or sign in on the Server tab |
| ATAK and WinTAK | Dashboard → Connect a device → the enrollment QR code, or a connection package |
| iTAK | Dashboard → Connect a device → the iTAK QR code, then sign in |
| Any TAK client | Open https://<server>:8446/package on the device, sign in and open the downloaded zip |
On the server itself, zyrn-tak qr NAME prints the same QR codes in the terminal and zyrn-tak user package NAME saves a connection package to copy to a device.
wss:// address, which either gives you.The TAK server dashboard
Open https://<server>:8446 and sign in as the administrator. The dashboard works on phones and desktops, follows the system's light or dark theme, comes in English, Spanish, French, German, Portuguese and Ukrainian, and Ctrl+K (or /) jumps to any page or setting.
- Overview shows connected devices, the addresses to connect to, the server's state and emergencies. Map shows live positions with their symbols and headings, routes, drawings, CasEvac reports and the last hour of tracks.
- Chat reaches every TAK client and ZYRNTOPO, and Calls makes voice and video calls between dashboard users.
- Connect a device has the QR codes, packages, single-use download links and manual settings. Users and Groups manage accounts, roles, channels, certificates and two-step sign-in.
- Files and Missions hold data packages and Data Sync. Video and Voice run the built-in video server and the Mumble voice server. Data feeds brings in ADS-B, AIS, Traccar, Meshtastic and CoT.
- Server links, Federation and Cluster join this server to others. Settings has every setting, and Updates and backup updates the server, backs it up and renews certificates.
Running your TAK server
The zyrn-tak command manages it. On Linux and macOS put sudo in front of commands that change something; on Windows run them in an Administrator PowerShell.
| Command | What it does |
|---|---|
zyrn-tak status | Service state, addresses, connected clients and a self-test |
zyrn-tak user add NAME | Adds a person (--admin, --groups, --callsign as needed) |
zyrn-tak user remove NAME | Deletes the user and revokes their certificates |
zyrn-tak logs -f | Follows the log |
zyrn-tak backup | Writes a backup of the data folder |
zyrn-tak update | Updates to the current release |
zyrn-tak uninstall | Removes the service and keeps the data (--purge deletes it) |
zyrn-tak help lists every command. zyrn-tak import-ca moves an existing TAK server's certificate authority over, so devices enrolled against the old server keep working.
The data folder is /var/lib/zyrn-tak on Linux, /Library/Application Support/ZYRN TAK on macOS and C:\ProgramData\ZYRN TAK on Windows. Back it up: certs/ca.key in it signs every device certificate.
| Port | Used by |
|---|---|
8089 | ATAK, WinTAK and iTAK, over TLS |
8090 | ZYRNTOPO and ZYRNMAPS (CoT over WebSocket) |
8443 | Data Sync, files, missions and the update server for TAK apps |
8446 | The dashboard, enrollment and package downloads |
New installs also open the video, voice and call-relay ports; everything else, such as federation and unencrypted CoT, stays off until you turn it on under Settings → Ports.
Linking TAK servers and server plugins
zyrn-tak peer invite NAME on one server prints a link code, and zyrn-tak peer join CODE on the other links them, encrypted and trusted both ways. Other TAK software links by address: tls://host:8089 for TAK Server, OpenTAKServer or FreeTAKServer, tcp://, udp://, ws:// or wss://, and fed://host:9000 or fed2://host:9001 for TAK Server federation.
A server plugin is any program the server starts, keeps running and stops with it. It gets an API token and the server's addresses in environment variables (ZYRN_TAK_URL, ZYRN_TAK_TOKEN, ZYRN_TAK_STREAM_URL and others), and a plugin with web pages shows up on the dashboard. Install one with zyrn-tak plugin install FOLDER|ZIP|URL. These run on the server and are a different thing from the app plugins below.
Plugins
Plugins add tools to the ZYRN apps: buttons and panes, map layers, calculators and field forms, and links to radios, drones and other systems. One plugin file runs unchanged in ZYRNTOPO on the web and Android, in ZYRNMAPS and in ZYRN Workstation, because every app runs plugins in the same small JavaScript engine with the same API.
| App | Where plugins live | Getting one |
|---|---|---|
| ZYRNTOPO | Plugins on the toolbar, or in Tools on a phone | Get plugins in the Plugins panel, or Import and From a link |
| ZYRNMAPS | Menu → Plugins | Get plugins, Install file, From a link, or open a .zyrnplugin from Files, Downloads or a chat app |
| ZYRN Workstation | The Plugins ribbon tab and pane | Get Plugins…, Install…, or double-click or drop a .zyrnplugin |
Get plugins lists the official plugins and the community plugins that passed the automatic check, with one-tap Install. A plugin installed from the list keeps itself up to date: the app checks the list shortly after it starts and every few hours after that. The Plugins page shows the same list.
A plugin can always add its own buttons, panes and map layers. The first time it asks for anything more (the internet, TAK messages, your position, files, your map items, or radios and other devices), the app asks you once whether to allow it, and says which servers it wants to reach. Turn a plugin off or remove it in the same place you installed it; everything it added goes with it.
Community plugins are marked as such, with their author, their licence and a Report button. Report one that is unsafe, misleading or broken; one that several people report is taken down and turns itself off on every device that has it. See Community plugins.
Community plugins
Anyone can publish a plugin to every ZYRN app. Sign in to your ZYRN account and submit the .zyrnplugin file under Submit a plugin on the Plugins page. It can be open source or closed source: you choose the licence when you submit, and nobody can download your code from the website. People get it from the Plugins section of their app.
A scanner on the ZYRN server checks each upload, usually within a minute. It reads the package and the code, then starts the plugin in the same engine the apps use, with nothing real behind it, and watches what it tries to do. The report appears under Your plugins on the same page and arrives by email. A plugin that passes is published at once, with nobody at ZYRNTOPO having to approve it. One that fails gets a report that says what to change: fix it, raise version in plugin.json and submit again.
The scan refuses:
- code that builds more code from text while it runs, such as
evalornew Function, and the tricks that reach them; - native programs, and hidden or scrambled code;
- a name or
idthat copies an official plugin or someone else's; - servers that exist to collect data or pass messages on, such as webhook services and chat bots.
WebAssembly and source files may be in the package, and the report notes them. Every new version goes through the same scan, and apps update to it on their own.
When three people with verified accounts report a plugin as unsafe, misleading or inappropriate, it is taken down: it leaves the list and every installed copy turns off. The author can bring it back with a fixed, higher version, unless it has been taken down three times, which needs a review. An author can also withdraw a plugin from Your plugins; it leaves the list, and copies people already have keep working without updates. The rules are in the Terms of Use.
Writing a plugin
A plugin is a folder with a plugin.json and an ES module. ZYRN Workstation makes a working one for you: Plugins → New Plugin… creates it, starts it and opens its folder, and the folder includes zyrn.d.ts and jsconfig.json so zyrn. autocompletes in VS Code. The plugin SDK has the same files and a template to copy.
weather/
plugin.json
main.js
lib/units.js imported from main.js as './lib/units.js'
icon.svg optional{
"id": "com.example.weather",
"name": "Weather at a Point",
"version": "1.0.0",
"author": "Example",
"description": "Pick a point and read the weather there.",
"main": "main.js",
"icon": "icon.svg",
"apiLevel": 4,
"permissions": ["network"],
"hosts": ["api.open-meteo.com"]
}| Field | What it does |
|---|---|
id | Required and unique. Lower case a-z 0-9 . - _, usually your reverse domain, such as com.example.weather. |
name | Required. What the Plugins screens show. |
version | Yours to set. A submitted update must be higher than the last one. |
main | The module to load. main.js if left out. |
icon | An .svg or .png in the folder, or a Material Symbols name such as air. |
apiLevel | The API level you need. This page describes level 4, and a plugin written for an earlier level keeps working in later apps. |
permissions | Any of network, cot, location, files, items, devices. |
hosts | The servers network may reach (api.example.com, *.example.com). Listing hosts implies network, and users see the list when they allow the plugin. |
The app calls activate(zyrn) when the plugin starts and deactivate() when it stops. Modern JavaScript works, including async/await, classes and modules.
export function activate(zyrn) {
const pane = zyrn.ui.pane({ title: 'Weather', icon: 'air' });
pane.label('wind', 'Wind', '-');
pane.button('pick', { label: 'Pick a point', primary: true, onClick: async () => {
const at = await zyrn.map.pick('Choose where you want the weather');
const r = await zyrn.net.fetch(`https://api.open-meteo.com/v1/forecast?latitude=${at.lat}&longitude=${at.lon}¤t=wind_speed_10m`);
pane.set('wind', `${r.json().current.wind_speed_10m} km/h`);
} });
zyrn.ui.button({ label: 'Weather', icon: 'air', onClick: () => pane.show() });
}
export function deactivate() {}When a plugin stops, the app removes everything it added: buttons, panes, menu entries, map layers, timers, listeners, sockets and requests. Imports are relative and must stay inside the plugin's folder; there is no npm, so bundle dependencies into the folder or ship a bundler's output as main.js. A call that breaks a rule, such as a host that is not in hosts, throws an Error that says what was wrong.
Trying it while you work
In ZYRN Workstation, edit the plugin in its folder (Open Folder in the Plugins pane) and it reloads within half a second of each save; Log shows console.log output and every error with its file and line. In ZYRNTOPO, Folder in the Plugins panel (Chrome and Edge) loads a folder from your computer and reloads it on save. For ZYRNMAPS, push the folder to the phone and each push reloads it:
adb push my-plugin /sdcard/Android/data/com.zyrntopo.maps/files/plugins/To share a plugin, zip its folder and rename the zip to name.zyrnplugin. The file installs in any of the apps, and installing a newer copy with the same id replaces the old one. To list it for everyone, submit it.
Limits
One call into a plugin, such as a click handler, a timer or activate(), may run for 5 seconds before the app stops the plugin and says why, so split long work with await or setTimeout. Each plugin may use 256 MB of memory. Plugins can't read or write files except through zyrn.files, can't start programs, and can't reach the network without network. A plugin that throws is not stopped; the error goes to its log.
ZYRN Workstation and ZYRNMAPS make network requests themselves, so a plugin can reach any server in its hosts, including plain http:// and ws:// addresses of a radio, a drone or another device on the local network. ZYRNTOPO runs plugins inside its web page, so net.fetch follows the browser's rules there: the server has to allow cross-origin requests (CORS), and a page on https can only reach https:// and wss:// addresses.
Plugin API reference
Everything is on the zyrn object handed to activate. zyrn.plugin.app says which app the plugin is in (ZYRNTOPO, ZYRNMAPS or ZYRNGIS for ZYRN Workstation) and zyrn.plugin.platform where (windows, macos, linux, web or android). Level 3 added pane.image, binary net.fetch, ui.openUrl and ui.playVideo; level 4 added zyrn.device, cot.inject, cot.outgoing, cot.link and zyrn.self().
zyrn.ui
| Call | What it does |
|---|---|
button({ label, icon?, tooltip?, onClick }) | A button for the plugin: on the Plugins tab in ZYRN Workstation, at the top of the Plugins panel in ZYRNTOPO, under Plugin tools in ZYRNMAPS. Returns { setEnabled, setLabel, setTooltip, setChecked, remove }. |
pane({ title, icon? }) | A pane built from rows (below): a task pane in ZYRN Workstation, a page in the Plugins panel in ZYRNTOPO, the whole Plugins screen in ZYRNMAPS. |
menu({ label, icon?, onSelect }) | An entry in the map's right-click or long-press menu. onSelect({ lat, lon }) gets the point. Returns { off() }. |
notify(text) | A short message. |
alert(title, text) | A notification. |
confirm(title, text) | A yes/no question. Promise<boolean>. |
prompt(title, text, value?) | Asks for a line of text. Promise<string | null>. |
openUrl(url) | Opens an http or https address in the browser. |
playVideo(url, { title? }) | Plays an http, https or rtsp stream. Needs network and the host in hosts. |
A pane is built with chained calls, and every row with an id can be read and changed later:
const pane = zyrn.ui.pane({ title: 'Search' });
pane.heading('Area')
.hint('Choose a spacing and press Run.')
.number('spacing', { label: 'Spacing', value: 50, min: 5, suffix: ' m', onChange: (v) => {} })
.select('mode', { label: 'Mode', options: ['grid', { value: 'line', label: 'Line search' }], value: 'grid' })
.text('note', { label: 'Note', placeholder: 'optional', lines: 3, onSubmit: (t) => {} })
.check('wide', { label: 'Wide', value: false, onChange: (on) => {} })
.label('result', 'Result', '-')
.progress('done', { label: 'Done', value: 0 })
.image('photo', { label: 'Camera', height: 160, placeholder: 'loading' })
.list('hits', { items: [{ label: 'One', detail: 'first', value: 1 }], onSelect: (value) => {} })
.separator()
.button('run', { label: 'Run', primary: true, onClick: () => {} });
pane.set('result', 'done').set('done', 0.5);
pane.get('spacing');
pane.enable('run', false);
pane.visible('note', false);
pane.show();Icons are Material Symbols names (extension, air, groups, my_location, route, flag, search and so on) or a file in the plugin's folder.
zyrn.map
| Call | What it does |
|---|---|
points(layer, [{ lat, lon, name?, description?, colour?, icon?, course? }]) | Draws a live point layer named after the plugin and layer. Calling again replaces it. |
lines(layer, [{ name?, points: [[lat, lon], …] }], { colour?, width? }) | Draws a live line layer. |
geojson(layer, data, { colour?, width?, fillOpacity?, radius? }) | Draws GeoJSON points, lines and polygons. A feature's name, description and colour properties are used. Returns how many shapes were drawn. |
tiles(layer, { url, maxZoom?, opacity? }) | Adds an XYZ tile layer from an https://…/{z}/{x}/{y}.png template. Needs network. |
clear(layer) | Removes the layer's points, lines, shapes and tiles. |
goTo(lat, lon, zoom?) | Moves the map. zoom is a web-map zoom level: 0 is the whole world, 15 a street. |
view() | { lat, lon, zoom, bearing, pitch, bounds: { north, south, east, west } } |
on('click' | 'longpress', ({ lat, lon }) => …) | A click or tap, or a long press or right-click. Returns { off() }. |
on('moveend', (view) => …) | The map stopped moving. |
pick(prompt?) | Lets the user choose a point. Promise<{ lat, lon }>. On a phone the panel moves aside while they choose, so write prompts like "Choose where the marker goes". |
elevation([[lat, lon], …]) | Ground height in metres, null where there is none. Promise<number[]>. |
addPoint(lat, lon, name) | A permanent point in the user's map items. |
addGeoJSON(name, geojson) | Adds GeoJSON as an ordinary layer. |
metric() | Whether the user works in metric units. |
zyrn.items items
The user's own markers, lines, areas and circles: the same items every ZYRN app syncs with their account, so what a plugin adds also appears on their other devices.
| Call | What it does |
|---|---|
list() | Every item: { id, kind, type, name, notes, colour, symbol?, lat?, lon?, radius?, points?, updatedAt }, where kind is point, line, area or circle. |
get(id) | One item, or null. |
add({ kind, name?, notes?, colour?, symbol?, lat?, lon?, radius?, points? }) | Adds an item and returns its id. Circles need radius in metres, lines two or more points, areas three or more. |
update(id, patch) | Changes the fields given. |
remove(id) | Deletes an item. |
on(change => …) | { change: 'add' | 'update' | 'remove', id, item } whenever any item changes, whoever changed it. |
zyrn.cot cot
Cursor on Target, the traffic ATAK, WinTAK and iTAK exchange, from TAK servers, nearby TAK devices and radios.
| Call | What it does |
|---|---|
on(message => …) | Every CoT message the app receives: { xml, via, kind, uid, type, callsign, lat, lon, hae, how, remarks, stale, course } for map items, { kind: 'chat', from, room, text } for chat, { kind: 'delete', uid } for deletions. The raw xml is always there, which is how a plugin reads what an ATAK plugin sends. |
send(xml) | Sends CoT on every open TAK connection. true if one was open. |
marker({ uid?, callsign, lat, lon, type?, remarks?, staleSeconds? }) | Builds a marker's CoT XML, ready for send. |
connected() | Whether any TAK connection is open. |
inject(xml, { via?, relay? }) | Hands CoT to the app as if it had arrived over via: the map, chat, alerts and other plugins see it, and it is passed on to the TAK network unless relay is false. |
outgoing(traffic => …) | The user's own position reports and team chat as the app sends them. |
link({ name?, connected, detail? }) | Declares the plugin a TAK link, as a radio plugin does: while it is connected, chat and positions go out over it even with no server. |
zyrn.self() returns { uid, callsign, team, role, lat?, lon? }, the user as the TAK network knows them.
zyrn.device devices
Radios and other hardware, as raw bytes in base64. kinds() says what the app can reach: serial, tcp and, where the platform allows, ble. serialPorts() lists serial and USB devices, bleScan({ service?, timeout? }) finds Bluetooth LE devices, and open({ kind, id?, baud?, host?, port?, service? }) opens one and returns a channel with write, close and on('open' | 'data' | 'close' | 'error', fn). Bluetooth channels also have read(characteristic) and subscribe(characteristic), and write takes the characteristic.
zyrn.net network
fetch(url, { method?, headers?, body?, timeout?, binary? }) returns Promise<{ status, ok, url, headers, text, base64?, json() }>. A body object is sent as JSON, and binary: true returns the body as base64, ready for an image row. socket(url, { protocols? }) opens a WebSocket and returns { send(text), close(), on('open' | 'message' | 'close' | 'error', fn) }. Only the hosts in hosts, when it is set.
zyrn.storage, zyrn.files and zyrn.location
zyrn.storage keeps small settings as JSON across restarts: get(key, fallback), set(key, value), remove(key), keys().
zyrn.files reads and writes the plugin's own data folder: read(name), write(name, text), list(). With the files permission, open({ title?, filter? }) lets the user choose a file to read and save({ title?, name?, filter?, text }) lets them choose where to save one.
zyrn.location (permission location): current() returns { lat, lon, altitude?, accuracy?, heading?, speed?, source } or null, and watch(position => …) calls back whenever it changes.
zyrn.bus: plugins working together
zyrn.bus.provide('weather.windAt', async (lat, lon) => ({ speed: 4.2, from: 270 }));
const wind = await zyrn.bus.call('weather.windAt', 43.7, -110.8);
zyrn.bus.on('search.segment-done', (data, meta) => console.log(meta.from, data));
zyrn.bus.emit('search.segment-done', { id: 'A3' });provide offers a named service any plugin can call, and emit and on broadcast events. Data is copied as JSON. Name services and topics yourplugin.thing so they don't collide. The official plugins use the zyrn.log topic: emit { text, lat?, lon? } on it and Unit Log records it with a time and place.
Plugins also get console.log, setTimeout, setInterval, Promise, JSON, Math, Date and basic Intl, and zyrn.plugin holds { id, name, version, dir, dataDir, apiLevel, app, appVersion, platform, permissions }.
Plugins in other languages
The apps only run JavaScript, but you don't have to write it. The plugin SDK turns a plugin written in Python, Java, Go, C or C++ into the same .zyrnplugin package, so it installs, runs and goes through the scan like any other. Copy the template for your language, change id, name and description in its plugin.json, and build it with Node.js 22 or newer:
node build.mjs path/to/your-pluginThe package lands in dist/ inside the plugin's folder.
- Python goes through Transcrypt (
pip install transcrypt==3.9.5), which runs on Python 3.13 or older. Callzyrnthe same way as in JavaScript. - Java goes through TeaVM. You need JDK 21 or newer; the template's Gradle wrapper fetches the rest.
zyrn.Zyrnhas the common calls andcallfor any other. - Go goes through TinyGo 0.43 and then
wasm2jsfrom Binaryen 133 or newer. - C and C++ go through the clang in wasi-sdk 34 or newer and then
wasm2js. You writezyrn_mainand call the app withzyrn_call(path, json); C++ gets a wrapper withstd::stringandstd::function.
The SDK's README says where each tool goes and how each language calls zyrn. Compiled languages carry their runtime, so their packages are bigger (a Go plugin starts near 700 kB, C and C++ near 30 kB), and the apps' engine has no JIT, so heavy number crunching runs slower than in the language's own runtime. Plugins that mostly react to the user and call the app don't notice. The scan checks the JavaScript the build produced; source files you leave in the package are checked for passwords and keys and then left out.
Native ZYRNMAPS plugins
Most plugins should be .zyrnplugin plugins, which run in every ZYRN app. The native SDK is for ZYRNMAPS only: it lets you extend ZYRNMAPS with a separately built, separately signed Android APK. Your plugin can add map sources and layers, put objects on the map, contribute a panel, answer search queries, add context-menu actions, react to taps, post notifications and talk on the app's event bus.
The one design decision worth knowing first
Everything your plugin creates through the context it is handed is tracked by the host, and released when the plugin is deactivated, whether or not your teardown ran, and whether or not it threw.
This is deliberate, and it is aimed at a specific well-known failure. ATAK plugins are notorious for leaking listeners across reloads, because onDestroyImpl has to manually undo whatever onCreate did and a human has to remember every line. Here the context records every source, layer, subscription, object, panel and cleanup callback you create, and deactivate() releases them regardless. A plugin that forgets to clean up cannot leak, because it was never the plugin's job.
The practical consequence: your onDeactivate can be empty, and in most plugins it should be. Register things through the context and let the host own their lifetime.
What a plugin cannot do
Named up front, because discovering a wall later is worse than being told about it now.
- It cannot draw its own UI. A panel is described as rows and the host draws them, in the host's own controls. A plugin that needs a chart or a camera preview cannot have one yet. That is the stated gap, and the reason is in Panels.
- It cannot consume a map tap. Tap delivery is read-only; a plugin that could swallow the map's primary gesture could make the app look broken with no way to find out why.
- It cannot set the camera. It requests a move, and the host's camera controller remains the only writer.
- It cannot choose a notification channel, importance or full-screen intent. A notice is a title and a body. A map layer should not be able to hold the screen on and buzz through a silent profile.
- It cannot touch MapLibre directly. It gets a narrowed facade, so an engine upgrade is not a plugin-breaking change.
Native plugins: quick start
Depend on the API, and only on the API
dependencies {
compileOnly(project(":plugin:api")) // or the published artifact
}compileOnly is load-bearing. This is the mistake that costs an afternoon. A plugin that bundles the API ships a second copy of every class under the same names. Loaded through a DexClassLoader, those are not the same types the host holds, so an is ZyrnPlugin check fails and the error reads as a ClassCastException between a type and itself. Compile against the API; never package it. No Compose, no MapLibre, no host internals either.Declare yourself in the manifest
<application>
<meta-data android:name="zyrn-plugin-api" android:value="2" />
<meta-data android:name="zyrn-plugin-class" android:value="com.example.MyPlugin" />
<!-- Never started. Exists so ZYRNMAPS can SEE this package. -->
<activity android:name=".PluginMarker" android:exported="true" android:enabled="false">
<intent-filter>
<action android:name="com.zyrntopo.plugin.ACTION_PLUGIN" />
</intent-filter>
</activity>
</application>getInstalledApplications returning other people's packages, so a host that only reads manifest meta-data finds nothing at all. The way through is <queries>, which filters by intent rather than by package, so a plugin declares an activity for this action purely to be visible, and the host declares a matching <queries><intent>. The activity is never started and needs no class behind it. ATAK's own template carries the same warning about its equivalent entry, for exactly the same reason.Implement ZyrnPlugin
class MyPlugin : ZyrnPlugin {
override val id = "com.example.myplugin"
override val name = "My Plugin"
override val minHostApi = 2
override val capabilities = setOf(Capability.PANEL, Capability.MAP_OBJECTS)
override suspend fun onActivate(ctx: PluginContext) {
ctx.addObject(PluginObject("home", """
{"type":"Feature",
"geometry":{"type":"Point","coordinates":[-122.68,45.52]},
"properties":{"name":"Base"}}
""".trimIndent()))
}
}Build, install, enable
./gradlew :myplugin:assembleDebug
adb install -r myplugin/build/outputs/apk/debug/myplugin-debug.apkThen ZYRNMAPS ▸ Menu ▸ Plugins ▸ enable it.
Native plugins: interface and lifecycle
What you declare
| Member | Type | Why the host wants it |
|---|---|---|
id | String | Namespaces your storage and your map objects |
name | String | What the Plugins screen shows |
minHostApi | Int | The lowest host API you work against, so the host can refuse rather than crash |
capabilities | Set<Capability> | What you need. Declared, never assumed |
tier | Tier | FREE (default) or PRO. The host refuses a Pro plugin without entitlement, and says so |
minHostApi to the lowest API level your plugin really needs. If you set it lower, the plugin loads into older apps and crashes when it calls a method they don't have. With the right value, the Plugins screen tells the user the app needs an update instead.Activation
suspend fun onActivate(ctx: PluginContext)It suspends, and that removes a distinction you would otherwise have to make. A plugin that lazily loads a large payload cannot finish inside a synchronous call, and a naive host marks such a plugin active the instant activation returns, leaving a failed load reporting as active with nothing behind it. A suspend function has one shape and cannot have that bug.
Deactivation
fun onDeactivate() {} // may be empty, and usually should beUndo only what you created outside the context: your own timers, threads, or state the API does not model. Everything registered through the context is released by the host afterwards, whether or not this ran, and whether or not it threw.
Why a plugin might be refused
Each of these is a different thing to tell the user, which is why they are separate values rather than one error string.
| Reason | Meaning |
|---|---|
HostTooOld | Built against a newer host than this one. The user needs a ZYRNMAPS update |
PluginTooOld | Built against a host API this version no longer speaks. The plugin needs updating |
NoEntryPoint | The manifest names no plugin class |
NotLoadable | The class could not be loaded, or is not a ZyrnPlugin. Carries the reason |
NotEnabled | Discovered and allowed; the user simply has not switched it on |
Native plugins: capabilities
The list is fixed. A plugin can't ask for a capability outside it, because no version of the app would know whether to grant or refuse it.
| Capability | What it grants |
|---|---|
MAP_LAYERS | Add sources and layers to the map |
MAP_CAMERA | Read the camera and request moves. Never writes it directly |
MAP_QUERY | Query rendered features under a point |
MAP_OBJECTS | Put objects on the map and take them off again |
MAP_EVENTS | Be told where the user tapped (read-only) |
MAP_ACTION | Contribute an entry to the map's context menu |
PANEL | Contribute a panel to the UI |
SEARCH | Contribute results to the search sheet |
NOTIFICATION | Post a notification |
LOCATION | Read the user's current position |
STORAGE | Namespaced persistent storage |
BUS | Subscribe and publish on the integration bus |
The last four in the list above (map objects, map events, notifications and search) were picked from what ATAK plugins register. Capabilities ZYRNTOPO has no equivalent for (radios, contacts, the GL item factory, video layers) are left out, so a plugin can't declare something the app can't provide.
Native plugins: the context
PluginContext is what you are handed on activation. Every registering method on it is tracked, and released on deactivate.
Map sources and layers
ctx.addSource("my-src", SourceSpec(type = "geojson", json = """{"type":"geojson","data":{...}}"""))
ctx.addLayer(LayerSpec(id = "my-layer", json = """{"id":"my-layer","type":"line","source":"my-src"}"""))Sources and layers are style JSON as text, not SDK objects, for the same reason the map is a facade: a plugin compiled against RasterSource breaks the day the SDK renames it. On teardown, layers are always removed before their sources.
The map facade
interface MapFacade {
fun hasSource(sourceId: String): Boolean
fun addSource(sourceId: String, spec: SourceSpec)
fun removeSource(sourceId: String)
fun hasLayer(layerId: String): Boolean
fun addLayer(spec: LayerSpec)
fun removeLayer(layerId: String)
fun requestCamera(intent: CameraIntent) // intent, not a command
fun camera(): CameraIntent // read-only
}The API is small because every method in it has to keep working through engine upgrades. A method is only added when a plugin can't do its job without it. There is no camera setter: requestCamera asks for a camera move, and the app's own camera controller makes it.
Storage, bus and entitlements
ctx.storage.set("last-run", System.currentTimeMillis().toString())
val sub = ctx.subscribe("position") { payload -> /* … */ }
ctx.publish("my.topic", payload)
if (ctx.entitlements.isPro) { /* … */ }Storage is namespaced by plugin id, so two plugins cannot collide on a key. Subscriptions are released for you; canceling one twice is not an error, because the host may already have released it.
Anything the API does not model
ctx.onCleanup { myTimer.cancel() }Native plugins: panels
A plugin describes rows; the host draws them, in its own controls, honoring night mode, glove mode and the accent color.
Why not just hand you a View?
A View passed across the DexClassLoader boundary would tie every plugin to the app's UI toolkit. Plugins would compile against our Compose version and theme, and every upgrade of either would break the plugins already installed. Panels drawn by plugins in their own style would also look out of place, and would keep the wrong colors when the map switches to night mode.
The downside is that a plugin can't show a chart or a camera preview yet.
The rows
| Row | Renders as |
|---|---|
Reading(label, value) | A label and a value, in the shape the diagnostics screens use |
Note(text) | A paragraph: an explanation, or a refusal with its reason in it |
Section(title) | A heading between groups of readings |
Action(id, label, enabled, onInvoke) | A chip that does something. enabled = false draws it dimmed rather than hiding it |
Toggle(id, label, on, onChange) | A chip that is lit or not, reporting its new state when pressed |
rows is a lambda, and you must call refreshPanels()
ctx.registerPanel(PluginPanel(
id = "mine",
title = "My Plugin",
rows = { listOf(PanelRow.Reading("Queue", "$queued")) }, // read at DRAW time
))rows is a lambda rather than a list so a panel showing a count, a status or a reading updates when the underlying thing moves. A snapshot taken at registration is a panel that is correct exactly once.
ctx.refreshPanels() whenever something a row displays has moved. It is cheap, idempotent, and safe to call from a background thread. Calling it when nothing changed costs one recomposition of one screen; not calling it is a panel that is correct once, the exact failure the lambda was chosen to avoid, arriving through the other door.Native plugins: objects, taps, notices and search
Map objects
ctx.addObject(PluginObject(id = "beacon", featureJson = """
{"type":"Feature",
"geometry":{"type":"Point","coordinates":[-122.6765,45.5231]},
"properties":{"name":"Beacon"}}
""".trimIndent()))
ctx.removeObject("beacon")Geometry is a GeoJSON Feature as text, which the object store already speaks and what every export path understands. The host rejects anything that is not one. Your id is namespaced to plugin:<your-id>:<yours>, so two plugins choosing the same name cannot collide, a deactivate removes exactly what you added, and nothing the user drew is ever touched.
Map taps
ctx.onMapTap { tap -> /* tap.lat, tap.lon, tap.zoom */ }Read-only, and you cannot consume the event.
Notifications
ctx.notify(PluginNotice(title = "My Plugin", body = "Something happened."))A title and a body, and nothing else; see what a plugin cannot do for why.
Search providers
ctx.registerSearch { query ->
if (!query.equals("beacon", ignoreCase = true)) emptyList()
else listOf(PluginSearchResult("Beacon", "A place only this plugin knows", 45.5231, -122.6765))
}The case this exists for is a team with its own gazetteer (grid squares, hut names, a numbered pole network) that no general geocoder has heard of, and which is exactly what somebody types into a search box first.
Native plugins: signing, trust and versioning
Signing
Sign with your own key. ZYRNMAPS reads the signature, records the SHA-256 fingerprint and shows it, and loads your plugin either way; see Who signed it for what users are told. Publish your fingerprint so people can check it.
keytool -genkeypair -v -keystore my-plugin.keystore \
-alias myplugin -keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=My Plugin, O=My Org, C=US"Host API versioning
| Constant | Value | Meaning |
|---|---|---|
HostApi.VERSION | 2 | The current host API |
PluginManifest.MIN_PLUGIN_API | 1 | The oldest plugin API still loaded |
Version 2 is where the surface grew map objects, map taps, notifications, search providers and panel content. A plugin built against 1 declares minHostApi = 1 and still runs, because the comparison is minHostApi > VERSION; a plugin that needs the newer surface says so and is refused with a sentence rather than a NoSuchMethodError at some later moment.
The floor stays at 1 until something is actually removed from the contract. Refusing an old plugin costs a user a working tool; loading one that calls a deleted method costs them a crash, so the floor moves only when the second becomes true.
Manifest keys, in one place
| Key | Value |
|---|---|
zyrn-plugin-api | The host API you built against. Doubles as the marker that this APK is a plugin |
zyrn-plugin-class | Fully-qualified class implementing ZyrnPlugin |
com.zyrntopo.plugin.ACTION_PLUGIN | The intent action your marker activity declares, so the package is visible on Android 11+ |
The same key gives the API version and marks the package as a plugin, so the two can't disagree.
Native plugins: a complete example
This exercises every surface the host honors today: it drops a marker, adds a panel with live counts, answers one search word, listens for taps and posts a notice on the third one.
class SamplePlugin : ZyrnPlugin {
override val id = "com.example.sample"
override val name = "Sample Plugin"
override val minHostApi = 2
override val capabilities = setOf(
Capability.PANEL, Capability.STORAGE, Capability.MAP_OBJECTS,
Capability.MAP_EVENTS, Capability.NOTIFICATION, Capability.SEARCH,
)
private var taps = 0
private var lastTap: MapTap? = null
override suspend fun onActivate(ctx: PluginContext) {
ctx.addObject(PluginObject("beacon", """
{"type":"Feature",
"geometry":{"type":"Point","coordinates":[-122.6765,45.5231]},
"properties":{"name":"Sample plugin beacon"}}
""".trimIndent()))
ctx.onMapTap { tap ->
taps++
lastTap = tap
ctx.refreshPanels() // without this the panel is frozen
if (taps == 3) ctx.notify(
PluginNotice("Sample plugin", "You have tapped the map $taps times.")
)
}
ctx.registerSearch { query ->
if (!query.trim().equals("beacon", ignoreCase = true)) emptyList()
else listOf(PluginSearchResult(
"Sample plugin beacon", "A place only this plugin knows about",
45.5231, -122.6765,
))
}
ctx.registerPanel(PluginPanel(
id = "sample",
title = "Sample plugin",
rows = {
listOf(
PanelRow.Reading("Taps seen", "$taps"),
PanelRow.Reading("Last tap", lastTap?.let { "%.5f, %.5f".format(it.lat, it.lon) } ?: "none yet"),
PanelRow.Note("Search \"beacon\" to find this plugin's marker."),
PanelRow.Action("reset", "Reset count") {
taps = 0; lastTap = null; ctx.refreshPanels()
},
)
},
))
}
// The object, tap handler, search provider and panel were registered
// through the context, which releases them on deactivate.
}Verifying your plugin actually loads
Unit tests cannot prove the part that really fails. Activating an object your test constructed says nothing about whether a separately compiled, separately signed APK loaded through a DexClassLoader sees ZyrnPlugin as the same interface the host holds. Install the real APK on a real device, enable it, and confirm the Plugins screen reports it as third-party and running.