HerdTag

Source: docs/health.md

Heat, health and the night alarm

The tag's accelerometer already sits on every ear. These features count how active each cow is, learn her normal day, and flag the departures a rancher cares about. The rules are plain arithmetic with every threshold written down, so each alert can be explained. The thresholds are starting points. They need checking against what the rancher and vet actually see on this herd before anyone relies on them.

Alert What it means What triggers it
Possible heat She may be in standing heat; check her Activity at least 2x her normal for that hour of the day, and at least 10 more active minutes than normal, in 4 or more of the last 12 hours
Moving less than usual Possible lameness or illness; check her The last 24 hours add up to 60% or less of her normal day, with data for at least 75% of those hours
Herd ran at night Possible predator, loose dog or theft; check the pasture 3 or more tags (half the herd for herds under 6) report sustained running within 10 minutes, during night hours (21:00–06:00 by default)

No heat or low-activity alert fires until a cow has 5 days of history. One heat alert per cow per 7 days (cycles are about 21 days apart), one low-activity alert per 2 days, and one night alarm per 30 minutes.

The code is in src/herdtag/server/health.py. The thresholds are constants at the top of that file.

How the data gets there

The tag adds up "seconds active" in 15-minute buckets. Every hour it sends an activity report on FPort 3, separate from the position check-in on FPort 1. It's 11 bytes, so it fits US915's longest-range data rate:

Byte Field Meaning
0 newest Number of the newest finished bucket (counts up, wraps at 256)
1 flags bit 0 running: sustained running since the last report. bit 1 reboot: first report since power-up
2 age Minutes since the newest bucket finished (max 254)
3–10 b[0..7] Active time per bucket, oldest first, in 4-second units (0–225 = 0–15 min). 255 = no data

Each report covers the last 2 hours, so reports overlap. If one is lost, the next one still carries those buckets. The server stores each bucket once, matching on bucket number and time.

Encoding is in src/herdtag/protocol.py (ActivityReport, ActivityAccumulator). The C version is in firmware/core (ht_activity_*). The two are checked against each other by the golden vectors (make -C firmware/core test).

Running means the tag saw hard, sustained movement (30 s in the firmware). The tag reports it right away at any hour, at most once every 5 minutes, because it doesn't know the farm's time zone. The server decides whether it's night.

How a cow's normal is learned

Where to see it

API:

Method Path Returns
GET /api/health Per cow: baseline ready, days of history, last-24 h minutes vs. normal, ratio, heat hours, the last 24 hourly values, and active alerts. Plus the latest night alarm and the thresholds in use
GET /api/health/heatmap?hours=48 Hour timestamps and, per cow, ratio to normal and active minutes for each hour
GET /api/health/{dev_id}/activity?days=14 Raw buckets and her normal by hour of day

Settings (environment variables): HERDTAG_TZ (the farm's time zone, e.g. America/Chicago; default is the server's), HERDTAG_NIGHT_HOURS (default 21-6), HERDTAG_HEALTH_MIN_DAYS (default 5).

Try it

uv run herdtag-server
uv run python scripts/seed_activity.py --night-scare   # 14 days of history: two cows in heat, one lame, a 2 am bolt
uv run herdtag demo --speed 600 --night-scare          # live virtual herd with activity reports

Demo options:

The demo's activity reports carry a virtual clock that runs at demo speed, so a simulated day shows up as a day on the health pages.

Limits