development · ble gps bridge
The BLE GPS bridge
The reader has no GPS. The phone does. Between them sits one Bluetooth LE service that carries position packets, an ASCII console and map files, on a device with 58 KB of free heap on that screen, where bringing the BLE stack up costs 57 KB of it. This is how that link works, and what it took to stop it wasting the phone’s battery.
One service, three jobs
Everything runs on one GATT service and one connection, on purpose: a second connection would cost heap the map screen does not have.
| Channel | Direction | Carries |
|---|---|---|
| position | phone → reader | the fix: latitude, longitude, heading, time |
| console | both ways | ASCII commands and their answers — the same console USB serial gets |
| transfer | phone → reader | map-file frames, one BLE write per frame |
| transfer status | reader → phone | the verdict on each transfer |
The reader refuses to start a transfer whose verdict has nowhere to go: a begin frame with nobody subscribed to the status channel is rejected outright. It is a small rule that removes a whole class of silent failure.
One trap, computed rather than measured, and worth repeating for anyone building a BLE file push: the central must ask for a large ATT MTU and a high-priority connection. At the default 23-byte MTU the link carries 15 bytes of file payload per write — about 375 B/s, so roughly 80 s for a 30 KB tile, against about 2 s at MTU 256 with a 15 ms interval. Both of those are arithmetic off the frame size and the connection interval, not a stopwatch. What has been measured is a real push at MTU 256: 42,681 bytes at 2.6 KB/s, with a matching checksum.
The reader advertises only while the map is open
Opening the map screen brings up the BLE peripheral; leaving it tears the whole stack down, because 57 KB of heap is not something this device can hold for a screen that is not using it. That has a consequence riders notice: “no device found” almost always means the map screen is not open, not that the link is broken.
It also handed us a feature for free. “The rider opened the map” is already on the air, so no protocol addition was needed to use it as a wake signal.
Opening the map wakes a killed app
A ride used to start with a chore: unlock the phone, find the app, open it, wait. Now the phone associates once with one reader through Android’s companion-device manager and asks the OS to watch for it. The OS does the scanning, so it survives the app process dying — including a swipe from recents. When the reader appears, the system binds a small service that starts the bridge and returns.
Verified end to end on hardware, 2026-08-11: real reader, real phone, app killed, map opened, one position packet sent with nobody touching the phone. Then again after a phone restart, which is the case that usually breaks this kind of thing.
Pairing with one reader fixed a second problem nobody had noticed: two of these readers look identical over the air — same name, same service. The app used to talk to whichever answered first, so a second device in the room was a coin toss, and the loser gets a stranger’s position on their map. It is a fixed address now, not a guess.
Real hardware also found a real bug in five minutes: the pairing screen sat empty while the app was connected to the very reader it was looking for, because the reader stops advertising once a phone connects. The app now steps off the link while you pair and steps back on afterwards.
What is not measured yet: how fast the OS-level background scan notices a reader that has been advertising at its slow interval (200–300 ms) for minutes. The fast interval (30–60 ms for the first 30 s after the screen opens) covers the case this feature is about; the slow one is an open measurement, and it is written down as one.
One switch for all the power the app spends
Turn the reader off and the app used to carry on: GPS at one fix a second, a wake lock held, and a notification claiming it was still sending your position — to nothing. Now one question decides everything: is the reader connected, or is the rider recording?
| Cost | On when | Off when |
|---|---|---|
| GPS and network location, 1 Hz | connected or recording | otherwise |
| partial wake lock | connected or recording | otherwise |
| the 1 Hz send timer | connected or recording | otherwise |
They are one decision on purpose. Nothing read a fix with the link down anyway — the sender returns immediately when disconnected — so GPS while disconnected bought a stale position and nothing else. With neither condition true the service shuts itself down after five minutes, and opening the map on the reader starts it again.
The foreground-service claim follows the same rule: the service goes foreground claiming only the connected-device type, and adds the location type at the moment it actually requests location. The claim matches the work, and Android 14+ refuses a background-started service that claims location without while-in-use location anyway.
The bridge is also how maps arrive
The same link carries map squares. The reader records every tile it tried to open and could not, with a hit count, across restarts — a record of what a real ride actually needed. The phone reads that list over the console channel, fetches those squares from the public tile server and pushes them onto the SD card mid-ride. What happens when the server does not have them either.