Skip to content

Latest commit

 

History

History
459 lines (344 loc) · 14.8 KB

File metadata and controls

459 lines (344 loc) · 14.8 KB

Universal Telemetry Software

Complete DAQ telemetry system for Formula Racing vehicles. The car and base station share the same Python source and role-specific runtime logic. The car Raspberry Pi runs that code natively as car-telemetry.service; base stations run it through Docker Compose with Redis, WebSocket/status services, video relay, and optional TimescaleDB logging.


Architecture

graph LR
  subgraph CAR["CAR — Raspberry Pi (systemd)"]
    CAN["CAN Reader\n(can0)"] --> UDP["UDP Sender\n(batch 20msg/50ms)"]
    CAN --> RB["Ring Buffer\n(60 sec)"]
    RB --> TCP_S["TCP Resend Server\n(:5006)"]
    CAM["USB Camera"] --> FFMPEG["ffmpeg\nx264 encoder"]
    FFMPEG -- "RTSP ANNOUNCE\nTCP :8554" --> MTX_PUSH["→ MediaMTX"]
    CTRL["Video Control\nHTTP :8081"] -. "quality preset" .-> FFMPEG
  end

  subgraph BASE["BASE — MacBook / RPi"]
    UDP_R["UDP Receiver"] --> Redis["Redis Publisher"]
    Redis --> WS["WebSocket Bridge\n(:9080)"]
    Redis --> STATUS["Status HTTP Server\n(:8080)"]
    TCP_C["TCP Client\n(recovery)"] --> Redis
    MTX["MediaMTX\n(:8554 RTSP in\n:8889 WebRTC out)"]
  end

  UDP -- "UDP :5005" --> UDP_R
  TCP_S -- "TCP :5006" --> TCP_C
  MTX_PUSH -- "RTSP push" --> MTX
  WS --> PECAN["PECAN Dashboard\n(:3000)"]
  MTX -- "WebRTC WHEP\n:8889" --> PECAN
  PECAN -. "quality change\nHTTP :8081" .-> CTRL
Loading

The deployed car service sets ROLE=car explicitly in deploy/car-telemetry.service. Base deployments run the same code with the base role inside Docker Compose. Auto-detection still exists for local development, but production deployment should use the role-specific wrappers: systemd on the car, Docker Compose on the base station.


Hardware Setup (Ubuntu)

This section covers setting up a CAN HAT (e.g. MCP2515-based) on Ubuntu before running the software.

1. Enable the CAN HAT kernel module

Install the SocketCAN tools:

sudo apt update && sudo apt install -y can-utils

Load the CAN modules at boot:

echo "can" | sudo tee -a /etc/modules
echo "can_raw" | sudo tee -a /etc/modules
echo "mcp251xfd" | sudo tee -a /etc/modules

2. Configure the HAT overlay

Edit /boot/firmware/config.txt (Ubuntu on RPi uses /boot/firmware/, not /boot/):

dtoverlay=mcp251xfd,oscillator=20000000,interrupt=25
dtoverlay=spi-bcm2835

Our HAT uses the MCP2517FD CAN FD controller with a 20 MHz crystal (S73305 20.000X15R) and MCP2562FDT-HSN transceiver. The interrupt GPIO (25) may need to be adjusted depending on how your HAT is wired — check the HAT schematic.

Reboot after editing:

sudo reboot

3. Bring up can0 automatically on boot

Create a systemd service to bring up the interface on boot:

sudo nano /etc/systemd/system/can0.service

Paste:

[Unit]
Description=CAN bus interface can0
After=network.target

[Service]
Type=oneshot
ExecStart=/sbin/ip link set can0 up type can bitrate 500000
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable can0
sudo systemctl start can0

4. Verify CAN is working

Check the interface is up:

ip link show can0
# Should show: can0: <NOARP,UP,LOWER_UP> ...

Listen for CAN frames (requires a live CAN bus):

candump can0

Send a test frame on a loopback setup:

sudo ip link set can0 type can loopback on
cansend can0 123#DEADBEEF
candump can0

Once can0 is confirmed working, proceed to deployment.


Quick Start

Prerequisites

  • Raspberry Pi 4/5 with Ubuntu
  • uv installed on the car Pi
  • Docker and Docker Compose installed on the base station
  • CAN HAT set up (see Hardware Setup above)
  • Network connection between car and base (LAN cable or Ubiquiti radios)

Car RPi Installation

Clone the repository:

git clone https://github.com/Western-Formula-Racing/data-acquisition.git /home/car/data-acquisition
cd /home/car/data-acquisition/universal-telemetry-software

Install dependencies and the native systemd service:

uv sync
sed -i "s/GIT_HASH=unknown/GIT_HASH=$(git rev-parse --short HEAD)/" deploy/car-telemetry.service
sudo cp deploy/car-telemetry.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable car-telemetry
sudo systemctl start car-telemetry

The service file currently sets REMOTE_IP=10.71.1.20 for the base station. Edit deploy/car-telemetry.service before installing it if the base station IP changes.

