By mid-October the collar worked on the bench and failed on the dog. The battery read 100% until it died, nights showed up as "no data", and synced windows came back corrupted. Over one week we rewrote most of the firmware, fixed sync on both phones, measured the battery on a real dog and put the Android app through Google Play. This is what changed and what the numbers say.
An audit before more fixes
In June we wrote about the LED that ate our battery. On 20 October we audited the deployed firmware, v0.8.1, line by line instead of adding another patch. The LED fix had never worked. The XIAO's RGB LED is active-low, our sketch defined "off" as low, so setup lit all three LEDs permanently. That alone costs about 1 mA, roughly six days on our cell.
The same audit found the rest:
- Battery always read 100%. The divider enable pin was never driven low, and the ADC scale was wrong.
- Each activity window is 322 bytes, but a BLE notification at our MTU carries at most 253. Every synced window was cut short.
millis()wraps every 69.9 minutes in this Arduino core, so timestamps jumped.- The Android app parsed the new window stream as the old 20-byte format.
Firmware v0.9: a log that survives
Version 0.9.1 went to hardware on 20 October. Every 20-second window now goes straight to the QSPI flash chip, with a sequence number, checksum and boot ID written last, so a reset mid-write cannot leave a half record. The phone clears only what the last completed sync delivered. Notifications are split into 244-byte chunks with a retry when the radio buffer is full. A 64-bit clock replaced millis().
The battery reading took two fixes. After enabling the divider it showed about 70 mV, "no battery". The conversion raw * 3600 * 1510 overflowed a 32-bit integer. With 64-bit maths the same raw value reads 4.14 V. Battery percent is now real, within about 5%.
The collar used to advertise in bursts every 30 minutes while asleep, so a phone coming home could wait half an hour to reconnect. Now it advertises continuously: fast for a short time after boot or disconnect, then every 1.1 seconds. On the bench, after 20 minutes away with the collar in deep sleep, the phone reconnected in 13 seconds, 10 times out of 10.
Version 0.9.3, on 21 October, fixed the empty nights. A dog asleep all night produced no windows, so the app showed "no data" until morning. In deep sleep the collar now records one real 2-second window every 30 minutes, at a cost of about 1.3 µA. Gaps are capped at about 30 minutes and count as sleep.
Sync on two phones
On iOS the app restored the Bluetooth connection only halfway after iOS relaunched it in the background, so it showed "connected" and never synced. It now stores just the peripheral, reconnects once Bluetooth is on, and retries failed connects immediately, because timers do not run in the background.
On Android the old app cleared the collar's log before uploading. A failed upload lost data. It now acknowledges a sync only after the server has the data, and every Bluetooth operation goes through one queue, since Android drops overlapping GATT calls.
Updates without a cable
Until v0.9.2 a firmware update meant a USB cable and a button press on the board. Now the app updates the collar over Bluetooth, in about three minutes on battery. If the connection drops, the update picks up where it stopped, and activity that has not synced yet survives every update.
Measured on a dog
From 21 to 25 October our prototype collar ran firmware 0.9.3 on a dog, syncing in the background. The battery went from 95% to 81%, 3.5% a day. Night readings agree (95, 91, 87.5, 84.5). On the prototype's small 150 mAh test cell that is about 28 days per charge, an average draw of about 0.22 mA.
Production collars will use a 300 mAh cell. At the same draw that works out to roughly eight weeks, but that is arithmetic, not a measurement. We will publish the real number once we have run the same test on the production cell.
The data was clean: 3,014 windows, all 322 bytes, no gap longer than 35 minutes and no duplicates. Median upload delay was 11 to 15 minutes. The worst case was 290 minutes, and the phone caught up with nothing lost.
Google Play
The Android app shipped to Play's internal testing track first. One build, 1.5.4, could not log in at all: the cloud build picked up a stale local Android project and skipped a step of our build setup. We now build Android locally and check every bundle before it goes to Google.
We applied for production access on 23 October and Google approved it on 25 October. Version 1.5.9 is the build going to production.
What is still open
Four days is a short test. We want a full drain curve, so the next step is logging voltage on every upload, first on the test cell and then on the 300 mAh production cell. The collar still has no true System OFF sleep with wake-on-motion, which could stretch the battery further but needs careful bench work first.
If you are building something similar
- Audit the firmware that is actually running before writing another fix.
- Check integer widths in every unit conversion. Ours read 70 mV instead of 4.14 V.
- Never clear data on a device until the server has confirmed it.
- Measure battery on the animal, not on the desk. Activity changes the draw.