Real-time GPS tracking platform for the MicroTransit Innovation Challenge — Florida Polytechnic University
An end-to-end cellular GPS tracking system: an Arduino-based hardware tracker publishes live telemetry over the Hologram IoT cellular network to a self-hosted MQTT broker, a Python backend stores and broadcasts every update in real time, and a browser dashboard renders live vehicle positions on an interactive map.
Arduino Uno + SIM7000A
└─ GPS + cellular (Hologram IoT SIM)
└─ publishes JSON over MQTT (port 1883)
│
▼
Mosquitto MQTT Broker (Docker, public VPS)
│
▼
Python Backend (FastAPI + paho-mqtt + SQLite)
└─ Kalman filter → SQLite → WebSocket broadcast
│
▼
Browser Dashboard (Next.js 16 + Leaflet.js)
| Feature | Description |
|---|---|
| GPS + cellular | botletics SIM7000A shield — GPS and GPRS in one |
| RAM-efficient JSON | Hand-written dtostrf-based builder; no ArduinoJSON heap bloat |
| EEPROM offline buffer | 56-entry circular buffer — stores fixes during outages, flushes on reconnect |
| Hardware watchdog | 8 s WDTO_8S — chip-level MCU reset on any software hang |
| Layered auto-reconnect | Network → GPRS → MQTT; full modem reinit when buffer fills (≈ 18 min) |
| Last Will message | Broker publishes "offline" within ~20 s of unexpected disconnect |
| Remote rate control | Dashboard can change the GPS publish interval without reflashing |
| GPS valid flag | gps_valid: false payloads keep the heartbeat alive when no fix is held |
| Feature | Description |
|---|---|
| MQTT subscriber | paho-mqtt daemon thread, thread-safe WebSocket bridge |
| Kalman filter | Server-side 2-D constant-velocity model — smooths GPS noise per device |
| SQLite persistence | 3 tables: devices, positions, alerts — survives restarts |
| Route segmentation | segment_id increments on GPS loss or reconnect — no false polyline gaps |
| Alert logging | Online/offline and GPS fix/lost events with timestamps |
| REST API | /api/devices, /api/history, /api/alerts, /api/get_rate, /api/set_rate |
| WebSocket | Real-time push for location and status messages |
| Docker deployment | Three containers via Docker Compose: Mosquitto, Python backend, Next.js frontend |
| Feature | Description |
|---|---|
| Live map | Leaflet.js markers update in real time on every WebSocket push |
| Route polylines | Per-device breadcrumb trail, broken correctly on GPS loss |
| Multi-device | Each device gets a distinct color; new devices appear automatically |
| Device panel | Collapsible cards — coordinates, speed, battery, GPS state, last seen |
| Fleet stats | Online count, GPS count, average/max speed, average/min battery |
| Navigation tabs | Map · Devices · History (replay) · Alerts |
| Settings | Map style, speed/battery units, publish rate, per-device labels |
| Dark / light mode | Warm aerospace light theme + soft dark theme |
| Mobile support | Fully responsive — works on phone without a separate app |
.
├── 01_Documents/
│ ├── 01_Proposal/ # Phase 1 proposal (Typst source + PDF)
│ ├── 02_Midprogress_Report/
│ ├── 03_Final_Report/ # Final report (Typst source + compiled PDF)
│ ├── 04_Media/ # Demo screenshots
│ └── 05_Presentation/ # Slide deck
│
├── 02_Arduino_Code/
│ └── MicroTransit_Tracking_Arduino_Code/
│ └── src/
│ ├── main.cpp # Main loop — state machine, WDT, EEPROM flush
│ ├── modem.cpp/h # TinyGSM init, GPS read, battery read
│ ├── mqtt.cpp/h # PubSubClient connect, callback, Last Will
│ ├── buffer.cpp/h # EEPROM circular buffer
│ ├── payload.cpp/h # Hand-written JSON builder
│ └── config.h # All pins, APN, broker, timing constants
│
├── 03_Backend/
│ ├── backend.py # MQTT subscriber + Kalman filter integration
│ ├── websocket.py # FastAPI app, REST endpoints, WebSocket server
│ ├── database.py # SQLite helpers
│ ├── kalman.py # 2-D constant-velocity Kalman filter
│ ├── config.py # Broker host/port/credentials, topic names
│ └── simulate_GPS_payload.py # Device emulator for testing
│
└── 04_Frontend/
├── app/ # Next.js app router (layout, page)
├── components/ # Map, DevicePanel, StatsPanel, TopNav, Settings
│ └── views/ # DeviceTable, HistoryView, AlertsView
├── hooks/useTracker.ts # WebSocket state machine
├── contexts/ # Settings context (localStorage persistence)
└── types/index.ts # Shared TypeScript types
| Component | Part |
|---|---|
| Microcontroller | Arduino Uno (ATmega328P, 2 KB SRAM) |
| Cellular + GPS shield | botletics SIM7000A |
| SIM card | Hologram IoT SIM (APN: hologram) |
| GPS antenna | Active antenna via bridge jumper on shield |
| Power | 2x 3.7 V LiPo (cellular module) + Arduino power |
| Wiring | SoftwareSerial TX/RX on D10/D11; PWRKEY D6; RST D7 |
Requirements: PlatformIO (VS Code extension or CLI)
cd 02_Arduino_Code/MicroTransit_Tracking_Arduino_CodeEdit src/config.h:
#define DEVICE_ID "tracker-01"
#define APN_SETTING "hologram" // your SIM's APN
#define MQTT_HOST "your.server.ip"
#define MQTT_PORT 1883
#define MQTT_USER "mosquitto"
#define MQTT_PASSWORD "your_password"Flash with PlatformIO:
pio run --target uploadThe built-in LED lights when a valid GPS fix is held.
Requirements: Docker with the Compose plugin
cp .env.example .env # edit .env with your broker credentials
docker compose up -dThis starts three containers: mosquitto (port 1883), backend (port 8000), and frontend (port 3000).
Open http://localhost:3000.
Requirements: uv, a running Mosquitto broker
Edit 03_Backend/config.py to match your broker, then:
cd 03_Backend
uv sync
uv run python backend.py # starts MQTT subscriber + FastAPI serverThe frontend is included in the Docker Compose setup above. To run it separately in dev mode:
Requirements: Node.js 20+ / Bun
cd 04_Frontend
bun install # or: npm install
bun run dev # or: npm run devOpen http://localhost:3000. The dashboard connects to the backend WebSocket automatically (configure the URL in hooks/useTracker.ts if needed).
To simulate a device without hardware:
uv run python simulate_GPS_payload.pyThe Arduino publishes one JSON message per cycle to tracker/{device_id}/location:
{
"device_id": "tracker-01",
"battery_mv": 3921,
"gps_valid": true,
"lat": 28.150612,
"lng": -81.847583,
"speed_kmh": 14.3,
"timestamp": "2026-04-11T18:42:07Z"
}When no GPS fix is available, only device_id, battery_mv, and gps_valid: false are sent — keeping the heartbeat alive without polluting the position history.
| Endpoint | Method | Description |
|---|---|---|
/api/devices |
GET | All devices with online state, GPS state, battery, last seen |
/api/history/{device_id} |
GET | Full position history ordered by timestamp |
/api/alerts |
GET | Most recent 500 online/offline and GPS fix/lost events |
/api/get_rate |
GET | Current GPS publish interval (seconds) |
/api/set_rate/{device_id} |
POST | {"rate": N} — update interval over MQTT without reflashing |
/ws |
WebSocket | Real-time push: location and status message types |
- TLS: The SIM7000A firmware on the provided hardware does not support
AT+CNACT/AT+CIPSSL, so the broker uses plain TCP on port 1883 with username/password authentication. Switching to a TLS-capable managed broker (HiveMQ Cloud, Flespi.io) requires changing two lines inconfig.hand one block inconfig.py. - Multi-device: Each additional tracker needs only a unique
DEVICE_IDinconfig.h. The broker, backend, and dashboard require no changes. - Kalman filter: Applied server-side before each position is saved to SQLite. Tuned for near-pass-through behaviour (R = 1×10⁻⁹) — smooths GPS jitter without lagging behind the vehicle.
| Document | Link |
|---|---|
| Wiring Instructions | Wiring_Instructions.md |
| Backend README | README.md |
| Final Report | final_report.pdf |
| Proposal | tracking_proposal.pdf |
| Mid-Progress Report | MicroTransitTracking_Midprogress_Report.pdf |
| Presentation | OpenTrack_Presentation.pptx |
2026 Steffen Ullmann · sullmann3121@floridapoly.edu · Florida Polytechnic University · MicroTransit Innovation Challenge