Luke Angel
← back to the bookcase
A DIY pet collar tracker on a workbench: a teal PCB puck clipped to a collar strap emits radio signal beside a coin-cell battery, a microcontroller chip, a soldering iron on a pad, and a location pin, with a dog wearing the collar. Notebook · 08 parts
Notebook · 8 parts · read in order
~72 min total

IoT Pet Health Tracker — Build

Twelve years of paying other people's subscriptions to learn my pets are home. This is the build log for the tracker I actually want — cheap, no-subscription, months of battery, location first and health later — designed the way I'd run any connected-product program, then soldered together nights and weekends.

The Pet IoT Field Guide is the observer's seat — twelve years of what shipped and what broke, kept in the order it happened. This notebook is the opposite chair: the builder's log.

I've spent a career building connected-hardware platforms and a decade buying connected-pet gear for Atom, Joule, Boson, and Quark. The two finally collided over a renewal notice. What follows is the whole train of thought — from "I'm tired of this" to a product spec, to the gear order, to the nights-and-weekends build that comes after. Location first, on BLE and LoRa. Cellular second. Health vitals third.

It's a real build with a full-time job attached, so it moves at the speed of evenings. Follow along — including the parts that don't work.

Start here
01 · The Renewal Notice That Made Me Build My Own Pet Tracker
open part 01 →
A growing stack of recurring subscription bills on one side, a single hand-built location tracker puck on a workbench on the other — years of renewal fees versus a one-time build. Part 01 of 08
IoT Pet Health Tracker — Build · part 01
Apr 27, 2026

The Renewal Notice That Made Me Build My Own Pet Tracker

Three pets, three subscriptions, and the bulk of what I get for the money is a green dot that says 'home.' After a decade of paying other people to tell me my pets are where I left them, I'm building the tracker I actually want.

The renewal notice landed for the third time this year — different pet, same number. Add it up across Quark's collar and the two on the cats and I'm spending north of three hundred dollars a year, most of it to confirm that animals who rarely leave my house are, in fact, in my house.

I've been in connected products my whole career, and buying connected-pet gear since Atom got his first Whistle in 2013. Twelve years. It took a renewal email to make me actually do the math on what I'm paying for.

What I'm actually paying for

Strip it down and the answer is uncomfortable: presence. Ninety-five percent of what these trackers tell me is "your pet is home," which I could get from a two-dollar radio that never leaves the house. The cellular link — the expensive part, the part the subscription exists to fund — earns its keep maybe a handful of times a year, when a pet is actually somewhere it shouldn't be.

And the cellular link is also why I'm on the charging treadmill. Always-on LTE is the reason these collars die every few days. So I'm paying a monthly fee and a recurring chore, both in service of a capability my indoor cats use approximately never.

The math only gets worse the longer you run it. A subscription is a line that never stops climbing — three pets at ~$100/year each, compounding for as long as you own the gear. A one-time build is a single step up for parts and then flat. The two lines cross sooner than the renewal email wants you to notice.

Cumulative cost over time: a red staircase for subscriptions, climbing roughly $300 a year across three pets and never leveling off, versus a teal line for a one-time DIY build that jumps once for parts and then stays flat near zero per year. The lines cross early, where the parts pay for themselves.

Tractive just proved my point

Right on cue, Tractive shipped the CAT 6 Mini this month — their newest cat tracker, and the first to carry real Health Intelligence: resting heart rate and respiratory rate on a 1.13-ounce collar. It's genuinely impressive, and it tells me the whole category is finally moving toward health, which is where the real value has always been.

But look at what it actually is: another cellular collar, another subscription, per cat. Joule and Boson have worn Tractive trackers for years and they don't leave the house. Putting a cellular plan on a cat that's asleep on the couch is the most on-the-nose version of the problem I've been paying into for a decade.

The same gaps, twelve years running

I started all this with Atom — the first Whistle on his collar two months after he came home in 2013. He's gone now, and across his whole life the trackers got better at counting steps and never once got better at the things that actually bothered me: the cost scaled linearly with every pet I added, the batteries needed charging constantly, and the indoor case always paid full freight for range it never used.

Quark wears the dog version now; the cats wear theirs. Better silicon, glossier app, same three gaps.

So I'm going to build it

Here's the thing — I do this for a living. I just open-sourced a reference IoT stack: AWS IoT Core, device certs, ingest, a dashboard, the works. The cloud half of a pet tracker is already sitting in a repo with my name on it. I have the backend, I have the wireless decision rubric I make every team run, and I have twelve years of receipts on exactly where these products fall short.

So I'm building the one I actually want. The bet is simple and it's the inverse of what everyone ships:

  • Cheap, with little or no subscription — because most of the value doesn't need a cellular plan.
  • Months of battery, not days — because nobody wants to charge the thing.
  • Location first, health later — get the dot reliable before it takes a heart rate.
  • Matched to how pets actually live — home most of the time, occasionally on a trail, rarely truly lost.

The inverted bet, four requirements side by side. What everyone ships, in red: a monthly cellular plan per pet; a battery that dies every few days; health-first features on a coin-cell radio; hardware that pays for range it never uses. What I actually want, in teal: cheap with little or no subscription; months of battery, not days; location first and health later; matched to how pets actually live — home most of the time, occasionally on a trail, rarely truly lost.

Next post is the part where I resist the urge to start soldering and do the PM work first: the use cases, the rubric, and an honest read on whether this is a product or just a grudge. Then we buy parts.

A collar puck with a paw mark radiates three concentric radio rings of increasing range, linked by a direct dashed line straight to its own cloud — no vendor box in between — beside a dollar coin struck through, standing for no recurring subscription. Part 02 of 08
IoT Pet Health Tracker — Build · part 02
May 13, 2026

Speccing a Pet Tracker That Doesn't Need a Subscription

Before I buy a single board, the PM work: three use cases, the wireless rubric, and the product line that falls out of it. The punchline is that the two loudest complaints about pet trackers — price and charging — have the same root cause, and you fix both by refusing to put cellular where it isn't needed.

Last time I talked myself into building a pet tracker. This time I do the part that keeps a grudge from becoming a garage full of dead prototypes: the product work, before the soldering iron.

Three use cases, in priority order

  1. Is my pet home / in the yard? The 95% case. Wants near-zero power, near-zero data, an instant "they left" alert, and activity tracking.
  2. My pet is off-grid with me — a trail, the woods. No cell coverage. I want to see where they are relative to me.
  3. My pet got out and is miles away. Rare, high-emotion — and the fear that actually sells trackers.

Most products are built entirely around #3 and bill you monthly for it. But #1 is where pets spend their lives.

The rubric, applied

I make every connected-product team I run answer five questions before picking a radio. Run them on a pet:

  1. Range? Home is meters. A trail is kilometers. "Lost in the next county" is anywhere.
  2. How often does it phone home, and how big is the message? A GPS fix is ~12 bytes, and at home you only need a heartbeat. Tiny.
  3. BOM budget? Consumer. Every dollar of radio locks the rest of the bill.
  4. Power budget? Small battery, and — per the last postno charging cult.
  5. Security model? Consumer. A device cert to the cloud, TLS, no drama.

Answer those honestly and you do not arrive at "always-on cellular for everything." You arrive at a spectrum.

The connectivity spectrum

RadioRangePowerCostGood for
BLEmeterssips (an AirTag runs ~a year on a coin cell)~free"Is it home?" presence
LoRa (915 MHz)kilometers, line-of-sightlowcheap, no carriertrail + local recovery, if you bring the gateway
Cellular (LTE-M)anywhere with a towerhungryhardware + monthly datathe genuinely-lost case

The whole insight is one line: match the radio to the usage. A pet that's home 95% of the time should be running its cheapest, lowest-power radio 95% of the time — and only reach for the expensive one during the rare event that needs it.

Three radios laid out along a range axis from meters to anywhere: BLE for the 95% home case (sips power, ~a year on a coin cell, no carrier), LoRa at 915 MHz for trail and local recovery (low power, cheap, bring your own gateway), and cellular LTE-M for the rare genuinely-lost case (hungry, days-to-two-weeks battery, hardware plus a monthly data plan).

The product line

That spectrum is the product line. Three SKUs, so the customer picks their own range / cost / battery trade instead of me guessing:

SKURadiosBatteryForMoney
LoRa collarBLE + LoRa + GPSmonthshome/indoor pets; trail dogs (with the base station)hardware + optional ~$20/yr cloud history
Cellular collarBLE + LTE-M + GPSdays–~2 wksroamers / escape artists / "find from the couch"premium hardware + data sub (still under the incumbents)
Base station / handheldBLE + LoRaweeks (docked = always charged)every LoRa-collar ownerbundled with the LoRa collar

The tiered-power trick

The LoRa collar does one clever thing: it lives in BLE presence mode at home (months of battery, ~no data), and only flips to findable mode — GPS fixes + LoRa beacons — when it crosses the geofence. The expensive radio work only happens during the rare event, so the months-long battery survives. That single decision is what delivers both of the things people complain about: low cost and no charging.