Base Station Installation

MacBook base station:

docker compose -f deploy/docker-compose.macbook-base.yml --env-file deploy/.env.macbook up -d

Raspberry Pi base station:

docker compose -f deploy/docker-compose.rpi-base.yml up -d

Verify

Check car logs:

journalctl -u car-telemetry -f

Check base logs:

docker compose -f deploy/docker-compose.macbook-base.yml logs -f telemetry

Access

Interface URL
Status page http://<ip>:8080
PECAN dashboard http://<ip>:3000
WebSocket ws://<ip>:9080

Deploying with Pre-built Images (GHCR)

Use docker-compose.macbook-base.yml for MacBook or docker-compose.rpi-base.yml for Pi base station. Both pull pre-built images from GHCR. The car does not use these Docker images in production; use deploy/car-telemetry.service instead.

# MacBook minimal LAN stack
docker compose -f deploy/docker-compose.macbook-base.yml --env-file deploy/.env.macbook up -d

# Optional local TimescaleDB writes
docker compose --profile timescale -f deploy/docker-compose.macbook-base.yml --env-file deploy/.env.macbook up -d

# RPi base station (ephemeral, no DB persistence)
docker compose -f deploy/docker-compose.rpi-base.yml up -d

Images are built for both linux/amd64 and linux/arm64 (Raspberry Pi).


Configuration

Environment Variables

Variable Default Description
ROLE auto Force car or base (auto-detects from can0)
REMOTE_IP 192.168.1.100 IP of the other RPi
UDP_PORT 5005 Real-time UDP streaming port
TCP_PORT 5006 TCP retransmission port
REDIS_URL redis://localhost:6379/0 Redis connection
WS_PORT 9080 WebSocket port for PECAN
STATUS_PORT 8080 HTTP port for status page
SIMULATE false Simulate CAN data (no hardware needed)
ENABLE_VIDEO false Enable video streaming (car: push RTSP; base: unused)
ENABLE_AUDIO false Enable audio streaming
ENABLE_TIMESCALE_LOGGING false Log telemetry to TimescaleDB; MacBook default is auto
ENABLE_WS_RELAY false Enable downlink-only WebSocket relay for remote viewers
RELAY_TOKEN unset Optional passcode for relay connections (?token=...)
RELAY_LISTEN_PORT 9089 Local port for the relay WebSocket server
RELAY_UPSTREAM_WS ws://127.0.0.1:9080 Upstream base-station WebSocket consumed by the relay
RELAY_REQUIRE_TOKEN_ON_LAN false Require token for LAN clients too, not just loopback/public clients
RTSP_PORT 8554 Port on base station where MediaMTX accepts RTSP push
VIDEO_STREAM_NAME car-camera RTSP/WebRTC stream path name
VIDEO_WIDTH 848 Capture width (overridden by quality preset)
VIDEO_HEIGHT 480 Capture height (overridden by quality preset)
VIDEO_FPS 30 Frame rate
VIDEO_BITRATE 800 Encoder bitrate in kbps (overridden by quality preset)
VIDEO_CONTROL_PORT 8081 HTTP port for runtime quality control on car

Ports

Port Protocol Purpose
5005 UDP CAN data streaming
5006 TCP Packet retransmission
6379 TCP Redis (internal)
8080 HTTP Status monitoring page
9080 WebSocket PECAN dashboard feed
9089 WebSocket Downlink-only relay for remote viewers
3000 HTTP PECAN dashboard UI
8081 HTTP Video quality control (car only, when ENABLE_VIDEO=true)
8554 TCP RTSP — car pushes H.264 to MediaMTX on base
8889 HTTP WebRTC WHEP — Pecan pulls video from MediaMTX

Video Streaming

Video uses a push architecture: the car Pi encodes H.264 with ffmpeg and pushes RTSP to MediaMTX running on the base station. Pecan receives the stream via WebRTC (WHEP protocol).

Car Pi (ffmpeg x264) → RTSP ANNOUNCE/RECORD → MediaMTX (:8554)
                                                    ↓
                                    WebRTC WHEP (:8889) → Pecan browser

Why MediaMTX

go2rtc (previous relay) does not support RTSP ANNOUNCE/RECORD (push). MediaMTX natively supports both push and WebRTC output with sub-second latency.

Encoder settings

ffmpeg runs on the car with libx264 -preset ultrafast -tune zerolatency. Key parameters:

Parameter Value Reason
-preset ultrafast Minimum encode latency
-tune zerolatency Disables B-frames and lookahead
-g 15 keyframe every 0.5s WebRTC can recover from packet loss within 0.5s
-bf 0 no B-frames Eliminates reorder buffer delay
-threads 1 Avoids thread synchronisation stalls
-rtsp_transport tcp MediaMTX only accepts TCP for incoming ANNOUNCE/RECORD

Quality presets

