This is a project to build out a telemetry system, specifically on a 1992 Honda Accord for a realtime dashboard and spectator livestream.
We use a combination of analog engine taps, GPS, and cameras streaming data back to a Jetson Nano through a 5G Modem, back to home base where a ground computer exposes all the data via a custom web dashboard.
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Arduino Mega β β RaceBox β β Camera1 β
β ECT TPS MAP β β Micro β β Camera2 β
β Brake Vbatt β β GPS/IMU β β Microphone β
β RPM VSS β β 25Hz β ββββββββ¬ββββββββ
ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β serial 115200 β BLE β USB
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Jetson Orin NX β
β β
β serial-bridge βββ β
β ββββΊ telem-server β
β racebox-bridge ββ (WAL engine) β
β β β
β video-streaming HTTP :4400 β
β (GStreamer/SRT) UDP ticks (live) β
β cam1 :9000 msgpack /wal/range β
β cam2 :9001 SSE /sessions/:id β
β audio :9002 β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββ
β Ethernet
βΌ
ββββββββββββββββ
β Cell Modem β
β GL-X3000 β
ββββββββ¬ββββββββ
β 4G/5G Β· Starlink Β· Tailscale
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Ground Computer(s) β
β β
β udp-client-receiver β
β udp :4402 βββΆ SSE :4401 (loopback) β
β β β
β ββββββββΌββββββ ββββββββββββββββββββ β
β β Browser β β OBS Studio β β
β β (Vite) β β SRT ingest β β
β β β β cam1/cam2 audio β β
β β Dashboard β β Browser src β β
β β Review β β overlays β β
β β Debug β ββββββββββ¬ββββββββββ β
β β Editor β β β
β ββββββββββββββ β β
βββββββββββββββββββββββββββββββββΌββββββββββββββ
β RTMP
βΌ
ββββββββββββ
β Twitch β
ββββββββββββ
src/ Arduino Mega firmware (PlatformIO)
server/ Node.js telemetry server + WAL engine
src/ Core: wal.ts, http.ts, udp-sender.ts, sessions.ts, lap-detector.ts, gear.ts, sensors.ts
scripts/ serial-bridge.ts, gen-data.ts, compact.ts, repair-sessions.ts
client/ Vite multi-page web app
src/ TypeScript sources for each page
udp-client-receiver/ Ground-station live-telemetry receiver (runs on your laptop)
bluetooth/ RaceBox BLE bridge (node-ble, UBX protocol)
streaming/ GStreamer video/audio capture scripts
tracks/ Track geometry JSON files (Sonoma, Sharon, etc.)
fonts/ Berkeley Mono
| Component | Model | Specs | Power | Cost | Category | Notes |
|---|---|---|---|---|---|---|
| Forward Wide Camera | Logitech C920x | 1080p Γ 30fps | USB 2.5W | $70 | Video | Driver POV |
| Driver Camera | Logitech C920x | 1080p Γ 30fps | USB 2.5W | $70 | Video | Shifting and pedal movements |
| On Board Compute | Jetson Orin NX | 16GB RAM, NVENC accelerator | 12V/5A β 60W max | $1150 | Compute | Hardware encoding accelerator avoids video bottleneck |
| 5G Modem | GL-X3000 | 5G, physical SIM | 12V/2.5A β 30W max | $323 | Connectivity | |
| SIM Card + Plan | Visible+ Pro | Unlimited data | β | $45/mo | Connectivity | Verizon works well at Thunderhill and Sonoma |
| GPS/Accel | RaceBox Micro | 25Hz, <1m accuracy | 12V, 0.2W max | $125 | Telemetry | Also works as standalone product with phone |
| Bluetooth Dongle | UD100-G03 | BLE 4.0 | USB, 2.5W max | $39 | Telemetry | Jetson lacks built-in BT; needed for RaceBox BLE |
| Microcontroller | Arduino Mega 2560 | 54 digital I/O, 16 analog inputs | USB, 1W max | $49 | Telemetry | Overkill; smaller 5V Arduino would suffice |
| Microphone | LavMicro-U | USB lavalier | USB, 0.5W | $30 | Audio | In-car audio, Opus 64kbps |
- Total cost:
~$1850for listed parts.~$2000when including random wires, resistors/diodes, butt joints, t splices, small wires, solder, perfboard, zipties, etc. - Total power:
~100Wmax theoretical, in practice steady state power draw was likely closer to20-30Wmaximum
| Telemetry Point | Sense Strategy | Signal Type | Arduino Pin | Sense Line |
|---|---|---|---|---|
| Video 1 | Camera | USB | β | β |
| Video 2 | Camera | USB | β | β |
| Car Audio | Microphone | USB | β | β |
| Brake Indicator | Binary yes/no voltage | 12V divided down 4.3Γ | A5 | White/Green brake light line |
| Battery Voltage | Analog | 12V divided down 4.3Γ | A6 | Tap off PDB +12V bus |
| Oil Pressure | AEM 30-2130-100, 0.5β4.5V = 0β100 psi | 5V analog | A0 | Oil pressure sender |
| Oil Temperature | AEM 30-2013 lookup table, 2.2kΞ© pull-up | Resistive | A1 | Oil temperature sender |
| Throttle Position | Calibrated 0β100% | 5V analog | A9 | D11 ECU D connector |
| Engine Coolant Temp | Lookup table | 5V analog | A8 | D13 ECU D connector |
| MAP | Lookup table | 5V analog | A10 | D13 ECU D connector |
| RPM (Tach) | Instantaneous pulses/sec | 12V square wave, stepped down to 5V | D18 | A7 BLU Dash connector |
| VSS | Instantaneous pulses/sec | 12V square wave, stepped down to 5V | D19 | B10 ECU B connector |
| GPS | RaceBox | Digital | β | β |
| Accel | RaceBox | Digital | β | β |
| Gyro | RaceBox | Digital | β | β |
We ran driver communication completely parallel to telemtry. We would communicate with drivers via discord audio call. This meant that if the telemetry stack restarted, our audio communications persisted. This was very useful during hotpits when we shut off telemetry, or if we ever had to crank the starter on track.
Custom append-only write-ahead log designed for low memory usage on the Jetson.
- Disk format: NDJSON with merged channels per line:
{"seq":1,"ts":...,"d":{"rpm":3500,"speed":65}} - Per-batch seq: One sequence number per ingest call (not per channel)
- File rotation: 5,000 ticks per file (~200s of data), range footer on each file (
#range:min,max) - In-memory index:
{file, minSeq, maxSeq}[]built on startup for fast range queries - Ring buffers: Per-channel in-memory (6,000 entries) for live SSE streaming
- Compaction: Merges same-timestamp lines within 50ms buckets, reassigns sequential seqs, repairs session pointers
- Locking:
wal.lockfile prevents concurrent servers and protects during compaction. Server returns 503 on WAL routes while compacting. - Wire format: MessagePack via
msgpackr(~40% smaller than JSON)
Live telemetry is lossy UDP, terminated on each laptop by
udp-client-receiver/. Replay is unaffected β it still
pulls complete data from the WAL over HTTP.
Live telemetry used to be SSE over TCP straight from the car, and on a lossy 5G link it ran 14 seconds behind and never recovered: TCP retransmitted samples that were already obsolete, and the server buffered writes without bound. Video on the same modem self-restored because SRT discards late packets β this gives telemetry the same property.
car ββUDP msgpack ticksβββΆ udp-client-receiver ββSSE localhost:4401βββΆ browsers
βββ leased subscription, renewed every 5s βββΆ car
- Merged ticks. One datagram per WAL batch (
{seq, ts, d:{...}}), the same shape as the disk format β not one frame per channel. ~4Γ smaller. - Leased. The car only sends to endpoints holding a live lease (15s TTL). A laptop that closes its lid stops costing uplink within the TTL.
- One stream per laptop. All local browsers share one subscription, so opening the debug page mid-session costs the car nothing. ~2.6 Mbps β ~136 kbps.
- Gaps are permanent. Lost datagrams are never retransmitted. The dashboard's
lossfield shows the rate;lagshould stay near zero regardless.
Every laptop runs its own receiver β they're independent subscribers and never proxy for each other. See udp-client-receiver/README.md.
The car's SSE /stream still exists for ?local development and returns
per-channel entries; /sessions/:id/stream still comes straight from the car.
| Page | Path | Purpose |
|---|---|---|
| Dashboard | / |
Live gauges (speed/RPM/TPS/brake), GPS maps (follow + overview), g-force dial, diagnostics, lap times with pace delta |
| Review | /review.html |
Session/lap browser, seekable replay, trail color modes (speed/throttle/RPM/brake), IndexedDB cache |
| Debug | /debug.html |
Auto-discovered channels with sparklines, systemd service management, camera controls |
| Editor | /editor.html |
Track polyline editor with Leaflet, toolbar modes, bearing slider, satellite toggle |
| Stream: Map | /stream/map.html |
Transparent overlay β track + car position (for OBS) |
| Stream: Car Data | /stream/car_data.html |
Transparent overlay β value + sparkline gauges |
| Stream: Lap Data | /stream/lap_data.html |
Transparent overlay β driver, lap count, timer, delta |
GStreamer pipelines on the Jetson, SRT over Tailscale:
- Video:
v4l2src β jpegdec β nvvidconv β nvv4l2h264enc (4Mbps, GOP 15) β MPEG-TS β SRT - Audio:
alsasrc (40ms buffer) β Opus (64kbps, 10ms frames) β MPEG-TS β SRT - SRT latency: 100ms (tuned for ~150ms Tailscale RTT)
- C930e always pinned to port 9000 regardless of USB enumeration
The Jetson listens (SRT listener) and OBS dials in, so the car can boot and sit there streaming to nobody until a viewer connects.
Add one Media Source per stream, uncheck Local File, and set Input:
| Source | Input | Input Format |
|---|---|---|
| Camera 1 | srt://gearados-nx:9000?mode=caller&latency=50000 |
|
| Camera 2 | srt://gearados-nx:9001?mode=caller&latency=50000 |
|
| Engine Mic | srt://gearados-nx:9002?mode=caller |
mpegts |
Set Reconnect Delay to 1s and Buffering to 0 MB on each, so a dropped stream comes back on its own without buying that back in latency.
latency is in microseconds here β ffmpeg's units, not the milliseconds the
GStreamer side takes, so 50000 is 50ms. SRT runs the link at whichever end
asks for more, so the sender's 100ms is what you actually get.
Overlays layer on top as Browser Sources pointing at the /stream/*.html pages.
Nothing to configure on the Jetson β it binds 0.0.0.0:9000-9002 and takes
whoever connects. Any machine on the tailnet points its Media Sources at
gearados-nx and gets the feed; two machines can pull at once.
Use the Tailscale name, not a raw 100.x address. Tailscale reassigns those per
tailnet, so a stale one quietly points at nothing.
Connection does not exist in the OBS log means the Jetson end isn't up: check
systemctl status video-streaming, and that gearados-nx resolves. A pipeline
that dies still takes all three down together, with systemd retrying every 5s.
GPS-based β no trackside hardware needed.
- Project GPS position onto track centerline polyline β normalized progress 0β1
- Detect finish line crossing:
prevProgress > 0.85 && currentProgress < 0.15 - First lap auto-flagged as out lap, session stop flags current lap as in lap
- Pace delta: progress-vs-time curve from best lap, interpolated at current position
On the Jetson:
# Install deps + systemd services
./setup.sh
# Check service status
./status.sh
# View logs
journalctl -u telem-server -u serial-bridge -u racebox-bridge -u video-streaming -f| Service | Description |
|---|---|
racebox-connect |
BLE connection to RaceBox Micro |
telem-server |
WAL telemetry server (port 4400) |
racebox-bridge |
RaceBox BLE β telem ingest |
serial-bridge |
Arduino Mega serial β telem ingest |
video-streaming |
GStreamer camera streams over SRT |
To watch the real car, you need the receiver running β live telemetry arrives over UDP and nothing reaches the browser without it:
# Terminal 1 β live telemetry receiver
cd udp-client-receiver && npm start
# Terminal 2 β client (Vite dev server, port 5173)
cd client && npm run devOpen http://localhost:5173.
For fully local work against synthetic data, run a server and generator instead,
and add ?local to the URL β that points the live feed back at the local
server's SSE so no receiver is needed:
# Terminal 1 β server
cd server && npx tsx src/main.ts
# Terminal 2 β synthetic telemetry
cd server && npx tsx scripts/gen-data.ts
# Terminal 3 β client
cd client && npm run devOpen http://localhost:5173?local.
To exercise the real UDP path locally, skip ?local and point the receiver at
your own server: cd udp-client-receiver && JETSON_URL=http://localhost:4400 npm start.
# Tests
cd server && npx vitest run
# Generate synthetic data
cd server && npx tsx scripts/gen-data.ts
# Compact WAL (run offline or delegates to server if running)
cd server && npx tsx scripts/compact.ts --data-dir ./data# Upload Arduino Mega firmware
pio run -e mega_serial -t uploadcurl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules
sudo service udev restart

