When BLE Ends: A Long-Range LoRa Uplink for a Pet Tracker
Every radio on a pet tracker has a horizon. Bluetooth reaches across a room, maybe a house on a good day. The moment a dog slips through a gate and trots down the street, that link is gone — and that is precisely the moment a pet parent most needs to know where their dog is. A tracker that only speaks Bluetooth goes silent exactly when it matters most.
So our tracker carries a second radio: a long-range, low-power LoRa uplink that wakes up when Bluetooth falls away and carries a tiny, precious payload — a location — over kilometers instead of meters. The catch is that "long range" is bought with bandwidth. This is a look at how we designed a wire format frugal enough to fit a pet's location into a couple of dozen bytes, and the policy that decides when to spend them.
The Problem: Range Is Paid For in Bytes
LoRa's superpower is reach. It trades raw throughput for the ability to punch through walls and across open ground at very low power — which is exactly the trade a battery-powered collar wants.
But the bill comes due in bandwidth. Our tracker's link runs at roughly 5 kbps of effective throughput — a rounding error next to Bluetooth, let alone WiFi. There's no streaming here, no batching of sensor history, no chatty protocol handshakes. Every transmission also costs airtime and battery, so you can't simply send more often to compensate.
That reframes the whole design problem. Instead of "how do we move the data," the question becomes "what is the absolute minimum set of bytes that still answers the question a worried owner is asking?" The answer is written into the format as a hard budget — a fixed twelve-byte header and a body that must fit in what remains of a very small frame:
33 #define HTRACKER_UPLINK_HDR_LEN 12u
34 #define HTRACKER_UPLINK_MAX_BODY (240u - HTRACKER_UPLINK_HDR_LEN)