Selectable live from Pecan's video feed (bottom-right). Pecan POSTs to the car's control server (http://10.71.1.10:8081/video/quality), which restarts ffmpeg with new encoder params.

Preset Resolution Bitrate
360p (low) 640×360 500 kbps
480p (medium, default) 848×480 800 kbps
720p (high) 1280×720 2000 kbps

Camera focus

If using a USB camera with autofocus that parks at infinity, set manual focus via v4l2:

# Disable autofocus and set manual focus (0=close, 255=infinity)
v4l2-ctl --device /dev/video0 --set-ctrl focus_automatic_continuous=0
v4l2-ctl --device /dev/video0 --set-ctrl focus_absolute=40

Monitoring

Status page (http://<ip>:8080): real-time connection status, unified system health, packet stats, live packet rate chart.

Health endpoint (http://<ip>:8080/health): JSON health snapshot for the base stack. It is served by the status server and backed by the telemetry process' HEALTH_FILE snapshot, which defaults to /tmp/daq-health.json. The endpoint reports infrastructure components separately from car presence, so a powered-off car does not imply Redis/WebSocket/Timescale are unhealthy.

PECAN dashboard (http://<ip>:3000): live CAN message visualization. Connects automatically to WebSocket on port 9080.


Remote Viewing via WebSocket Relay

The base station can publish the live telemetry WebSocket through a downlink-only relay on port 9089. The relay connects upstream to the normal local WebSocket (9080) and rebroadcasts frames to viewers; downstream messages are not forwarded back to the car or base station.

Enable the relay

For the MacBook base stack the local relay process is enabled in the telemetry container:

ENABLE_WS_RELAY=true
RELAY_UPSTREAM_WS=ws://127.0.0.1:9080
RELAY_LISTEN_PORT=9089
RELAY_TOKEN=<optional passcode>

You can set or change the token from the status page at http://localhost:8080. Tokens saved from the UI take effect for new relay connections immediately.

Local viewers can connect to:

ws://<base-station-ip>:9089?token=<token>

Forward with Cloudflare Tunnel

Cloudflare Tunnel is the recommended way to expose the relay as secure wss:// without opening inbound firewall ports.

Install and log in:

brew install cloudflared
cloudflared tunnel login

Create a tunnel:

cloudflared tunnel create daq-ws-relay

Create ~/.cloudflared/config.yml:

tunnel: <tunnel-id-or-name>
credentials-file: /Users/<you>/.cloudflared/<tunnel-id>.json

ingress:
  - hostname: daq-relay.example.com
    service: http://localhost:9089
  - service: http_status:404

Route DNS:

cloudflared tunnel route dns daq-ws-relay daq-relay.example.com

Run the tunnel while the base station stack is up:

cloudflared tunnel run daq-ws-relay

Remote viewers connect with:

wss://daq-relay.example.com?token=<token>

This same relay can be published by any reverse tunnel provider; Cloudflare is only the documented default.


Redis Channels

can_messages

Published by base station, consumed by PECAN.

[{ "time": 1234567890, "canId": 256, "data": [146, 86, 42, 123, 205, 255, 0, 0] }]

system_stats

Published by base station every second.

{ "received": 45, "missing": 1, "recovered": 0 }

Troubleshooting

No data flowing

systemctl status car-telemetry                         # car: check native service
journalctl -u car-telemetry -f                         # car: confirm sending
docker compose logs telemetry | grep "Initial sequence" # base: confirm receiving
ping <other-rpi-ip>                                    # confirm network

WebSocket not connecting

docker compose logs telemetry | grep "WebSocket"
# Expected: "WebSocket server running at ws://0.0.0.0:9080"

can0 not detected

ip link show can0       # check interface exists
dmesg | grep mcp        # check for kernel errors loading the HAT driver
systemctl status can0   # check the bring-up service

File Structure

universal-telemetry-software/
├── main.py                     # Main orchestrator
├── src/
│   ├── data.py                 # UDP/TCP + Redis (car & base)
│   ├── audio.py                # Audio streaming
│   ├── video.py                # Video streaming
│   ├── websocket_bridge.py     # Redis -> WebSocket for PECAN
│   ├── timescale_bridge.py     # TimescaleDB logging (Redis → server TimescaleDB)
│   ├── leds.py                 # LED status indicators
│   ├── link_diagnostics.py     # Radio link health
│   └── poe.py                  # PoE monitor
├── tests/
├── deploy/
│   ├── car-telemetry.service           # Native car systemd service
│   ├── CAR_DEPLOY.md                   # Car RPi systemd deployment
│   ├── docker-compose.macbook-base.yml # MacBook base stack with optional profiles
│   ├── docker-compose.rpi-base.yml     # RPi lightweight base (ephemeral)
│   ├── docker-compose.staging.yml      # Staging (:test-latest images)
│   ├── docker-compose.test.yml         # Integration test stack (CI)
│   ├── docker-compose.can-test.yml     # vCAN pipeline tests
│   ├── docker-compose.jitsi.yml        # Optional Jitsi comms addon
│   └── WHICH_ONE.md                # Compose file reference
├── Dockerfile
└── requirements.txt

Built by Western Formula Racing — London, Ontario, Canada