Project case study · IoT and computer vision
BusLink: from ESP32-CAM to a live transit dashboard.
Connecting a bus-mounted camera and GPS receiver to a Python backend and a React interface.
BusLink explores two questions a passenger has before boarding: where is the bus, and how crowded is it? My published implementation combines location telemetry with camera-based crowd estimates and presents the result in a web interface.
One system, five connected stages
- Capture: an ESP32-CAM takes a JPEG image while a NEO-6M receiver supplies GPS coordinates and speed.
- Transmit: the firmware uses Wi-Fi when available and falls back to a SIM800L cellular connection.
- Receive: a Python FastAPI service receives the image and bus telemetry.
- Analyze and store: cloud models estimate the crowd size, and Firestore stores the latest location and crowd status.
- Display: a React interface listens for updates and presents bus cards with crowd indicators.
The hardware constraint: small memory, slow uploads
The first article documents an ESP32-CAM, a NEO-6M GPS receiver and a SIM800L modem. The firmware streams image data in 1,024-byte chunks instead of constructing a second large upload buffer. This keeps the upload logic manageable on a small device.
Chunking alone is not a delivery guarantee. A robust sender must check how many bytes each write actually accepted, handle partial writes, set timeouts and confirm the server response. Those are separate concerns from choosing a chunk size.
The original setup uses 2G cellular hardware. Anyone adapting the design should check local network availability and modem support before choosing components.
The API contract matters
The hardware article describes a multipart upload, while the backend example reads raw request-body bytes. These examples need an explicit, shared contract when combined: either parse the multipart image field on the server or send raw JPEG bytes with the matching content type. Feeding multipart boundaries into an image decoder will fail.
Each telemetry message should also identify its bus and capture time. Capture time and server receipt time answer different questions, especially when a device reconnects and sends older observations.
Crowd estimates are measurements with uncertainty
The cloud article uses Gemini and a Hugging Face DETR model concurrently, then combines their counts into a crowd category. Concurrent execution can reduce waiting compared with sequential calls, but the benefit depends on each service's response time.
Two models do not guarantee accuracy or availability. Zero can mean an empty bus; it should not double as an error flag. A stronger result format separates the count from success, timeout and failure states. An unavailable estimate should appear as unknown in the interface.
Keeping the dashboard honest
The implementation uses Firestore merge writes to update telemetry while preserving fields such as route information. The React interface listens for changes and displays crowd categories.
A useful next step is a visible freshness indicator. A last-known position is still useful, but it should include its age. Location updates should also continue when crowd inference is unavailable, so a slow model does not prevent the map from receiving fresh telemetry.
Scope and next steps
This page summarizes the implementation documented in my two original articles and adds a design review. It does not claim a measured fleet deployment, audited uptime or independently validated passenger-count accuracy. A field evaluation would need labeled samples, upload latency measurements, recovery tests and clear rules for image access and retention.