The collar's two power states. BLE presence mode at home — GPS off, LoRa quiet, BLE heartbeat only, accelerometer logging activity — gives months of battery. Crossing the geofence flips it to findable mode: GPS fixes on, LoRa beacons out to the base station, an RSSI proximity beep on approach; battery drains fast but only briefly. Coming back inside flips it down to presence mode again.

The base station is the network

Normally it's a docked listener at home. But it's built as a handheld in a charging cradle — Garmin-Alpha style — so you grab it off the dock and it becomes a mobile LoRa gateway. Pet got out? You drive a grid around the neighborhood and re-acquire its beacon; the collar's GPS gets you to the block, and an RSSI proximity beep (faster and louder as you close in) gets you to the actual bush. On a trail, you just carry it and watch the pet relative to you, phone over BLE for the map.

And the strategic part: crowdsource the base stations. Every docked unit passively listens for any lost collar nearby and reports it. At neighborhood density that's a pet-specialized recovery mesh — the Apple Find My / Amazon Sidewalk model, seeded by always-on home base stations instead of phones. Tractive can't copy it; pure cellular has no network effect. The honest catch is cold-start: the mesh is only magic once it's dense, so early on recovery leans on you and your own handheld. Same chicken-and-egg every crowdsourced network faced.

Little features that punch above their BOM

