HerdTag

Source: docs/architecture.md

Architecture

Components

Component What it does Code
Ear tag Takes GPS fixes, runs the fence controller, plays cues, sends position reports, receives fence updates. Works with no network firmware/tag/ (sketch), firmware/core/ (logic)
Gateway A LoRaWAN gateway on a high point on the farm (planned: RAK7268V2, US915). Relays radio packets to the network server over Ethernet or Wi-Fi hardware only; see hardware/BUILD.md §7
The Things Stack (TTS) LoRaWAN network server. Handles device joins, encryption, deduplication and the downlink queue. Calls our backend by webhook for every uplink external service (TTS Sandbox to start)
Backend Decodes uplinks, stores positions and events, validates fences, turns fences into downlink fragments, tracks which tag has confirmed which fence src/herdtag/server/
Dashboards The operator map at /: live tags, draw fences, send them to tags, events. Other views (farmer, ops, trial, simulator explorer) are being added src/herdtag/server/static/
Simulator Offline herd + GPS-error simulator for testing control logic and power trade-offs before touching an animal src/herdtag/sim.py
Virtual tags Simulated cows running the real controller and talking to a live backend in TTS webhook format src/herdtag/demo.py

Data flow

flowchart LR
  subgraph Tag["Ear tag (RAK4631)"]
    GNSS[GNSS fix] --> CTL[Fence controller]
    CTL -->|cue| PIEZO[Piezo cue]
    CTL -->|next fix time| GNSS
  end
  CTL -- "11-byte uplink, FPort 1" --> GW[Farm gateway]
  GW --> TTS[The Things Stack]
  TTS -- "webhook POST /ingest/ttn" --> BE[Backend]
  BE --> DB[(SQLite)]
  DB --> UI[Map and dashboards]
  UI -- "draw fence, send to tags" --> BE
  BE -- "fence fragments, FPort 2" --> TTS
  TTS -- "Class A downlink after next uplink" --> GW --> CTL

A fence change makes a round trip:

  1. The operator draws a polygon in the UI → POST /api/fences. The backend validates it (3–32 corners, no crossing edges, within ±32 km of its centre).
  2. "Send to all" → POST /api/fences/{id}/assign. The backend encodes the fence into ≤51-byte fragments and pushes them onto each device's TTS downlink queue. Without TTS credentials it keeps them in a local queue that the virtual tags read.
  3. LoRaWAN Class A devices can only receive in the two short windows after they transmit. So fragments trickle down one per uplink. While a fence is half-received, the tag sends an uplink every 30 s to pull the next one.
  4. When all fragments arrive and the CRC checks out, the tag swaps in the new fence and sends an uplink with the fence_ack flag and the new fence id. The backend logs fence_synced and the UI shows the tag as synced.

Design principles

The tag decides locally. Cue timing can't depend on a network round trip. LoRaWAN downlinks are slow and only arrive after an uplink. So the tag holds the fence polygon and runs the whole controller itself. The network is only for reporting and for changing the fence.

The controller is deterministic and small. Given the same sequence of fixes, it produces the same decisions. There's no randomness, no learning on the device, and nothing that wouldn't port to a microcontroller. This makes it testable and explainable to a vet or a regulator. Adaptive or learned behaviour can come later, on the backend, once there's real data.

Python is the spec, C is the port. src/herdtag/controller.py (with geo.py and protocol.py) is the reference implementation. scripts/gen_vectors.py runs it across 11 scenarios and writes the expected outputs into firmware/core/test/vectors.h. The C test replays those inputs and checks every output. Any behaviour change goes into Python first, then the vectors are regenerated, then C is made to match. See Firmware.

Welfare constraints are in the code, not the manual. No cue on a bad fix, no cue once the animal is outside, hard caps per episode and per hour, and fixed volume. See Controller.

Hardware-agnostic upstream. The backend only knows the 11-byte uplink and the fence fragment format (Protocol). Swapping the GNSS module, the board, or even the network server only touches one layer.