Real-time telemetry platform for a Formula Student electric race car. Streams simulated vehicle data over WebSocket, visualises it in a live dashboard, and supports session recording, replay, and post-run analysis.
| Layer | Technologies |
|---|---|
| Frontend | React 18, TypeScript, Vite, Tailwind CSS v4, Recharts, Radix UI |
| Backend | Python 3.12+, FastAPI, Uvicorn, Pydantic v2, WebSockets |
| Data | JSONL run logs, structured metadata files |
| Testing | Pytest, Ruff, TypeScript strict mode |
| DevOps | Docker Compose, GitHub Actions CI, Makefile |
┌─────────────────────────────────────────────────────────────────────────┐
│ Telemetry Simulator (driving-state machine) │
│ States: PRECHARGING → READY_TO_DRIVE → ACCELERATING → CORNERING │
│ → BRAKING → COOLDOWN (→ rare FAULT) │
│ Correlated signals: APPS → torque → current → voltage sag → heating │
└────────────────────────────────┬────────────────────────────────────────┘
│ TelemetryFrame @ SAMPLE_HZ
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ FastAPI Backend │
│ ├─ GET /health System status + uptime │
│ ├─ WS /ws/telemetry Live frame stream │
│ ├─ WS /ws/replay/{run_id} Recorded session playback │
│ ├─ POST /api/recording/start Begin JSONL capture │
│ ├─ POST /api/recording/stop End capture + finalize metadata │
│ ├─ GET /api/recording/status Current recording state │
│ ├─ GET /api/runs List recorded sessions │
│ ├─ GET /api/runs/{run_id} Run metadata + frame count │
│ └─ GET /api/runs/{id}/download ZIP archive of run data │
└────────────────────────────────┬────────────────────────────────────────┘
│ WebSocket JSON frames
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ React Dashboard │
│ ├─ Overview Speed, power, SOC, temps, safety, recording │
│ ├─ Accumulator Cell voltages, temperatures, gauges, charts │
│ ├─ Drivetrain Torque, RPM, inverter state, thermal derating │
│ ├─ Vehicle Dynamics G-forces, wheel speeds, driver inputs, yaw │
│ ├─ Diagnostics WS status, CAN load, RSSI, CPU, storage │
│ └─ Runs Record, list, inspect, replay, download │
└─────────────────────────────────────────────────────────────────────────┘
- Python 3.12+
- Node.js 20+ (22 recommended)
- (Optional) Docker and Docker Compose
make install# Terminal 1 — backend
cd backend
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Terminal 2 — frontend
cd frontend
npm run devBackend: http://localhost:8000/health Dashboard: http://localhost:5173
docker compose up --build| Method | Path | Description |
|---|---|---|
GET |
/health |
System health, sample rate, uptime, connected clients (public) |
POST |
/api/auth/login |
OAuth2 password form → JWT access token |
GET |
/api/auth/me |
Current user profile (Bearer token) |
WS |
/ws/telemetry?token=… |
Live telemetry frame stream (JWT query param) |
WS |
/ws/replay/{run_id}?token=… |
Replay recorded run at original sample rate |
GET |
/api/recording/status |
Current recording state |
POST |
/api/recording/start |
Begin recording — engineer or admin only |
POST |
/api/recording/stop |
Stop recording — engineer or admin only |
GET |
/api/runs |
List all recorded runs |
GET |
/api/runs/{run_id} |
Run metadata and frame count |
GET |
/api/runs/{run_id}/download |
Download run as ZIP |
GET |
/api/runs/{run_id}/jsonl |
Download raw JSONL |
JWT bearer tokens protect all routes except GET /health and POST /api/auth/login. WebSockets accept the same token as a token query parameter (browsers cannot set WebSocket headers).
Roles
| Role | Recording | Runs / replay / download |
|---|---|---|
viewer |
No | Yes |
engineer |
Yes | Yes |
admin |
Yes | Yes |
Completed runs include a recorded_by field (username of whoever started the session).
Default dev users (seeded automatically on first startup when the user table is empty):
| Username | Password | Role |
|---|---|---|
admin |
admin |
admin |
engineer |
engineer |
engineer |
viewer |
viewer |
viewer |
Set SECRET_KEY to a strong random value in production (openssl rand -hex 32). Token lifetime (8 hours) and algorithm (HS256) are application constants, not environment variables.
Login example
TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=engineer&password=engineer' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/runsSchema changes: If you already have a Postgres volume from before auth, reset it once: docker compose down -v && docker compose up --build.
{
"t": 142,
"timestamp": "2026-05-23T01:45:12.345678+00:00",
"run_id": "RUN-SIM-001",
"mode": "MOCK",
"driving_state": "ACCELERATING",
"battery": {
"soc": 78.4,
"ts_voltage": 388.2,
"ts_current": 152.3,
"min_cell_voltage": 3.741,
"max_cell_voltage": 3.782,
"avg_cell_voltage": 3.762,
"max_cell_temp": 34.2,
"min_cell_temp": 30.8,
"temp_spread": 3.4,
"cell_delta_mv": 41.0,
"pack_power_kw": 59.1
},
"drivetrain": {
"motor_rpm": 4680,
"torque_request": 142.5,
"torque_actual": 138.2,
"inverter_temp": 48.3,
"motor_temp": 52.1,
"dc_bus_voltage": 388.2,
"derating": false,
"inverter_state": "ENABLED"
},
"vehicle": {
"speed": 62.4,
"apps": 68.2,
"brake_front": 0.0,
"brake_rear": 0.0,
"steering_angle": 3.2,
"yaw_rate": 8.4,
"lat_g": 0.38,
"long_g": 0.52,
"wheel_speed_fl": 62.1,
"wheel_speed_fr": 62.8,
"wheel_speed_rl": 63.2,
"wheel_speed_rr": 63.5
},
"safety": {
"ams": "OK",
"imd": "OK",
"sdc": "CLOSED",
"precharge": "COMPLETE",
"air_positive": "CLOSED",
"air_negative": "CLOSED",
"rtd": "ON",
"active_faults": 0
},
"logger": {
"recording": true,
"storage_gb_free": 112.8,
"cpu_temp": 48.2,
"dropped_frames": 0,
"can_bus_load": 28.4,
"can_errors": 0,
"telemetry_rssi": -54.0,
"packet_loss": 0.12
}
}Schema: backend/app/schemas/telemetry.py
The telemetry simulator is a driving-state machine that cycles through track phases:
PRECHARGING → READY_TO_DRIVE → ACCELERATING → CORNERING → BRAKING → COOLDOWN → (repeat)
Rare FAULT events interrupt the cycle temporarily.
Signals are correlated, not random:
- APPS → torque request → torque actual (with lag)
- Torque → current draw → voltage sag → pack power
- Sustained power → cell/inverter/motor heating → derating
- Braking → speed reduction; cornering → steering/yaw/lateral G
- SOC decreases under load
- Wheel speeds differ under traction slip
- CAN load and packet loss correlate with activity
Start/stop via the dashboard UI or API:
curl -X POST http://localhost:8000/api/recording/start \
-H "Content-Type: application/json" \
-d '{"driver": "Alex Kim", "event_type": "endurance", "notes": "Session 3"}'
curl -X POST http://localhost:8000/api/recording/stopRuns are stored in RUN_DATA_DIR (default backend/data/runs/):
RUN-20260523-014512-A3B7C1.jsonl ← one frame per line
RUN-20260523-014512-A3B7C1.meta.json ← metadata (driver, event, times, frame count)
Replay in dashboard or via WebSocket at /ws/replay/{run_id} (closes with code 4200 on completion).
make test| Test file | What it covers |
|---|---|
test_imports.py |
Module structure and import paths |
test_simulator.py |
Frame validity, signal bounds, correlations, state cycling |
test_recorder.py |
JSONL persistence, metadata, path traversal safety |
test_api.py |
Health, recording lifecycle, runs, WebSocket frames |
Copy the template and edit your values:
cp .env.example .envSingle file at the repo root (.env) — backend and frontend both read it.
backend/
app/
api/routes/ Route handlers
core/ Config, state, dependencies
schemas/ Pydantic models
services/
simulator/ Driving-state machine
telemetry/ WS hub, broadcast, replay
recorder.py JSONL recording
utils/ Helpers
main.py App factory
tests/
frontend/
src/
features/
dashboard/ Tab views
runs/ Recording panel
components/ Shared UI
hooks/ useTelemetry, useTheme
lib/ Types, config, utilities
docker-compose.yml
Makefile
.github/workflows/ci.yml
| Issue | Fix |
|---|---|
| Dashboard stuck on "Connecting" | Start backend first; check VITE_TELEMETRY_WS_URL |
| CORS errors | Add your frontend origin to CORS_ORIGINS |
| Empty runs list | Record a session first |
npm run build fails |
Use Node 20+; run npm install |
| Tests fail on import | Run pytest from backend/ with deps installed |