Real-Time Bus Times Where There’s No Signal

Apps already show real-time bus times, if you have signal.

But plenty of stops don’t. Rural routes, mountain roads, the edge of town, underground stations, places where the cell tower is 5 km away and the nearest power outlet is further still. Those are often the stops where passengers are most dependent on transit and most in the dark about it. The irony of modern transit information is that the passengers who need it most are the ones whose stops are least likely to have it, because every piece of infrastructure we’d normally use to deliver it, mains power, cellular data, a backhaul connection, assumes we’re already in the places that are easy to serve.

This project is an attempt to break that assumption.

What it does

I built a small passenger information display that works over LoRaWAN instead of cellular. A device sits at a bus stop, wakes up once a minute, sends its stop ID to a LoRaWAN gateway, and gets back the next few departures, already merged with live delays and cancellations from the agency’s GTFS-Realtime feed. The times then render on an onboard LCD, and in the next iteration, on an e-ink panel.

The whole loop is live right now, running against a real transit agency’s feed, displaying real next-departure times on a Heltec LoRa development board.

Why LoRaWAN

LoRaWAN is a low-power, long-range radio protocol designed for devices that need to send small amounts of data occasionally over distances of several kilometres, on battery power, for years at a time. A single gateway can serve a wide area, and in many regions there are community networks like The Things Network where gateways already exist and anyone can join.

The tradeoff is that LoRaWAN is strict about airtime. In the EU868 band, each device is limited to roughly one percent duty cycle per hour. That sounds generous until you realise it means a few seconds of transmission per hour, total. Any protocol that assumes “just fetch the data” the way HTTP does will not survive contact with LoRaWAN. You have to design around the constraint, not despite it.

The 20-byte payload

The trick that makes this project work is that the device never touches the actual GTFS data. GTFS-Realtime feeds are delivered as protocol buffers that are often 1 to 2 megabytes, updated every 30 seconds or so, covering the entire transit network. No LoRaWAN device is going to download that.

So the server does all the work. It ingests the full static GTFS feed into SQLite, polls the GTFS-RT feed continuously, and merges scheduled times with real-time delays per stop. When a device asks for its stop, the server has already pre-computed the answer, and it packs the next few departures into a 20-byte payload.

Twenty bytes is enough, if you’re careful. Route identifiers become small integers via a lookup table. Times become minutes-until-departure rather than absolute timestamps, which compresses to a single byte each. Status flags (delayed, cancelled, on-time) fit in a few bits. The device decodes the payload, renders the times, and goes back to sleep.

This is the part that makes the whole thing physically possible, and it’s also the part that took the most thought. Everything else is plumbing.

AI-assisted development

This was an AI-assisted build. The protobuf parsing, the SQLite schema for the static feed, and the merge logic between scheduled and real-time times were worked out with an LLM in the loop. A good chunk of the initial PHP endpoint was drafted the same way.

The architecture is mine. So is the LoRaWAN payload format, the 20-byte packing scheme, and the decisions about what the device should and shouldn’t do. Those are the parts where being wrong costs you hardware trips and debugging sessions, and they needed to come from someone who could see the whole loop at once.

AI compressed the boring middle. I kept the shape of the thing. That division of labour is, I think, roughly the right one for this kind of project, and it’s the reason it took a few evenings rather than a few months.

Why it matters

Because GTFS is a global standard, the same server works for any transit network that publishes a feed, which is most of them, from major metropolitan systems to small rural operators. The interesting use case isn’t replacing transit apps in cities where coverage is already good. It’s bringing real-time information to the stops that apps can’t reach.

Think about what a small transit agency faces if they want to install a conventional passenger information display. Trenching for mains power. A cellular data contract per sign. Ongoing maintenance visits. The hardware itself is often the cheapest part of the project. That cost structure is why most rural and peri-urban stops have nothing but a printed timetable that goes out of date the moment there’s a detour.

A LoRaWAN sign changes that structure. No trenching, no cellular contract, no mains wiring. One gateway covers many stops. The economics flip.

What’s next

The next step is a fully self-contained unit: solar panel, battery, e-ink panel, weatherproof enclosure. E-ink is the right display technology for this, readable in direct sunlight, sipping power between refreshes, and perfectly suited to a once-a-minute update cycle. With solar and battery handling power, and LoRaWAN handling data, the device has no external dependencies at all. You bolt it to a post and walk away.

At that point, the installation cost drops to roughly the hardware cost. A full-size departure board at a rural station or a remote park-and-ride becomes something a small municipality can actually afford, and that’s the version worth building.

If you want to talk about this

If you work in transit, run a LoRaWAN network, or are thinking about deploying something similar, I’d be glad to hear from you. The server code is general enough to serve any GTFS feed, and I’m interested in what it would take to turn this from a prototype into something a small agency could actually deploy.

Leave a Comment