Telemetry Pipeline¶
The pipeline has three stages: ingestion (sensors → raw data), fusion (raw → confidence-annotated frame), and distribution (frame → hot path + DuckDB persistence).
Stage 1: Ingestion¶
Racelogic Mini¶
Professional-grade GPS + IMU. Actual VBO output is 10Hz (sample period 0.100s), not the hardware-native 20Hz. This is the effective rate for all downstream processing.
| Signal | Rate | Range (Observed from 183 sessions) | Confidence |
|---|---|---|---|
| Latitude / Longitude | 10Hz | 8 tracks identified by GPS clustering | 0.95 |
| Speed | 10Hz | 0–198 km/h (peak in VBOX0318) | 0.95 |
| Heading | 10Hz | 0–360° | 0.95 |
| Lateral Acceleration | 10Hz | -1.81 to +1.63 G | 0.95 |
| Longitudinal Acceleration | 10Hz | -1.66 to +0.21 G | 0.95 |
| Combined Acceleration | 10Hz | 0–2.29 G | 0.95 |
| Altitude | 10Hz | 8–93 m (48m delta at Sonoma) | 0.80 |
Connection: Bluetooth or serial to Pixel 10.
Confidence adjustment: If satellite count drops below 60 (encoded quality flag, not literal count) or HDOP exceeds 2.0, GPS confidence drops to 0.60. IMU confidence remains 0.95 regardless of GPS quality.
10Hz, Not 20Hz
The Racelogic hardware captures at 20Hz internally, but the VBO file output is 10Hz (sample period 0.100s confirmed across all 183 files). All latency budgets, frame rates, and model inference timings are based on 10Hz = 100ms per frame.
USB-CAN Adapter¶
CAN bus extraction via USB (python-can + cantools). Signal ranges verified across 183 VBO sessions (originally recorded with OBDLink MX; the as-shipped architecture uses USB-CAN adapters for lower latency):
| Signal | Rate | Range (Observed) | Confidence | Status |
|---|---|---|---|---|
| Brake Pressure | 10Hz | 0–104 bar | 0.95 | Working — most valuable CAN signal |
| Brake Position | 10Hz | 0–1.0 | 0.95 | Working |
| Throttle Position | 10Hz | 0–99% | 0.95 | Working |
| Steering Angle | 10Hz | -1024° to +372° (clip to ±500°) | 0.90 | Working, wraps at extremes |
| Engine RPM | 10Hz | 843–8,582 rpm | 0.95 | Working |
| Coolant Temp | 10Hz | 68–99°C | 0.90 | Working |
| Oil Temp | 10Hz | 84–121°C | 0.90 | Working |
| Oil Pressure | 10Hz | 0.6–5.85 bar | 0.85 | Working |
| Fuel Level | 10Hz | 20–46% | 0.80 | Working |
| Battery Voltage | 10Hz | 13.1–13.6 V | 0.90 | Working |
| Gear | — | 255 (constant) | — | Not mapped — derive from RPM/speed |
| Clutch Position | — | 255 (constant) | — | Not mapped |
| Air Fuel Ratio | — | 500 (constant) | — | Not connected |
| Exhaust Temp | — | -50 (constant) | — | Not connected |
| Vehicle Speed (OBD) | — | 500 (constant) | — | Not configured — use GPS velocity |
Connection: Bluetooth SPP to Pixel 10.
7 Broken Signals
Gear, Clutch, AFR, EGT, Vehicle Speed, Head Temp, and Brake_Press_(Calc) are constant or mislabeled across all 183 files. These CAN IDs are not mapped in the DBC configuration for this car (BMW M3 S54). Gear must be derived from RPM/speed ratio.
Confidence adjustment: If the USB-CAN adapter disconnects, all CAN signals are marked stale within 100ms. Reconnection triggers 2-second validation before restoring confidence.
Stage 2: Sensor Fusion¶
Even with professional hardware, fusion adds value.
graph TB
subgraph Speed Fusion
RL_SPD[Racelogic Speed<br/>10Hz, 0.95 conf] --> KF[Kalman Filter<br/>Racelogic weight 0.7<br/>CAN weight 0.3]
OBD_SPD[USB-CAN Speed<br/>via wheel speeds<br/>10Hz, 0.95 conf] --> KF
KF --> SPEED[Fused Speed<br/>50Hz, conf 0.95]
end
subgraph G-Force Processing
RL_IMU[Racelogic IMU<br/>20Hz raw accel] --> BW[Butterworth 2nd order<br/>cutoff 12Hz<br/>removes road vibration]
BW --> GFORCE[Clean G-Forces<br/>20Hz, conf 0.95]
end
subgraph Position Fusion
RL_GPS[Racelogic GPS<br/>20Hz, sub-meter] --> CF[Complementary Filter<br/>GPS corrects drift<br/>IMU fills between fixes]
RL_DR[IMU Dead-Reckoning<br/>50Hz between GPS fixes] --> CF
CF --> POS[Smooth Position<br/>50Hz, conf 0.95]
end
subgraph Input Pass-Through
OBD_IN[USB-CAN<br/>throttle, brake, RPM,<br/>gear, steering] --> PASS[Pass-through<br/>no fusion needed<br/>direct CAN = ground truth]
PASS --> INPUTS[Driver Inputs<br/>50Hz, conf 0.95]
end
SPEED --> FRAME[Confidence-Annotated Frame]
GFORCE --> FRAME
POS --> FRAME
INPUTS --> FRAME
Why Fuse Pro Hardware?¶
- Racelogic GPS in a tunnel/tree cover: GPS drops to 5Hz, HDOP spikes. Fusion detects this and reduces GPS confidence while IMU dead-reckoning maintains smooth position.
- Road surface vibration: Even Racelogic's IMU picks up bumps. Butterworth filter at 12Hz removes road noise while preserving real driving dynamics (which peak at ~5Hz for aggressive cornering).
- Racelogic vs CAN speed disagreement: If one sensor has a momentary glitch, the Kalman filter smooths through it instead of passing a spike to the coaching engine.
Confidence-Annotated Frame¶
Every signal carries metadata (from Pitwall ADR-001, adapted for pro hardware):
@dataclass
class SignalValue:
value: float
confidence: float # 0.0-1.0
source: str # "racelogic:gps", "racelogic:imu", "usb_can:can"
hz: float # actual update rate
stale: bool # no update in >2x expected period
@dataclass
class TelemetryFrame:
timestamp: float
sources: list[str]
# Position (fused)
latitude: SignalValue
longitude: SignalValue
speed: SignalValue
heading: SignalValue
# G-forces (Racelogic IMU, filtered)
g_lat: SignalValue
g_long: SignalValue
# Driver inputs (USB-CAN, direct)
throttle: SignalValue
brake: SignalValue
rpm: SignalValue
gear: SignalValue
steering: SignalValue
# Derived
distance: SignalValue # cumulative from GPS
corner_proximity: float # meters to nearest corner
current_corner: str | None # "Turn 3" or None
lap: int
lap_time: float
Stage 3: Distribution¶
The fused frame goes to two consumers simultaneously:
Hot Path Feed (Local, <20ms)¶
Direct in-process feed to Gemma 4 on the Pixel 10 TPU. No serialization, no network.
# In the fusion engine's output callback:
async def on_frame(frame: TelemetryFrame):
# Hot path: direct function call, no network
gemma_response = await gemma_engine.evaluate(frame)
if gemma_response:
await arbiter.submit(gemma_response)
# Buffer for DuckDB persistence
burst_buffer.append(frame)
Frame rate: 10Hz (every 100ms). The hot path evaluates every frame.
DuckDB Persistence (Local, in-process)¶
Telemetry frames are persisted locally to DuckDB as they arrive. No cloud dependency.
graph LR
FRAMES[Fused Frames<br/>10Hz continuous] --> BUFFER[Frame Buffer<br/>batch insert every 100ms]
BUFFER --> DUCK[DuckDB In-Process<br/>telemetry table<br/>session-local storage]
subgraph Analytics
DUCK --> LITERT[LitertCoach<br/>warm-path analysis<br/>Gold Standard comparison]
DUCK --> ANALYTICS[Session Analytics<br/>lap times, corners,<br/>scorecard, highlights]
end
Persistence cadence: Every 100ms (configurable via --can-flush-ms). Frames are batch-inserted into the telemetry wide table.
Reliability guarantee: DuckDB runs in-process with zero network dependency. Session data is persisted to disk and survives process restarts.
Burst format:
{
"session_id": "sonoma-2026-05-23-team2",
"burst_id": 42,
"timestamp_start": 1716483600.0,
"timestamp_end": 1716483610.0,
"frame_count": 500,
"driver_level": "intermediate",
"car": "BMW M3",
"track": "sonoma",
"frames": [
{
"timestamp": 1716483600.0,
"speed": {"value": 42.5, "confidence": 0.95, "source": "fused:kalman"},
"g_lat": {"value": 0.85, "confidence": 0.95, "source": "racelogic:imu"},
"brake": {"value": 72.0, "confidence": 0.95, "source": "usb_can:can"},
...
}
]
}
Local Analytics: DuckDB¶
In addition to feeding the hot path, the fusion engine maintains a DuckDB in-process database for all analytics and warm-path coaching:
# Append every frame to DuckDB in-memory table
duckdb.execute("""
INSERT INTO telemetry VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", [frame.timestamp, frame.speed.value, frame.g_lat.value, ...])
Use cases:
- Rolling G-force envelope (last 10 seconds) for friction circle HUD
- Lap time splits (computed from GPS crossing start/finish)
- Corner-by-corner metrics for the event-sourced driver profile
- Post-session aggregation that feeds the driver profile events
DuckDB is the sole persistence layer. All analytics, coaching, and session queries run locally with zero latency. There is no cloud dependency.
| Query | Engine | Latency |
|---|---|---|
| "What's my rolling max gLat?" | DuckDB | <1ms |
| "How does this lap compare to my best?" | DuckDB | <10ms |
| "How does Driver A compare to Driver B across all sessions?" | DuckDB | <100ms |
| "Generate the post-session debrief" | LitertCoach + DuckDB data | 2–15s |