Skip to content

Protect Go, the app for the place without a connection

Smart Parks Protect Go (named Field until 2026-10-17; the repository and the installers were renamed with it) is a separate program (SmartParksOrg/smartparks-protect-go, its documentation) that a person runs on a laptop they take to the offload stations. It reads a station's SD card where there is no connection, decodes the trackers' frames with Protect's own code, shows the positions and measurements, and hands the untouched card files to Protect when there is a connection again. Protect decodes them itself; nothing decoded in the field comes back. Decision D331 (2026-10-17); Protect Go's own design is its ADR 0001.

Installing the app from the Offload page

The Offload page's "Protect Go" button opens a card with two buttons and one line (decision D332; Tim, 2026-10-17: as little text as it can): "Open Protect Go" starts the installed app connected to this server and signed in as the person on the page (decision D333: the button asks for a one-time code, POST /api/v1/auth/go-handoff, opens protect-go://connect?server=…&code=…, and the app exchanges the code once, POST /api/v1/auth/go-handoff/exchange, for a token of its own that lasts 30 days); "Install for Windows" (or macOS, Linux, by the visitor's system) downloads the newest installer from releases/latest/download, the other systems and the app's guide a link away. The app is installed and updated the way the desktop AddaxAI is (its decision D6): a release on GitHub carries the three installers, and on Windows the app downloads and installs a newer one itself (its decision D17), macOS follows once the app is signed.

What Protect gives the app

The field bundle, GET /projects/{id}/field-bundle, for a member of the project. One document with:

  • the project (id, name, slug, time zone) and the versions that made the bundle (Protect's version and commit, the server's address for the upload);
  • the project's devices the caller may see, each with its type and driver, its serial and DevEUI, its status and firmware, the entity it tracks now, and whether it is an offload station;
  • each tracker's LP0 address, and its two session keys when the caller may set keys in this project (device control in the project, or a server admin, the rule of the device page's credentials card); a viewer gets the address and keys_set and no key;
  • the entities those devices track, with type and icon and a place set by hand;
  • the offload stations with their EUI and where they stand;
  • the trackers of other projects the stations served in the last 30 days (decision D348): address, device and project name, never a key, so the app names a card's frames of such a tracker instead of offering to add a device Protect already has;
  • the metric registry (key, label, unit, value type, category, description; decision D334), so the app's Data explorer names and units the values it decodes as Protect does;
  • the project's regions (areas, decision D365): the boxes of its features, its entities' and devices' places and the current positions of its devices and entities, grouped where they lie within the server's region gap (100 km) of one another, which the offline map is cut for; area is their envelope.

Every download is an audit log entry (project.field_bundle_downloaded) with how many devices, stations and keys it carried. The caller's visibility applies as everywhere: a member limited to some groups gets those groups' devices.

The decode path, shared/station_decode.py: unwrap_station_frame opens a frame with the tracker's keys and refuses it with the same code and words the server's trace carries (no keys, a station naming another tracker than the address belongs to, an LP0 metadata version this code does not read, a frame that does not verify); decode_station_frame has the device's driver read the records exactly as the pipeline has it read them. The pipeline calls the same two functions, and a test decodes one frame both ways and compares the positions, so Protect Go's reading of a card and Protect's cannot drift.

The parser, shared/station_files.py: the module that reads a station's files and unpacks a card's zip, with no database, broker or settings import, so Protect Go reads a card on a laptop with the same code the file worker runs on the server. Protect Go installs Protect's shared package pinned to a commit and imports this module, the LP0 codec and the OpenCollar driver.

The offline map

Protect Go draws positions on a map where there is no connection, so the map must travel with the bundle (Protect Go's decision D4; phase 43 P3). Protect cuts it: POST /projects/{id}/field-bundle/map, for a member who may create exports (an analyst or an admin; a map is a file made for members as an export is), asks the export worker to run pmtiles extract on the newest Protomaps planet build, a single file of some 140 GB at build.protomaps.com that answers range requests, for the project's regions, each with a five kilometre margin, down to zoom 14, in one file (decision D365, Tim, 2026-10-28: the offload station stood in the Netherlands and the tracker it served had gone to Namibia; one box over both is most of Africa, so places farther apart than MAP_EXTRACT_REGION_GAP_KM are regions of their own). The worker reads only the ranges the regions need, keeps the result in the map-extracts bucket and writes its size, digest and build date on the project's row; GET on the same path, for every member, streams the file once it is ready, answers 202 with the state while it is being made, and 409 with the reason when the attempt failed. A map younger than a month whose regions cover every region the project has is kept; force cuts again. The bundle names the map's state, with covers_areas saying whether the map still covers the project; when a tracker went somewhere the map does not reach, Protect Go asks for a new one at its next load. A park-sized area at zoom 14 is a few tens of megabytes; MAP_EXTRACT_MAX_ZOOM, MAP_EXTRACT_MAX_BYTES and MAP_EXTRACT_SOURCE (a build pinned by URL, or a file for a server without internet) are the settings. The tiles are OpenStreetMap's, under the ODbL, built by Protomaps; Protect Go's map says so. Satellite imagery stays online: its terms do not allow caching.

What the app gives Protect

The card's files, exactly as read, through POST /devices/{id}/offload-files, the same upload the station's device page makes. Protect decodes them, attributes the records as of their record time, and leaves out a file it already holds by its content digest (the answer names it with duplicate, decision D353, never a refusal) and a frame it already holds by its hash, so a card read in the field and uploaded later costs nothing twice. The app itself leaves out a file whose bytes it read before (its decision D30), so a card read twice on the laptop is read and uploaded once.

What never travels

No decoded row, no master key (Protect holds none, decision D327), and for now no notes or edits made offline. Should people need to record things in the field that must reach Protect, Protect Go's plan parks an append-only outbox for it.

Still to come

Protect Go's own pages for the bundle, the cards and the upload (Protect Go's phase 2); on this side, nothing is open.

The project follows the server

The app compares the loaded project's fingerprint with the server's every five minutes while it is online and signed in (GET /projects/{id}/field-bundle/revision, decision D344; the app's decision D28, 2026-10-19). When the project changed on the server, a device, a station, a tracker's keys, an entity or the map, the app loads the project again by itself, reads the frames of trackers unknown before with it, and every page shows the new state; a line in the app says when that happened. Loading by hand on the Server page still works for a laptop that was offline for long.