Real-Time Data Streaming
How telemetry flows from your car to the dashboard in near real-time.
The Data Pipeline
Car ECU → ESP32 → Phone → Internet → HiveMQ → Bridge → TimescaleDB → Grafana
(poll) (WiFi) (MQTT) (insert) (store) (display)
Message Flow (Every ~2 Seconds)
Step 1: ESP32 Polls the Car
The ESP32 sends OBD-II PID requests to the ELM327 adapter:
ESP32 sends: "010C\r" (request RPM)
ELM327 returns: "41 0C 1A 2B" (raw hex response)
ESP32 parses: RPM = (0x1A * 256 + 0x2B) / 4 = 1674
This repeats for all 10 PIDs (~100ms each = ~1 second total).
Step 2: ESP32 Builds JSON
After polling all PIDs, the ESP32 constructs a JSON payload:
{
"rpm": 2500,
"speed": 80,
"coolant": 90,
"intake_temp": 35,
"throttle": 45,
"engine_load": 60,
"map_kpa": 101,
"fuel_level": 75,
"timing_adv": 12,
"battery_voltage": 14.2
}
Size: ~150-200 bytes per message.
Step 3: MQTT Publish
ESP32 publishes to HiveMQ Cloud:
mqttClient.publish("car/telemetry", jsonPayload);
- Topic:
car/telemetry - QoS: 0 (fire and forget)
- Retain: false (don't store for late subscribers)
Step 4: Broker Routes Message
HiveMQ Cloud receives the message and delivers it to all subscribers on car/telemetry:
Publisher (ESP32) ──► HiveMQ ──► Subscriber (Bridge)
│
└──► Future: Mobile app
└──► Future: Alert service
Step 5: Bridge Inserts to Database
The Python bridge receives the message:
def on_message(client, userdata, msg):
data = json.loads(msg.payload)
cursor.execute("""
INSERT INTO car_metrics (time, rpm, speed, ...)
VALUES (NOW(), %s, %s, ...)
""", (data['rpm'], data['speed'], ...))
Step 6: Grafana Displays
Grafana queries TimescaleDB every 5 seconds:
SELECT time, rpm, speed, coolant
FROM car_metrics
ORDER BY time DESC
LIMIT 1
And updates the dashboard gauges.
Latency Breakdown
| Stage | Latency |
|---|---|
| OBD polling (10 PIDs) | ~1000ms |
| JSON construction | ~1ms |
| MQTT publish | ~50-200ms |
| Internet transit | ~50-100ms |
| Bridge processing | ~10ms |
| Database insert | ~5ms |
| Grafana refresh | 5000ms |
| Total | ~2-7 seconds |
This is "near real-time" - you see data within a few seconds of it happening.
Data Volume
At 1 message every 2 seconds:
- 200 bytes × 30 messages/minute = 6 KB/minute
- 6 KB × 60 minutes = 360 KB/hour
- 360 KB × 8 hours = 2.9 MB/day
HiveMQ free tier gives 10 GB/month - plenty of headroom!
Message Patterns
Current: Request-Response Polling
ESP32 ──request──► ELM327
ESP32 ◄──response── ELM327
(repeat for each PID)
Slow but reliable. Each PID takes ~100ms.
Alternative: Event-Driven (Not Used)
Some advanced OBD adapters support streaming mode where the ECU pushes changes. Our ELM327 doesn't support this.
QoS Levels Explained
| QoS | Name | Guarantee | Use Case |
|---|---|---|---|
| 0 | At most once | None - fire and forget | Telemetry (our choice) |
| 1 | At least once | Delivered, may duplicate | Commands, alerts |
| 2 | Exactly once | Delivered once only | Financial, critical |
We use QoS 0 because:
- Lowest latency
- If one message is lost, we get another in 2 seconds
- Telemetry is "perishable" - old readings don't matter
Scaling Considerations
Current system handles:
- 1 car
- ~0.5 messages/second
- ~10 data points/message
To scale to multiple cars:
- Use topic hierarchy:
car/{car_id}/telemetry - Bridge subscribes to
car/+/telemetry(wildcard) - Add
car_idcolumn to database
Failure Modes
| Failure | Behavior |
|---|---|
| WiFi drops | ESP32 retries every 5 seconds |
| MQTT disconnects | ESP32 retries every 5 seconds |
| Bridge crashes | Messages queue in HiveMQ (limited) |
| Database down | Bridge fails insert, logs error |
| Grafana down | Data still collected, view later |
The system is resilient - most failures result in temporary data gaps, not data loss.