A piezo buzzer costs pennies, so both the collar and the base get one:

  • Collar: beep to find it (it fell off, or the pet's hiding), a recall/training cue, and a humane no-shock "you're leaving the yard" boundary warning.
  • Base: an audible "he got out" alert the moment the pet crosses the geofence or drops off the radio — so you're not glued to the app — plus that proximity beep while you search.

The build phases

  1. Location — BLE + LoRa. The novel, can't-buy-it part: presence, the geofence flip, the mesh, the base station. The hardest phase, on purpose.
  2. Cellular. The "anywhere" premium SKU. A paved road (Nordic's nRF9160 + reference firmware), so it's a low-risk bolt-on.
  3. Health. And here's why it's last: pet vitals are mostly an IMU + algorithms problem, not a new sensor. A sensitive accelerometer picks up the body's micro-vibrations at rest — that's how the incumbents derive resting heart rate and respiratory rate, fur and all. So the accelerometer I put on the collar in phase 1 becomes the vitals sensor in phase 3, once tracking is rock-solid.

The connectivity spectrum drawn as three SKUs and the build order. The LoRa collar (BLE plus LoRa plus GPS, months of battery, hardware plus optional cloud history) for home and trail; the cellular collar (BLE plus LTE-M plus GPS, days-to-two-weeks battery, premium hardware plus a data sub) for roamers; and the base/handheld (BLE plus LoRa, always charged on its dock) that is the gateway and the recovery mesh, bundled with the LoRa collar. Below, the phased build runs location first (BLE plus LoRa, geofence, mesh — the hardest), then cellular as an nRF9160 bolt-on, then health, where the phase-1 accelerometer becomes the vitals sensor.

A phase-4 idea worth writing down now: the base station is stationary, right where the pet sleeps. It could later carry a mm-wave radar for fully contactless resting vitals — heart and respiratory rate with no collar contact at all. That's the magic version of "the base station is more than a charger."

The honest part

  • AirTag already owns cheap + no-subscription + find-my-network at $29. So my moat is not raw location — it's the pet features Apple won't build (activity, sleep, vitals trends, multi-pet, family alerts, vet-shareable data) plus the pet-specialized mesh. The position is Whistle's features, AirTag's price model.
  • A LoRa collar can't find a pet past the edge of the mesh and your car's reach. That's exactly what the cellular SKU is for. The line lets the buyer choose.
  • At volume the hardware BOM is ~$30. The reason incumbents charge $120/yr is the cellular data plan — remove cellular and you remove the cost basis for the subscription, which is how ~$20/yr for cloud history is honest and still 6× cheaper.

Spec's done. Next post: I stop typing and hit buy.

A pet collar tracker at the center, emitting concentric LoRa signal arcs to a base station and taking a GPS fix from a satellite overhead, with a small battery gauge on the tag — the gear at the heart of the build. Part 03 of 08
IoT Pet Health Tracker — Build · part 03
May 18, 2026

The Gear, the Bill, and a Six-Month Nights-and-Weekends Plan

The spec is set, so today I hit buy — but the order got smarter than the plan. I was going to build the handheld base station from modules; turns out it already exists off the shelf, pre-flashed and FCC-certified, as a two-pack. So step one is a proof-of-concept I can strap to Quark the day it lands, then the collar build behind it.

The spec is done, which means today is the dangerous day: I hit buy. But the cart looks different from the plan — because while I was sketching the handheld base station, I found out somebody already builds it.

The head start

The cloud is done. The reference IoT stack I open-sourced — IoT Core, device-cert provisioning, ingest, DynamoDB, a dashboard — already accepts a real device with near-zero change. The collar just publishes pet telemetry (lat, lon, battery, activity) instead of tool telemetry. That's months of backend I don't have to write.

The shortcut I didn't expect: the Wio Tracker L1

I'd specced the base station as a handheld-in-a-cradle — screen, GPS, LoRa, battery, a button or two. Turns out that's a shipping product: the Wio Tracker L1, sold as a ready-to-use Meshtastic handheld, pre-flashed, FCC-certified, in a two-pack.

The Meshnology Wio Tracker L1 two-pack — two ready-to-use Meshtastic handheld units with nRF52840 + SX1262 LoRa, GPS, and a 1.3-inch OLED, used as the pet-tracker proof-of-concept base station and test node.

The spec reads like I wrote it for myself:

Wio Tracker L1 specification — nRF52840 with 1 MB flash and 256 kB RAM, Wio-SX1262 LoRa 862–930 MHz, Bluetooth 5.0, L76K multi-constellation GNSS, 1.3-inch 128×64 OLED, Grove and plated-through-hole interfaces plus reserved SWD pads, USB-C / solar / lithium power, FCC/CE/RoHS certified.

  • nRF52840 + Wio-SX1262 (862–930 MHz) — the same chip family as the collar I'm going to build, so one firmware codebase covers both, and the reserved SWD pads mean the nRF9160 DK I already ordered can debug it.
  • L76K multi-GNSS, 1.3" OLED, onboard buzzer, 3000 mAh + solar + USB-C. Everything the base station needs, including the beeper for the "he got out" alert and the proximity search.
  • Grove + plated-through-hole I/O — I can hang an I²C sensor off it without soldering if I want to prototype the collar's heat-risk sensing on the bench.
  • Pre-flashed with Meshtastic — which is the whole point of starting here.

Wio Tracker L1 internals — the Seeed Wio-SX1262 board with an onboard buzzer and L76K GPS antenna, and a 3000 mAh lithium battery, fitted into the handheld case.

Wio Tracker L1 case detail — handheld enclosure with a 1.3-inch OLED, antenna port, USB-C, and power, reset, menu, and 4-way control buttons.

Because it's pre-flashed and comes as a pair, it isn't just the base station — it's my entire proof-of-concept. One on Quark, one in my hand, and I can watch him move on a phone map over Bluetooth the day it arrives. No firmware, no soldering, no waiting. That's the right first dollar.

The order, reordered around the PoC

1 — Proof-of-concept + base station (ordered today, arrives tomorrow):

  • Meshnology Wio Tracker L1, 2-pack (Amazon) — base unit + test node, pre-flashed Meshtastic.

2 — The collar build kit (RAK — this was the original plan; I'll revisit it once the PoC proves the range is real):

QtyPartPrice
2WisBlock Meshtastic Starter Kit, US915 (SKU 116016 — base + RAK4631 core)~$64 (buy-2 −8%)
2RAK12500 GNSS — u-blox ZOE-M8Q$51.24 (buy-2 −5%)
1RAK1904 3-axis accelerometer — ST LIS3DH$7.97
2RAK1901 temp/humidity — Sensirion SHTC3$14.36 (buy-2 −20%)

Plus a passive piezo buzzer (Amazon) and LiPos (Adafruit). The RAK4631 inside that starter kit is the same nRF52840 + SX1262 as the Wio L1 — so whatever firmware I prove on the PoC carries straight to a custom collar.

3 — Cellular (already ordered, for phase 2):

  • Nordic Thingy:91 + nRF9160 DK (~$295, DigiKey). The DK doubles as my debugger for everything above.

Three buys reordered around a proof-of-concept: Buy 1 is the pre-flashed Wio Tracker L1 two-pack that needs no firmware or soldering; Buy 2 is the RAK4631 collar kit, deferred until range proves out, sharing the same nRF52840 plus SX1262 chip family so one firmware codebase carries across both; Buy 3 is the cellular nRF9160 Thingy:91 and DK on a separate LTE path for phase two, with the DK doubling as the debugger for all of it.

The thread that ties the first two buys together is the silicon: the Wio L1 and the RAK4631 collar are both nRF52840 + SX1262, so the firmware I prove on the proof-of-concept is the firmware the custom collar runs. Cellular is the one part on a different chip — and it's deliberately last.

The phases, and what each one has to prove

  1. Location — BLE + LoRa. Presence at home, the geofence flip to findable mode, collar → base over LoRa, data into the existing cloud, beep + proximity search. The hard part, first.
  2. Cellular. Swap in the nRF9160 path for the "anywhere" collar. Lower risk — a well-documented road.
  3. Health. Resting heart rate and respiratory rate off the accelerometer the collar already carries. Only after the dot is boringly reliable.

The phased build plan: phase one is location over BLE and LoRa — the hard part, taken first; phase two is the cellular nRF9160 path, lower risk and well-documented; phase three is health, heart rate and respiratory rate from the collar's accelerometer, attempted only once the location dot is boringly reliable. Below, a six-month phase-one timeline of nights and weekends: month one a proof-of-concept and range tests, month two the collar publishing GPS and battery to the cloud, month three BLE presence and the geofence-exit flip, month four base-station behaviors and an RSSI proximity beep, month five enclosure and charging that survives a wet Lab, month six a field test on Quark and a write-up of what broke.

The honest timeline

Six months, evenings and weekends, shipping delays included. Phase 1 only — cellular and health are next year's problem.

MonthGoal
1Wio L1 two-pack PoC — one on Quark, range tests around the neighborhood and on a trail, GPS → phone map
2RAK4631 collar publishing GPS + battery into the existing IoT Core stack
3BLE home-presence + geofence-exit flip to findable mode; the power budget that makes "months" real
4Base station behaviors: home listener, grab-and-go gateway, RSSI proximity beep
5Enclosure + collar mount + charging; survive a wet Lab
6Field test on Quark; write up what broke

Step one isn't a product. It's a proof-of-concept that earns the rest of the spend — before I commit to building a collar, I want to know the range and the GPS-to-phone loop actually hold up in my yard and on my trails.

Next: a scorecard, not a vibe

Which is exactly why the next post won't be "look, a dot moved." It'll be a test plan with a scorecard — range, time-to-first-fix, battery drain, in-house presence reliability — so the decision to build the real collar is made on numbers, not excitement. The boxes land tomorrow. The notebook turns into a real build log from here — the parts that work, and the parts that don't.

A decision scorecard on a clipboard: five weighted criterion rows, four passing with green checks and one failing with a red X, feeding a fork that splits into an open gate (go, build the collar) and a closed gate (hold, reconsider). Part 04 of 08
IoT Pet Health Tracker — Build · part 04
May 26, 2026

A Scorecard, Not a Vibe: How I'll Decide Whether to Build the Collar

The proof-of-concept hardware is here, so the temptation is to post a screenshot of a dot moving and call it a win. Instead, here's the scorecard — range, fix time, battery, presence reliability — with pass/fail targets, so the decision to build the custom collar gets made on numbers, not adrenaline.

The Wio Tracker L1 two-pack landed, which means I could post a screenshot of Quark as a dot on a map and call the proof-of-concept a success. That would be a lie of omission. A dot moving proves the radio turns on. It doesn't tell me whether this architecture survives my neighborhood, my trails, and a battery I don't want to charge every other day.

So before I spend another dollar on the custom RAK collar, the PoC has to earn it — against a scorecard with numbers I set before I started, so I can't move the goalposts when I get excited.

What landed

It doesn't show up as a finished gadget. Each unit is a flat-pack: a board, a 3D-printed enclosure in two halves, a 3000 mAh lithium pouch, a GPS patch antenna on a u.FL lead, a stubby LoRa whip, and a baggie of screws. No soldering and no firmware — but some assembly, which is honest about what this stage actually is.

Wio Tracker L1 unboxed on a work mat: a 3.7-volt 3000 mAh lithium pouch cell, the Wio-SX1262 board with a stubby LoRa antenna and a GPS patch antenna on a u.FL lead, a 3D-printed handheld enclosure, and a bag of assembly screws.

Wired up before I closed the case, it's exactly the bill of materials from last post: the nRF52840 and the Wio-SX1262 LoRa radio on one board, the L76K GPS antenna on its pigtail, the LoRa antenna on an SMA bulkhead, the battery on a JST plug.

The Wio Tracker L1 wired up before closing the case: the nRF52840 plus Wio-SX1262 board with the L76K GPS patch antenna on a u.FL pigtail, an SMA LoRa antenna connector, and the 3000 mAh battery on a JST lead, resting in one half of the printed enclosure.

Put a ruler against the enclosure and the verdict writes itself: about 10 cm tall. That's a fine handheld — clip it to a belt, drop it in a truck cradle — and obviously not something a cat wears. Which is the split this whole series keeps circling back to: the base station you can buy off the shelf; the collar you still have to build small. This unit is the base. The collar is the custom job behind it.

The Wio Tracker L1 enclosure against a centimeter ruler — roughly 10 cm tall, clearly handheld-sized rather than collar-sized.

The other half of the enclosure with a ruler for scale, about 6 cm across — a reminder that this unit is the handheld base station, not the wearable collar.

The setup

Two nodes: one rides Quark (call it the collar), one stays with me (the handheld), phone paired over Bluetooth with the Meshtastic map open. That's the whole architecture in miniature — collar beacons its position over LoRa, handheld receives it, phone draws the map.

Two-node proof-of-concept setup: a collar node riding Quark beacons its GPS position over LoRa to a handheld node that stays with me; the handheld is paired to a phone over BLE, and the phone draws the position on a Meshtastic map.

Getting it on the air — and what bit me

Two units, a foot apart, both powered on, both completely deaf to each other. Meshtastic ships with the LoRa region unset, and an unset radio won't transmit — legally it can't, because it doesn't yet know which band and duty cycle it's allowed to use. Set Region = US on each and they found each other instantly. First lesson, and a good one for any radio project: "powered on" is not "transmitting."

Then the collar looked dead anyway. It was in the node list, but its "last heard" read thirty minutes — no fresh beacons. Two defaults conspire there: the position broadcast interval ships at roughly 15 minutes, and smart positioning only beacons when the node has moved far enough. A collar sitting still on a bench is, by design, silent — wonderful for battery life, useless for a range walk, where I need a steady heartbeat to catch the exact second the link drops. So I forced it: fixed 1-minute interval, smart positioning off. Now it chirps every 60 seconds whether it's moved or not.

I rehearsed the signal before trusting it. Powered the collar off — "last heard" climbed and stuck: one minute, two, four. Powered it back on — it snapped to "now" within ten seconds. That climb-and-stick versus snap-back is the whole language of the range test: heartbeat alive, or heartbeat gone stale.

The surprise: my block was already a mesh

Here's the part I didn't see coming. The first time the app finished scanning, my node list had 41 nodes in it. I'd added two. The other 39 were my neighbors.

Meshtastic's default channel is a public commons — every node within radio earshot shows up. Thirty-nine of them around my block, most reachable only over two hops, which means my little handheld's signal was already being relayed across the neighborhood by strangers' nodes I'll never meet.

That cuts two ways. First, it's a problem for an honest range test: if a neighbor's node rebroadcasts my collar's beacon, my handheld keeps "hearing" the collar long after the direct link is dead — and I'd be measuring the neighborhood's mesh instead of my own radio. The fix was to move my two nodes off the public commons onto their own named channel. Because Meshtastic derives the radio's frequency slot from a hash of the channel name, renaming it also lifts the pair onto their own frequency, clear of the public traffic — then I switched the node view to a direct-only (zero-hop) filter, so the only thing I count is what my handheld hears firsthand.

But step back from the annoyance and that accidental discovery is the moat I sketched in the PRD. The reason a crowd-sourced tracker beats a pure-cellular one is the network effect: every unit sold becomes a relay that improves recovery for everyone — the same model behind Apple's Find My and Amazon Sidewalk. I'd assumed I'd have to bootstrap that network from zero, one sale at a time. Instead, 39 nodes of it already exist within a few hundred meters of my front door, before I've shipped a thing. The mesh isn't a someday feature on a roadmap. On my street, it's already on the air.

The scorecard

1. LoRa range — the make-or-break. If this fails, nothing else matters.

SettingTarget
Open line-of-sight≥ 1.0 km
Suburban / through houses≥ 300 m
Dense foliage / trail≥ 150 m

2. GPS.

TestTarget
Time-to-first-fix (cold)≤ 60 s
Time-to-first-fix (warm)≤ 15 s
Position accuracy vs phone≤ 10 m

3. Battery — the second make-or-break. The whole pitch is "you don't charge it constantly."

TestTarget
Active beaconing → projected runtime≥ 3 days
Idle / presence mode → projected runtime≥ 2 weeks

4. Presence + link.

TestTarget
Home ↔ away detection (10 transitions)≥ 9/10 correct
BLE reconnect after walking out of range≤ 30 s, no manual fix

5. The real test — Quark. Fit in a collar pouch, watch him cross the yard in real time, and an "escape" sim: someone walks him down the block while I re-acquire him on foot with the handheld's proximity readout.

The decision gate

This is the part that keeps it honest:

  • Build the custom collar if all three range targets hold for how I'd actually use it, GPS passes, and projected active battery clears three days.
  • Reconsider if range falls short in the neighborhood — that points to a better antenna, or leaning cellular-first. If battery fails, power tuning becomes priority one before any collar work.

The decision gate as a scorecard: four criteria with pass targets — LoRa range and battery runtime are hard stops, GPS fix and presence/BLE link are required — all feeding a single AND gate. If every criterion holds for real-world use, the gate opens to build the collar; if range or battery falls short, it routes to reconsider: a better antenna, going cellular-first, or power tuning before any collar work.

Multi-hop mesh — the crowdsourced-recovery magic — I can't test yet; that needs a third node. It waits.

Next post is this same scorecard with the cells filled in, and an honest verdict. If the numbers say stop, I'll say stop — that's the difference between a build log and a highlight reel.

A range-walk diagram: a base station with an antenna at left, a dashed direct link reaching a walking figure at about 1,250 feet where the link goes stale, and faint relay nodes scattered beyond that hand the signal onward to a much greater distance. Part 05 of 08
IoT Pet Health Tracker — Build · part 05
May 28, 2026

The Range Test: 1,250 Feet Direct — and Miles Through the Mesh

Range was the make-or-break criterion on the scorecard — if it failed, nothing else mattered. It didn't fail. A zero-hop direct link held to 1,250 feet in the suburbs, clearing the bar I set before I started. And the neighborhood mesh I stumbled onto last post was already relaying beacons miles further.

Last post I wrote the scorecard before I ran a single test, so I couldn't move the goalposts once I got excited. Criterion #1 was LoRa range, flagged the make-or-break: if this fails, nothing else matters.

It didn't fail. Here are the first numbers, against the targets I set.

The walk

One node on the move (call it the collar), one node home as the base station, beaconing forced to a 60-second heartbeat so I could catch the exact second the link died — the setup from last post. I started with the link solid and walked a straight suburban line until the heartbeat went stale.

The collar controller's OLED at the start of the walk, link alive: it shows the Base-Station node heard seconds ago, with a compass arrow and distance — a healthy, fresh heartbeat.

The number: the direct link held to 1,250 feet before the heartbeat went stale and stuck — last-heard climbing to one minute, two, three, no fresh beacons. That's where I stopped.

The collar controller at the limit of the walk: the Base-Station node now reads 1250 ft away, last heard 3 minutes ago — the heartbeat has gone stale, the direct link is gone.

1,250 ft is about 381 meters. Against the scorecard:

SettingTargetResult
Open line-of-sight≥ 1.0 km— not yet walked —
Suburban / through houses≥ 300 m381 m — pass
Dense foliage / trail≥ 150 m— not yet walked —

The suburban bar — the one that matters most for a dog who gets out the front door — cleared, with margin.

Zero hops — why that number is honest

Here's the part I want to be precise about, because it's easy to accidentally cheat a range test. The app showed the base station at Hops Away: 0.

The Meshtastic app's node details for the Base-Station: Device Role CLIENT, Last heard Now, and Hops Away 0 — a confirmed direct, zero-relay link.

Zero hops means my handheld heard the collar firsthand — the radio itself reached 1,250 feet, with no neighbor's node quietly relaying the beacon and inflating the result. Last post I found 39 strangers' nodes already on the air around my block, so I deliberately moved my two nodes onto their own channel and filtered the view to direct-only. 1,250 feet is the floor: what one radio does, alone, with nobody helping.

And the mesh was already doing miles

The Meshtastic app node list with both my nodes online — Collar at 97% battery and 8 GPS satellites, Base-Station at 76% and 157 ft away in this snapshot — under a header reading "2 online / 2 shown / 39 total." The 39 is the neighborhood mesh already on the air.

Now lift the zero-hop filter off and the story changes scale. Those 39 nodes are a working mesh, and it is emphatically not local. Sort the list by distance and there's a station — Mesh_WYVERN, 33 miles (53 km) out, four hops away:

The Meshtastic node list sorted by distance: a node "Mesh_WYVERN" 33 miles away reached over four relayed hops, above a "North Point" tower node 8.2 miles out — direct evidence the mesh I'm sitting on spans tens of miles, not a city block.

That's the network, not my radio — and my own collar rides it: on earlier runs across the public channel, with beacons free to relay hop to hop, the collar's position came back from about ten kilometers out. The reach my pet's beacon can borrow is measured in miles, not feet.

Put the whole span on one axis and the gap is the story — what my own radio did, against what the mesh carried for nothing:

A logarithmic distance axis from 10 meters to 100 kilometers. My collar's direct, zero-hop link sits at 381 meters (1,250 feet), measured on the walk; far out on the same axis are real nodes pulled from the mesh log — a North Point tower node at 8.2 miles and Mesh_WYVERN at 33 miles reached over four relayed hops, roughly 140 times my direct link, on hardware I don't own.

My own radio's direct link tops out around 381 m. On the same axis, the neighborhood mesh reached North Point's tower 8.2 miles out and Mesh_WYVERN 33 miles away over four hops — about 140× my direct range, on nodes I didn't buy and didn't place. (The log even caught a node at 100 km; that one's almost certainly a misconfigured fixed position, so I'm not counting it.)

That's the moat I sketched in the PRD, and I'd assumed I'd have to bootstrap it one sale at a time. Instead it already exists, on my street, before I've shipped anything — the same network effect behind Find My and Sidewalk. Zero-hop clears the bar on its own; the mesh turns feet into miles for free. The direct link is what I can promise; the mesh is the upside I don't have to build.

And I haven't touched the antenna or the power

The 1,250 feet is conservative for a third reason: it's the stock rig. The base is running the little screw-on whip the Wio Tracker ships with — and the Meshtastic crowd is near-unanimous that it's the cheapest weak link to fix, with higher-gain antennas that clearly outrun it. It's a fiddly part to swap on this enclosure, but it's swappable. I also haven't pushed transmit power toward its ceiling.

Both are pure link budget, and link budget turns into range through one clean equation. Every decibel you add — antenna gain on either end, plus transmit power — multiplies range by:

range × 10 ^ ( ΔdB / (10 · n) )

where n is the path-loss exponent: about 3 in the suburbs (free space is 2; dense foliage pushes toward 4). Anchored at my measured 1,250 ft / 381 m:

Link budget addedMultiplier (n = 3)Direct range
+3 dB1.26×~1,580 ft
+6 dB1.58×~1,980 ft
+9 dB2.0×~2,500 ft
+12 dB2.5×~3,140 ft

Range as a function of added link budget at suburban path loss (n = 3), drawn as a bar chart anchored at the measured 1,250 feet with zero decibels added: the bars climb to about 1,980 feet at +6 dB, 2,500 feet at +9 dB, and 3,140 feet at +12 dB. A note shows the decibels come from two knobs — a higher-gain base antenna and more transmit power — and a dashed line marks the regulatory EIRP ceiling that caps how far the two can be stacked.

Where those decibels come from: trading the ~2 dBi stock whip for a ~5.8 dBi collinear is about +4 dB, and there's usually a few dB of transmit-power headroom on top — call it +8 to +10 dB on the base side, which the table turns into roughly double the direct link, a quarter-mile-plus. Two honest limits keep it from being free. US 915 MHz ISM has an EIRP cap, so you can't stack gain and power without bound. And the collar antenna can't grow — a cat won't wear a collinear — so the realistic gains live on the base side. The upside: the same decibels lift every mesh hop too, so a better base antenna stretches both the 1,250-ft direct floor and the ~10 km mesh ceiling.

I'll confirm it with a real walk once the new antenna's on. For now it's a model — but the model is just radio physics, and it's why range isn't the thing that worries me.

Messaging works too

The link isn't just position beacons. I sent a plain text from the collar mid-walk and it landed at the base station:

The base station node sitting in a window, its OLED showing a received message: "hi from collar," heard 2 minutes ago — proof the link carries text both directions, not just position pings.

Small thing, big implication for an actual escape: I can ping the collar and get a fix back on demand, not just wait for the next scheduled beacon.

Presence — the heartbeat is the signal

The two walk photos above are also the presence test in miniature: at the start the heartbeat reads seconds ago (home / in range), at the limit it climbs and sticks (away / out of range). That climb-and-stick is exactly the home-vs-away signal the scorecard asks for — clean, with no false "still here" once the radio's truly gone.

The scorecard so far

CriterionStatus
LoRa range — suburbanPass (381 m, zero-hop)
LoRa range — open LOS / foliageNot yet walked
Presence — home/away heartbeatLooks clean; full 10-transition run to come
Messaging over the linkWorks both directions
GPS — time-to-fix, accuracy— to come —
Battery — active runtime~3.5 days at periodic beaconing — pass (continuous GPS + lit screen: ~10 h)
Battery — idle / presence runtime— to come —
The Quark test (escape sim)— to come —

Where this leaves the build

The criterion that could have killed the project — range — cleared the suburban bar direct, and the neighborhood mesh already extends recovery to miles I didn't have to engineer. The build stays alive. And the other make-or-break, battery, is looking like a pass too: at periodic beaconing the collar projects to ~3.5 days on a charge — past the three-day bar — while a continuous-GPS, screen-lit stress run collapses it to ~10 hours, which is exactly why the production collar runs headless and duty-cycles the GPS. What's left is GPS fix quality, the idle/presence number, and a per-mode bench measurement to make the battery verdict airtight. Those are next.

A dense urban row-house block seen from above: a fixed base-station node at one house emits concentric radio rings that grow fainter outward, while a walking figure with the collar moves along the sidewalk and the rings around them dim with distance. A second, smaller rendering of the firmware-cache failure overlays as a stuck pin on the wrong block. Part 06 of 08
IoT Pet Health Tracker — Build · part 06
Jun 04, 2026

The City Walk: Path Loss ~4 in Dense Urban — and the Cache That Wouldn't Die

Five walks in city density before the data was real, because the collar's firmware kept broadcasting a stale coordinate while the GPS chip was happily producing fresh fixes nobody could see. Factory reset was the only thing that cleared it — and then the clean walk gave the number I'd come for: path loss ~4 in dense urban row housing, about half the suburban range, exactly what the textbooks say.

Last range test was a clean suburban line — 1,250 feet, zero hops, scorecard cleared with margin. The city test should have been the easy follow-up. Same rig, denser environment, walk the block, get the number.

Five walks later I had no number.

Not because the link failed — it didn't — but because every position the collar broadcast was a stale value baked into firmware, while the GPS chip itself was producing fresh fixes nobody could see. Once that finally cleared, the actual walk took six minutes and gave me the headline I came for: path loss exponent of about 4 in dense urban row housing, roughly half the suburban range — exactly what dense-urban LoRa literature predicts.

The bug is the interesting half of the post.

The rig and the plan

Same two Wio Tracker L1s as the suburb test, US 915 MHz, LONG_FAST preset, 30 dBm TX, hop limit 3. One stays home as a base station with a manually-set fixed position. One walks with me as the collar, GPS on, broadcasting position every 30 seconds and a range-test seq packet every 15. A small Python listener on the base writes each received packet to a CSV with timestamp, sender lat/lng, receive SNR, and a great-circle distance computed against the base's known coord.

That last detail is important: by computing distance on the receive side from a coord the base knows is correct, the SNR-vs-distance curve isn't at the mercy of whatever the collar's firmware claims about itself. Or so I thought.

The first four walks — same stuck coordinate, every time

Every position broadcast from the collar carried the same lat/lng, to the bit: 40.2953856, -75.6246912. Every one. Across multiple walks, across reboots, across a full power-cycle. That coordinate is about 3 km from where the collar was actually standing — a stuck value from another part of the city the collar was never near.

When the same firmware emits the same precise float across hours of broadcasts and a power loss, the value isn't from a sensor. It's coming out of flash.

I had a working theory the moment I saw it: somewhere in the device's persistent storage, the Meshtastic firmware keeps a "last broadcast position" that the POSITION_APP plugin pulls from when emitting a packet, separate from whatever the GPS chip is currently reporting. If that cache gets corrupted with a stale value, the broadcast keeps shipping it even when the chip downstairs has a perfectly good live fix.

But the theory needed a level of access the tool in my pocket couldn't give me. The diagnostic moved up a ladder, one walk at a time.

Each tool got me one layer closer

Walk 1 — the Meshtastic Android app in my pocket. Confirmed the link was alive: collar in the node list, hop count, a position. Small screen, the position display sits behind a default-imprecise grid square, and I was walking — I noticed something was off about where the collar said it was, but I didn't slow down to look hard.

The Meshtastic Android app on a phone, Nodes screen, showing two cards heard "Now" — Collar (COLR, 96% / 4.14 V, SEEED_WIO_TRACKER_L1, CLIENT role) and Base-Station (BASE, 100% / 4.19 V, altitude 30 m MSL). The header reads "Nodes — 2 online / 4 shown / 13 total," and a third card peeks below for someone else's amateur-callsign node, N0JUH-0, heard yesterday. This was the only view of the mesh on walk 1: a small screen in a pocket, enough to confirm the link was alive and not much more.

Walk 2 — same app, phone mirrored to the laptop side-by-side with a map. Bigger screen, the "wait, that's not where I am" feeling sharpened into a specific question. The Range Test module's config screen is also a clean tell of what the app can and can't do — there's a "Save .CSV in storage" toggle right there, greyed out, with "(ESP32 only)" next to it. The Wio Tracker L1 is nRF52840, so on-device CSV logging is off the table. That's the first time on this walk I knew I'd have to log the test from somewhere outside the app — still GUI, still abstracted from the firmware fields I actually needed to see.

The Meshtastic Android app rendered inside Microsoft Phone Link on a Windows laptop — the window title bar reads "LUKE's S24 Ultra." On screen is the Range Test module config: a "Range test enabled" toggle (currently off), a "Sender message interval (seconds)" dropdown showing "Unset," and a "Save .CSV in storage (ESP32 only)" toggle greyed out — the on-device CSV save is gated to ESP32 boards, which the nRF52840-based Wio Tracker L1 isn't. The walk-2 takeaway sits in that one parenthetical: I'd have to log the test from somewhere else.

Walk 3 — the Meshtastic Web Client over Bluetooth. Genuinely cool moment: a Chrome tab, a Web Bluetooth handshake, the device's config tree in a browser, no driver install or USB cable. Caveat worth knowing: one device at a time — Web Bluetooth pairs one peripheral per session, so I had to disconnect and reconnect to swap between collar and base. Useful enough to confirm the fixedPosition flag was already off on the collar and the broadcast still carried the stuck coord, which ruled out the obvious cause and made the bug feel deeper.

The Meshtastic Web Client at client.meshtastic.org with the "Add connection" modal open. Three connection types are tabbed across the top — HTTP, Bluetooth, and Serial — with an HTTP form below it asking for a node name and URL or IP. A toast in the lower right confirms a prior Bluetooth-paired device (BT: BASE_e631) was just removed. Zero install, zero driver, no app store — just a web app reaching the radio over the browser's standard wireless and serial APIs.

The browser's native Web Bluetooth pairing prompt, headed "client.meshtastic.org wants to pair." A scanning list shows one in-range Meshtastic device, BASE_e631, marked "Paired," with Pair and Cancel buttons at the bottom and a "Scanning…" label. This OS-mediated dialog is the layer browser automation tools like Playwright can't reach — it sits outside the page DOM and only a real human click can advance it.

The Meshtastic Web Client showing the full mesh node list once paired over Bluetooth. The left sidebar identifies the currently-connected device as BASE, with battery 100% / 4.19 V and firmware 2.6.10. The main table lists seven nodes with last-heard, SNR, hop count, model, and MAC — my Base-Station and Collar both directly connected as SEEED_WIO_TRACKER_L1s, alongside several neighborhood-mesh nodes a few hops out, the same neighbor mesh the previous range-test post called out. The whole network, in a browser tab, no install required.

Walk 4 — the Meshtastic Python CLI (pip install meshtastic) over USB serial. This is the rung where the bug became visible. The CLI reads the device's raw protobuf, prints every field of the local node dump including cache values the GUI doesn't expose, and lets me query the base's view of the collar separately from the collar's view of itself. That's the comparison the smoking gun depends on; the GUI couldn't make it.

The progression isn't accidental. Each tool ships its own view of the device — the app shows you what an end user needs, the Web Client shows you what a tinkerer needs, the CLI shows you what a developer needs. When the bug lives in a field none of the user-shaped views expose, you have to keep climbing until you can see it. There's one more rung beyond the CLI in this story — but that one comes later, and it's about a different bug.

The smoking gun

The cleanest evidence: the collar's own meshtastic --info and the base's view of the collar disagreed.

latitudelongitudealtitudesource
Collar self-view (chip output)40.272606-75.614714824 mLOC_INTERNAL (fresh GPS)
Base's view of collar (received broadcasts)40.2953856-75.624691224 mLOC_INTERNAL (claimed fresh)

Read that altitude column twice. The broadcasts were arriving with fresh altitude every time. They were marked LOC_INTERNAL, the firmware's flag for "this came from my GPS chip just now." Time field, fresh. Lat/lng — locked. Same value across two hours of walking and power events.

Whatever path the firmware uses to assemble an outgoing position packet, the altitude and time fields are getting pulled from the live GPS, and the lat/lng fields are getting pulled from a different place — somewhere persistent, somewhere that doesn't update when the chip does, and somewhere --remove-position and --reboot and full unpower don't reach.

A diagram of the collar's position-broadcast path. The L76K GPS chip on the left feeds fresh lat, lng, altitude, time into a circular firmware buffer marked "live position." From that buffer, three small arrows route altitude and time correctly into the outgoing POSITION_APP packet on the right. A fourth arrow for lat/lng is broken with a red ✗ — instead, lat/lng is pulled from a separate flash-backed box labeled "broadcast cache (corrupted)" containing the stuck coord 40.2953856, -75.6246912. The cache box is marked as surviving --remove-position, --reboot, and full power cycle. The packet leaves the device with a mix of fresh and stale fields.

What didn't clear it

The depressing list, in the order I tried them:

  • meshtastic --port COM5 --remove-position — clears the fixedPosition config flag and zeros fixedLat/fixedLng. Does not touch the broadcast cache. No effect on the stuck coord.
  • --reboot — software reboot. Cache survives.
  • Full power-cycle (unplug USB, hold 30 s to drain capacitors, replug). Cache survives.
  • Toggling position.gps_mode, gps_update_interval, position_broadcast_smart_enabled. Cache survives.
  • Reading the cache via --get position to inspect it directly. The field isn't exposed in the public config surface — there's nothing to set.

I confirmed the GPS chip was healthy in the middle of all of this — it briefly reported a clean indoor fix at 40.2719211, -75.6144608 while I was watching, then proceeded to broadcast 40.2953856 thirty seconds later. The chip was telling the truth the whole time. The firmware just wouldn't repeat it.

What did clear it

meshtastic --port COM5 --factory-reset. Bluntest hammer in the box: it wipes the channel config, the owner name, the LoRa settings, the position settings, and the persistent broadcast cache. Cost is about ten minutes to rebuild — re-import the channel URL, set owner name, push LoRa region and preset and TX power and hop limit, push the GPS interval and the broadcast cadence. The configs live in YAML in this repo so the rebuild is a single --ch-set-url plus a chain of --set calls.

Except — and here's the fifth rung I promised earlier — in meshtastic Python CLI 2.7.8, --factory-reset itself is broken. The flag dispatches an AdminMessage whose factory_reset_config field expects an integer (1 for soft, 2 for hard), but the CLI assigns the Python True. The protobuf encoder catches it and aborts with Expected an int, got a boolean. Different bug, same shape of fix: drop one layer further down, from the CLI to the Python API the CLI is built on. Twelve lines: open a SerialInterface, build the admin message with factory_reset_config = 1, send via localNode._sendAdmin(). First try. The CLI flag was the wrong wrench for a fastener the API ships in the same toolbox; the device-side firmware is perfectly happy as soon as you hand it the integer it actually wanted.

After the reset, the broadcast cache was gone. The next --info from the base showed the collar reporting 40.2714555, -75.6146695 — a real coord, near my house, varying between consecutive readings the way GPS jitter is supposed to. End-to-end: fresh.

The walk that worked

About six minutes, down the block east and back, holding the collar at shoulder height. Eleven position broadcasts with fresh varying coords, plus the seq packets every fifteen seconds. Distance and SNR pulled from the live capture:

TimeDistanceSNR
19:57:27377 m+6.0
19:57:45367 m+5.5
19:58:05268 m+6.25
19:58:25310 m+6.0
19:58:45303 m+3.5
19:59:05303 m+5.5
20:00:05363 m+4.5
20:00:25379 m+5.75
20:01:05379 m+2.75
20:01:25463 m+3.75
20:01:46486 m−4.0

The seq packets after the last GPS broadcast kept arriving with SNR slipping further: −3.75, then −6.75, then −9.5 — about one decibel above where LONG_FAST loses lock. If I'd walked another thirty seconds I'd have caught the first dropped packet.

An SNR-versus-distance scatter for the clean dense-urban walk. The x-axis runs from 200 m to 600 m in meters; the y-axis runs from -10 dB to +10 dB SNR. Eleven dots mark the fresh-GPS position broadcasts, clustering near +5 to +6 dB from 268 m through about 380 m, then dropping sharply to -4 dB at 486 m. A dashed model curve fitted through the points slopes downward, labeled "path loss exponent n ≈ 4." A faint horizontal band near -10 dB marks the LoRa LONG_FAST sensitivity floor, with a small annotation noting the seq-packet tail kept arriving past 486 m, slipping to -9.5 dB before I turned around.

Path loss — the number

Plugging the closest and farthest fresh-GPS fixes into the standard log-distance model:

SNR(d₀) − SNR(d) = 10 · n · log₁₀(d / d₀)

With (d₀, SNR) = (268 m, +6.25 dB) and (d, SNR) = (486 m, −4.0 dB):

10.25 = 10 · n · log₁₀(486 / 268) = 10n · 0.2585

n ≈ 3.97 — call it 4.

That's not arbitrary. Path-loss-exponent tables for 900 MHz consistently put dense urban / row-house environments at 3.5 to 4.5, suburban at 3 to 3.5, free space at 2. My suburb walk in post 5 was calibrated against n = 3 and I extrapolated from a 1,250 ft direct number — that math holds for that environment. Dense urban density is roughly a decibel-per-meter harsher: brick walls, narrow row-house canyons, parked cars at antenna height, no clean line-of-sight past about a block.

Anchored at this walk's measured limit (SNR hitting LoRa's sensitivity floor at roughly 600 m given the tail slope), the direct-range ceiling in this density is somewhere around 600 to 900 m — half to two-thirds of the suburban result, exactly what the path-loss math forces. The mesh-relay extension I found before still applies — there are neighborhood Meshtastic nodes the collar can see from any given block, and a beacon that doesn't hit the base direct can still get there in two hops via somebody's roof.

Against the scorecard

The original scorecard split the LoRa range bar into three environments, and this walk fills in the dense-urban row:

CriterionTargetResult
LoRa range — open line-of-sight≥ 1.0 kmNot yet walked
LoRa range — suburban≥ 300 m381 m direct — pass (post 5)
LoRa range — dense urban / row housing≥ 300 m~500 m measured, ~600–900 m extrapolated — pass
LoRa range — dense foliage / trail≥ 150 mNot yet walked

Two of four environments cleared. The bar I cared about most for an escaped pet in a city — a block-plus of clean direct range through row houses — is met.

What I'd tell a team

Two things.

One. When a device's own self-view and the wire output disagree, the bug is almost always in the assembly path between sensor and packet, not the sensor itself. The altitude field being fresh while the lat/lng was stale was the find: it told me which fields lived where in firmware memory, and which path was broken. If both had been stale, I'd have spent another two hours suspecting the GPS chip.

Two. Persistent state survives more than people expect. --reboot is not a state reset. Power-cycle is not a state reset, for flash-backed values. Factory-reset is the only thing in the Meshtastic admin surface that wipes the whole flash-backed config, and in this case it was the only thing that mattered. The lesson for the production collar is to expose a deliberate "clear all persistent caches" operation in the device's own UI — not bury it behind a destructive reset that also wipes the channel and the owner. If users ever hit this in the field, they need a recovery path that doesn't unjoin them from their mesh.

What's next

Two open scorecard rows after tonight: GPS time-to-fix and accuracy (which I now actually trust the broadcasts to report, post-reset), and the Quark test — the escape simulation where I hand the collar to my Lab pup, open the back door, and time how fast the system says "the dog is gone" versus how long until it reads "the dog is back." That's the demo that matters.

The configs and the scripts from tonight are in the repo if you want to run the same walk: the collar YAML, the base YAML, the Python listener for live capture, and the twelve-line factory-reset workaround for the CLI bug. The CLI flag will get fixed in a release — the workaround is a today thing, not a forever thing.

An aerial view of a residential block at dusk: several houses emit concentric radio rings of varying intensity from their Echo and Ring devices, the rings overlapping into a patchwork of coverage strength. A small tracker collar sits inside a strong coverage zone near the middle of the block, while a second collar at the block edge sits in a quiet gap between rings, signal not reaching. The contrast between dense overlap and gap is the visual point. Part 07 of 08
IoT Pet Health Tracker — Build · part 07
Jun 06, 2026

Auditing the 95%: A Data-Driven Look at Amazon Sidewalk Before I Bet a SKU on It

Amazon claims Sidewalk reaches 95% of the US population. I'm about to bet a pet-tracker SKU on it, so I sat down with the math: bridge counts, urban-vs-rural distribution, geographic coverage, and the AWS IoT cost stack. Some of it holds up. Some of it is marketing dressed in a percent sign. Here's the audit, then the data path my collar's data would actually take through AWS — wired up before any hardware arrives so I'm not fumbling the integration on Day 1.

The cellular module for the Phase 2 build is sitting in a warehouse in China for three weeks while it makes its way to my workbench. Which gives me time to test: Amazon Sidewalk — does it actually reach the places I'd want a pet tracker to reach?

Amazon's marketing line is "approximately 95% of the US population is covered" — a number Amazon updates roughly annually as the bridge count grows. That's the number that justifies betting an entire product SKU on the Sidewalk tier, the way Tile did, the way Pebblebee did. So before I commit, I'm going to count.

What follows is the audit. Bridge math, coverage math, AWS-side cost math. Then a tangent on the AWS IoT integration path my collar's uplinks will actually take — because waiting for hardware is a good time to teeth-cut on the cloud side.

The 95% claim, dissected

The headline number traces to Amazon's Sidewalk launch press materials and the coverage.sidewalk.amazon public map. The claim is "approximately 95% of the US population is within Sidewalk coverage" — measured by counting Sidewalk-enabled Amazon devices (Echo, Ring) and computing which households fall within their effective sub-GHz / BLE radius.

Two words matter in that sentence, and they're the ones the marketing doesn't unpack. The phrase "approximately 95% of the US population" turns on that last word: population.

It's a population number, not a geographic number. And it's an eligibility number, not a reliability number. Both of those distinctions are the difference between "this will work for my customers" and "this will sometimes work for some of my customers."

Let's count the bridges

Public estimates put the US Echo install base at ~95–110 million devices as of mid-2026, with Ring devices adding another 12–15 million. Tile, since the Life360 acquisition completed and Sidewalk integration shipped, contributes another estimated 25–35 million bridge-eligible devices. Round numbers, but they bracket the order of magnitude.

SourceEstimated US bridges (M)Bridge type
Amazon Echo95–110sub-GHz LoRa + BLE + FSK
Ring (Doorbells + Floodlight Cams)12–15sub-GHz + BLE
Tile (post-Life360 / Sidewalk)25–35 (eligible — actual participation lower)BLE only
Other certified third-party Sidewalk bridgesunder 5mixed
Total potential bridges140–165 M

That's a lot of bridges. Divided by ~129 million US households, that's roughly one bridge per household on average. Translated to area:

  • Dense urban (NYC, SF, Chicago): thousands of bridges per square mile
  • Suburban (Atlanta sprawl, San Jose, Houston suburbs): dozens per square mile
  • Exurban / small-town: single digits per square mile
  • Rural (most of MT, WY, NV, AK): approaching zero

The aggregate number is huge. The distribution is what kills the marketing claim for any specific use case.

Population covered is not land covered

The US is ~3.8 million square miles. The US is also ~333 million people. Those two numbers don't track each other.

Here's the actual Amazon Sidewalk coverage map for North America, screenshot today from coverage.sidewalk.amazon:

Amazon's official Sidewalk coverage map for the continental US, southern Canada, and northern Mexico. Areas where Sidewalk bridges are deployed are shown in dark navy blue against a pale beige basemap; uncovered areas remain pale. Dense navy fills the Northeast Corridor solidly from Boston down through New York, Philadelphia, DC, and Baltimore; another dense band runs through Chicago and the upper Midwest into Ohio and Michigan; the Southeast from Atlanta down through Florida is heavily filled; the Texas triangle Dallas-Houston-Austin-San Antonio is solid; California shows two heavy clusters at the Bay Area and LA basin, plus Seattle and Portland in the Pacific Northwest. In contrast, most of Montana, Wyoming, Nevada, western Texas, the Dakotas, large stretches of the Great Plains, and most of northern Canada are nearly empty of coverage. The map tells the population vs land story in one image — almost every populated metro is covered, almost no rural area is.

~80% of Americans live in urban areas, which occupy ~3% of the land. The other 20% — roughly 67 million people — are scattered across the remaining 97% of the country. So when Amazon says "95% of the population is covered":

  • ~315 million people are inside coverage
  • ~18 million people are outside coverage
  • The 18 million sit in the 3.7 million square miles of low-density geography

A US silhouette split into two visualizations side by side. On the left, a population pie reads 95% covered / 5% uncovered, with the covered slice marked 315 million people and the uncovered slice marked 18 million. On the right, the same US silhouette is shaded by Sidewalk bridge density — dark green dots clustered tightly in urban metros (Northeast corridor, Bay Area, LA, Chicago, Atlanta, Dallas, Houston, Miami), thinning rapidly across the Midwest and Mountain West, and disappearing entirely across most of Wyoming, Nevada, Montana, Alaska, and large swaths of the Plains. A caption notes that 95% of the people is roughly 5% of the land — and dogs roam the land, not the census tract.

That difference matters because dogs don't read census data. A dog that lives in San Francisco but escapes during a camping trip to the Sierra has just walked out of 95% coverage into 0% coverage, in one weekend. A retriever owned by a couple in suburban Pennsylvania near a state forest crosses the coverage edge every time it chases a deer.

A pet tracker that's "covered" in your driveway but "uncovered" at the dog park three blocks away ships exactly zero units to anyone who cares about finding their dog.

What coverage actually means

Even within the 95% population, "covered" is a more generous word than the marketing implies. There are four ways a bridge can be in your neighborhood but not bridging your packets:

  1. Powered off. Echo Dots get unplugged. Spare bedrooms get cleaned out. Estimated unplug rate is somewhere between 10–20% of installed devices at any given time. The coverage map doesn't deduct for this.

  2. Sidewalk opted-out. Amazon shipped Sidewalk with default-on in 2021, and gave users an explicit opt-out toggle. Estimated opt-out rate is in the 15–25% range of US Sidewalk-eligible households (no official number; press coverage and forum sampling). The coverage map shows potential, not actual.

  3. WiFi-dead / backhaul-dead. Sidewalk uses your home WiFi to forward packets to Amazon's cloud. If the Echo has lost WiFi (router rebooted, ISP outage, the kid changed the password), the radio still works locally but the packets go nowhere. The collar gets an ACK. The owner gets nothing.

  4. RF-shadowed. Bridges behind metal kitchen cabinets, basement Echos behind brick, Ring cameras under a metal awning — all sit in the device count but contribute little real coverage.

Multiply: even within the 95% population number, the effective real-world coverage is meaningfully lower. A reasonable internal estimate for "your packet actually reaches Amazon's cloud from this point on the map" is probably 60–75% of the headline number — which puts the real US household-level reliability at something like 57–71% of households when accounting for all four failure modes.

That's still huge. It's still better than any other crowdsourced network in the US. But it's not 95%. And the marketing doesn't tell you which 30–40% you're in until you ship.

So why am I still betting on it?

Because the alternative is worse, and the math at scale still works out.

Cellular comparison

Cellular LTE-M / NB-IoT (the comparison tier) gives you 99%+ population coverage and ~30% land-area coverage in the US — both better than Sidewalk on every axis. So why not just ship cellular?

TierCoverage (pop.)Cost/month (per device)One-time hardware $
Sidewalk57–95% (depending on what you count)~$2–7 (AWS)$0 (rides Amazon infra)
LTE-M / NB-IoT99%+$5–10 (Hologram / 1NCE / Soracom)$30 module + $1–10 SIM
LoRaWAN (Helium / TTN)under 30% (US)~$1 (data credits)$0 (rides community infra)

Sidewalk's value isn't best-in-class coverage. It's "good enough" coverage at zero recurring cost to the manufacturer, in a country where the customer base skews urban/suburban (which is where Sidewalk is strongest). For a $79 collar with no monthly subscription as the product pitch, Sidewalk hits a price point cellular literally cannot, while covering the segment of the population that's most likely to buy a $79 collar in the first place.

What I'd ship

In the three-SKU plan, Sidewalk is the middle SKU — the "rides Amazon's infrastructure, no subscription, works for urban/suburban customers" middle tier between the LoRa-mesh community SKU (lowest cost, geographic gaps) and the Cellular SKU (highest cost, universal coverage). That's a defensible product matrix. Sidewalk doesn't have to be 95% reliable to be the right answer for the right customer. It just has to be honest about which customer.

AWS-IoT data path

When the cellular module finally lands and I start running real Sidewalk tests in three weeks, the cloud-side plumbing has to already be in place. Here's the path a single position broadcast takes from the collar to "the owner's app shows a pin on the map":

A flow diagram showing the Sidewalk-to-AWS data path. From left to right: a small collar device emits a packet labeled "POSITION_APP" via BLE + sub-GHz. The packet arrives at an Echo or Ring device drawn as a small speaker, labeled "Sidewalk Bridge". From there, an arrow crosses an "Amazon-side" boundary into a cloud labeled "Amazon Sidewalk Network Server (ASNS)". A second arrow then crosses into a "Your AWS account" boundary, into a cloud labeled "AWS IoT Wireless (IoT Core for Amazon Sidewalk)". Inside that cloud, the packet flows through three sub-boxes in sequence: a "Wireless Device Profile" routing rectangle, a "Destination" rectangle, and an "IoT Topic Rule" labeled with SQL syntax. The output of the IoT Topic Rule fires a Lambda function on the right, which writes to a DynamoDB table and pushes a notification to the owner's phone via SNS. Small annotations on each segment note who controls that segment: Amazon-side is "Amazon's infrastructure, you don't pay or manage"; AWS account side is "your AWS bill, your IAM roles, your code".

The breakdown of what I need to set up before any uplinks fly:

  1. AWS account + region. Pick us-east-1. AWS IoT Core for Sidewalk runs in us-east-1 and us-west-2; everywhere else and you're routing across regions for no reason.

  2. Wireless Device Profile. Tells AWS what kind of device this is (Sidewalk-spec, US915 band, your manufacturer info). One profile, reusable across every device I ship.

  3. Destination. The routing rule that says "uplinks from this profile go to this IoT Topic." Single destination per product line.

  4. IAM Role. Lets IoT Wireless actually invoke whatever you've pointed the destination at. Annoying to debug if you skip it; the API silently swallows the failure.

  5. IoT Topic Rule with a SQL filter. Routes the message to a Lambda function. SQL syntax looks like SELECT *, topic() as topic FROM 'iot/wireless/sidewalk/+/uplink'. The + is the wildcard for device ID.

  6. Lambda. Reads the base64 payload, decodes the binary, extracts lat/lng/timestamp/SNR, writes to DynamoDB, fires an SNS notification if the dog is outside its geofence.

That's the minimum. Total AWS setup time, having done this once before: ~30 minutes. The first time you do it, ~3 hours, because every tutorial assumes you know the difference between an IoT rule (the SQL filter) and an IoT destination (the routing endpoint), and they're different things with similar names.

The cost math at pet-tracker scale

Cloud costs are what eventually decide whether your "no subscription" product story is actually sustainable, so let's count them too. AWS IoT Wireless billing is roughly:

  • Per-device fee: $0.10/device/month (Sidewalk-registered)
  • Per-message uplink: $0.0008 per message
  • IoT Rules executions: $0.15 per million
  • Lambda: ~$0.0000002 per ~10 ms invocation
  • DynamoDB writes: ~$0.25 per million write capacity units

For a single pet collar at three reasonable ping rates:

Ping intervalMessages / monthCost / month / device
1 / minute (active tracking)~43,200$34.66 — way too much
1 / 5 minutes (battery-saving active)~8,640$6.96 — competitive with cellular
1 / 15 minutes (idle / sleep)~2,880$2.40 — genuinely cheap
Hybrid: 1 / 15 min sleep + 1 / 1 min when accel-motion-triggered~6,000–10,000$5–8 — the right answer

A horizontal bar chart titled "AWS IoT cost per Sidewalk device per month, by ping rate." Five horizontal bars represent ping intervals 1/minute, 1/5min, 1/15min, hybrid (motion-triggered), and a comparison "cellular LTE-M baseline" bar. The 1/minute bar extends furthest at $34.66, marked in red and labeled "unsustainable for a no-subscription product." The 1/5min bar at $6.96 is marked teal and labeled "competitive with cellular." The 1/15min bar at $2.40 is marked dark green and labeled "genuinely cheap." The hybrid bar at ~$6 is marked teal and labeled "the right answer." The cellular baseline bar at $5-10 is shown as a translucent reference band overlaying the upper portion of the chart. A small footnote reads "Costs include: AWS IoT Wireless per-message $0.0008 + per-device $0.10/month + Lambda/DynamoDB write costs at this scale (~$0.10/month negligible)."

The hybrid mode is the answer. Sleep at 15-minute intervals when the accelerometer says "the dog isn't moving." Spike to 1-minute intervals when the 6-axis IMU detects motion (running, escaping, chasing). Average somewhere in the $5–8/device/month range. That's competitive with cellular but with no SIM to manage and no carrier relationship.

And for the customer experience: ~1 km of urban coverage at 1-minute resolution when the dog is moving, dropping to ~15-minute resolution when the dog is asleep on the couch. The accelerometer earns its place in the BoM all over again.

What I'm going to verify when hardware arrives

The audit-driven hypothesis list, things I'll test on the Sidewalk Starter Kit in the next few weeks:

  1. Does my house actually have a working Sidewalk bridge nearby? The coverage map says yes. The reality I'll measure by registering the dev kit and watching whether uplinks land.
  2. What's the actual roundtrip latency, packet-to-Lambda? Marketing implies "near real-time." I bet it's actually 5–30 seconds, and bursty.
  3. What's the daily packet loss rate? Single-device samples in dense urban can hit 5–10% over 24 hours. I'll find out for my specific location.
  4. Does coverage actually exist on the dog-walk route? Static map says yes; walking the route with a counting device will say definitively.
  5. What's the bridge-density gradient on a 5 km drive away from the house? Specifically, where does the link die?

Those are the five numbers that decide whether Sidewalk graduates from "interesting product tier" to "shippable product tier" in this build.

What's next

The cellular hardware lands in ~3 weeks. The AWS-side plumbing should be wired up before then. The Sidewalk Starter Kit lands in ~2 days and I can start running coverage tests immediately. Next post is the actual results, with numbers instead of estimates.

If you want to follow along: the bridge density in your own neighborhood is at coverage.sidewalk.amazon. Worth checking before you bet a SKU on the network.

A battery cell on a baseline with two discharge curves leaving its top — one plunging steeply to empty close by, one gliding gently across the full width before it lands — the same cell, two very different lifetimes. Part 08 of 08
IoT Pet Health Tracker — Build · part 08
Jun 21, 2026

The Battery Test: Runtime Is a Dial, Not a Number

Range could have killed the project; so could battery. The whole pitch is a tracker you don't charge constantly — so I let both nodes run for a day and read their own telemetry. The collar lasts three to four days beaconing, and ten hours with the GPS pinned on and the screen lit. Same board, same cell. Runtime turned out not to be a number at all.

The range test cleared its bar, which left the other criterion that could quietly kill this whole thing: battery. The pitch I wrote in the PRD was a tracker you don't charge constantly — and a tracker you charge every other day is a tracker that ends up dead in a drawer. So the scorecard set two hard numbers: ≥ 3 days of active beaconing, ≥ 2 weeks in idle/presence mode.

Reading the battery without a meter

I expected to babysit a multimeter for this. I didn't have to. Meshtastic logs Device Metrics on every node — battery %, voltage, channel utilization, air utilization — on an interval, and the app graphs them. So I just left both nodes running and read their own telemetry back. Two runs worth comparing: the range-walk day, where I had everything cranked, and a plain ~24-hour sit afterward.

The collar — the unit that actually has to last

The collar's Device Metrics screen over 24 hours: battery 73% at 3.92 V, uptime 22 h 56 m, channel utilization 7.1% and air utilization 3.7%, with the battery line sloping gently down from the high-90s across the day.

After 22 hours 56 minutes of uptime the collar read 73% at 3.92 V, having started the day in the high-90s. That's roughly 1% per hour, which projects to 3.5–4 days on a charge — and at the 72-hour mark it still has about a quarter of the battery left. It clears the three-day bar with margin. Channel utilization sat at 7.1% and air at 3.7%, well under the levels where the mesh starts throttling, so it isn't burning the budget fighting for airtime either.

…and then the same collar lasted ten hours

Here's the honest counterweight. On the range walk, with the position interval forced to 60 seconds and the OLED lit the whole time, the same unit went from 97% to 66% in three hours — about 10% an hour, ~10 hours to empty. Same board. Same 3000 mAh cell. Eight times the drain, set entirely by how hard I was working it.

So the battery doesn't have a runtime. It has a runtime per mode:

A chart of battery percentage versus time. A steep red line — continuous 60-second GPS plus a lit screen — falls from full to empty in about ten hours, through a measured point of 66% at three hours. A gentle green line — periodic beaconing with the screen off — falls only to about a quarter at the three-day target and reaches empty near 3.5 to 4 days, through a measured point of 73% at 23 hours. A faint dashed line projects the production headless collar lasting well past a week.

Why the dial works

The radio sleeps almost all the time. What costs energy is the wake-ups — spinning up the GPS for a fix and keying the LoRa transmitter — plus, on this handheld, an OLED that has no business being on a collar. Average current is the sleep floor plus the area of those spikes. Stretch the interval, kill the screen, duty-cycle the GPS, and the average collapses toward the floor:

Current draw over time drawn as a sawtooth: a low deep-sleep floor interrupted by brief tall spikes for each GPS fix and LoRa transmit. On the left, in active mode, spikes come every minute and the average-draw line sits high; on the right, in presence mode, spikes are rare and the average line drops near the sleep floor.

That's the whole reason the production collar is headless and will duty-cycle its GPS hard: it lives on the favorable end of that dial by design, not by luck.

A gut-check that the gauge isn't lying

Two independent readings, two voltages: the collar at 73% / 3.92 V, the base at 59% / 3.80 V. Both sit right where a lithium cell's discharge curve says they should — 3.9 V is high-70s, 3.8 V is high-50s. The firmware's percentage and the raw voltage agree, which means I can trust the number on the graph instead of second-guessing it.

The base barely moved

The base station's Device Metrics over 24 hours: battery 59% at 3.80 V, uptime 23 h 23 m, channel 6.0% and air 3.8%, the battery line nearly flat across the whole window.

The base is a different animal — it mostly listens, and receiving is cheap next to transmitting. Its battery line is nearly flat across the day, which is idle-mode territory all on its own. And the base gets to cheat anyway: in real life it's plugged into the wall at home. The collar is the only node with a hard power budget.

What this still doesn't tell me

Two caveats keep me honest. First, these are the handheld Wio units, with screens — the production RAK collar drops the display draw entirely and can sleep the GPS harder, so it should beat everything here. Second, I've shown active clears three days, but I have not yet proven the two-week idle target; the base hints at it, but a hint isn't a measurement.

For that I need the spikes and the sleep floor in real milliamps, not a percentage that ticks once a minute. That's a job for a bench meter — a Nordic Power Profiler Kit II, which sources the board and measures current from nanoamps to amps. With it I can read the deep-sleep floor, integrate the energy in a single GPS-fix-and-transmit spike, and project each mode honestly instead of eyeballing a slope. It's on the way.

The verdict, against the scorecard

Battery criterionTargetResult
Active beaconing → runtime≥ 3 days~3.5–4 days — pass
Idle / presence → runtime≥ 2 weeksLooks low-drain; bench measurement to confirm

Active passes. Idle is pointing the right way but isn't nailed down. And the real lesson is the one in the title: the lever that makes or breaks runtime isn't a bigger battery — it's firmware, the duty cycle, the dial. Which is good news, because firmware is the cheapest thing on this whole project to change.

Next: the bench numbers, GPS fix quality, and the test I actually care about — putting it on Quark.