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:
- 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). - "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. - 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.
- When all fragments arrive and the CRC checks out, the tag swaps in the new fence and sends an uplink with the
fence_ackflag and the new fence id. The backend logsfence_syncedand 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.