The Approach: A Fixed Binary Frame, Not a Flexible One
On a fat link you reach for a self-describing format — JSON, or a packed binary map — because flexibility is cheap. On this link, flexibility is unaffordable. Field names, delimiters, and type tags would each cost bytes that could have been coordinates.
So the uplink uses a fixed binary layout: a small common header shared by every message, followed by a type-specific body whose structure is agreed in advance by both ends. Nothing on the wire describes itself; the receiver knows the shape because it compiles the same definition.
That common header is twelve bytes, and every field earns its place:
8 * Common header (12 bytes), shared by every uplink type:
9 *
10 * [0] u8 type (one of UPLINK_TYPE_*)
11 * [1] u8 seq (rolling, +1 per uplink; dedup / loss-counting)
12 * [2:5] u32 LE uptime_ms (tracker uptime since boot)
13 * [6:11] u8[6] mac_tail (chip MAC, BLE identity — stable per device)
One byte says what kind of message this is. One byte is a rolling sequence number — the cheapest possible reliability instrument, letting the receiver spot duplicates and count losses without any acknowledgement traffic. Then uptime, and six bytes of stable device identity so the hub knows which collar spoke.
Note what's absent: no length prefix for the header, no version field, no checksum. Those jobs belong to the layer below, and duplicating them here would be bytes spent twice.
The Process: A Location in Twelve Bytes
The primary message is a GPS fix, and its body is a masterclass in frugality — three integers, twelve bytes, no floats:
49 /* UPLINK_TYPE_GPS (12 bytes total body):
50 * [0:3] i32 LE lat_e7 (latitude × 1e7, signed)
51 * [4:7] i32 LE lon_e7 (longitude × 1e7, signed)
52 * [8:11] u32 LE fix_epoch_s (UTC seconds since 1970, from GPS time)
Latitude and longitude are stored as scaled integers — the real coordinate multiplied by ten million and truncated to a signed 32-bit value. That's the key trick. It gives roughly centimetre-level resolution, far finer than any consumer GPS actually achieves, in exactly eight bytes.
Compare the alternatives. Two double-precision floats would cost sixteen bytes for precision no receiver can use, and a text representation costs more still while inviting parsing and locale bugs. Fixed-point integers are smaller, exact, endian-explicit, and trivial to decode on a microcontroller with no floating-point hardware.
Add the header and a location fix costs 24 bytes on the wire. A dog's position, anywhere within kilometres, for less than the size of this sentence.

The Policy: Speak Only When Bluetooth Can't
Having a second radio is only half the design. The other half is knowing when to use it — because every LoRa transmission costs battery and airtime, and Bluetooth already carries far richer data whenever it's available.
The rule is a small state machine, written directly into the header as the contract both sides live by:
54 * Emission policy (see communication_lora.c task_tx):
55 * - BLE L2CAP CoC open → silent (BLE g1 frames carry GPS already)
56 * - BLE down < 30 s → grace period, silent
57 * - BLE down ≥ 30 s + fix → emit every 5 min
58 * - no fix → silent (don't ship 0,0)
Read it as an escalation ladder. While Bluetooth is connected, the long-range radio stays completely silent — the rich local link is already delivering location, so spending airtime would be pure waste. When Bluetooth drops, there's a 30-second grace period, because most disconnections are momentary and resolve themselves; reacting instantly would burn power on every brief blip.
Only when the link has genuinely been gone for half a minute and the tracker holds a real satellite fix does it start uplinking — a location every five minutes, enough to follow a wandering dog without draining the collar.
The last line is my favourite, and the most quietly important: with no fix, stay silent. It would be easy to emit a frame anyway with zeros in the coordinate fields, and it would be actively harmful — a phantom location at the equator is worse than an honest gap. The system would rather say nothing than say something false.

The Results: A Tracker That Doesn't Go Dark
The outcome is a collar with graceful degradation built in. In the house, Bluetooth carries a rich stream — motion, audio events, detailed activity. Out of range, that stream stops, but the device does not go dark: it falls back to a trickle of twenty-four-byte fixes that still answer the only question that matters when a pet is missing.
Two design choices make it durable in the field. The rolling sequence number lets the hub measure exactly how many uplinks were lost to radio conditions, turning an invisible failure into a metric. And the format is forward-compatible by construction — new message kinds can be added without breaking anything already deployed:
22 * Backwards compat: when adding new uplink types, only allocate a new
23 * UPLINK_TYPE_* value. Old decoders log it as "unknown" and skip.
There's a matching discipline that keeps a fixed binary format safe across two independent codebases: the tracker's definition and the hub's must stay byte-for-byte identical, changed together in the same commit. With no self-describing metadata on the wire, a one-byte divergence wouldn't raise an error — it would silently mis-decode every field after it. The rule is written into both files precisely because the failure mode is so quiet.
Why It Matters at Hoomanely
Hoomanely is reinventing healthcare for pets — replacing reactive, imprecise care with continuous, clinical-grade monitoring that catches problems early. Our devices form a Physical Intelligence ecosystem: sensors fused at the edge, feeding the Biosense AI Engine that turns raw signals into personalized, preventive insights.
Continuous care can't have a hard edge at the property line. Most of the time a pet is home and our richer links do the heavy lifting — but the minutes after a dog gets loose are the highest-stakes minutes a pet parent will ever have with our product. A long-range fallback is how we make sure the collar keeps speaking when everything else has fallen away.
It's also a lesson in engineering values. Choosing to send fewer, smaller, more honest messages — and to send nothing at all rather than something false — is the same discipline that runs through every layer of our stack. Trustworthy beats chatty, at every range.
Key Takeaways
- Range is paid for in bandwidth. A long-range radio buys reach by giving up throughput, so the design problem becomes finding the minimum bytes that still answer the question.
- Use fixed binary layouts when bytes are scarce. Self-describing formats cost real payload; a shared compiled definition costs nothing on the wire.
- Fixed-point beats floating-point. Coordinates as integers scaled by ten million give centimetre resolution in half the bytes, with exact, endian-explicit decoding.
- A one-byte sequence number is the cheapest reliability tool there is. It enables dedup and loss-counting with zero acknowledgement traffic.
- Decide when not to transmit. Staying silent while the local link works, riding out brief dropouts, and never shipping a fake position are what make the fallback both efficient and honest.
Author's Note
This long-range uplink sits at the far edge of Hoomanely's Physical Intelligence ecosystem — the radio that carries a collar's voice past the end of every other link. Most of its life is spent perfectly silent, which is exactly right. But when a dog is somewhere it shouldn't be, twenty-four carefully-chosen bytes travelling a very long way is the difference between a frantic search and a map with a pin on it.