Skip to content
Merged

Dev #65

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions .github/workflows/kvaser-bridge-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
name: Build Kvaser Bridge Binaries

on:
workflow_dispatch:
inputs:
release_tag:
description: 'Optional existing release tag to attach binaries to (for example: v1.2.3)'
required: false
type: string
push:
branches: [ main, dev ]
release:
types: [ published ]

jobs:
build-binaries:
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact_name: kvaser-bridge-windows
release_asset: kvaser-bridge-windows.exe
- os: macos-latest
artifact_name: kvaser-bridge-macos
release_asset: kvaser-bridge-macos.app.zip

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install bridge dependencies
run: |
python -m pip install --upgrade pip
pip install -r kvaser-bridge/requirements.txt

- name: Build standalone binary
shell: bash
run: |
cd kvaser-bridge
[[ -f build.spec ]] || { echo 'Missing kvaser-bridge/build.spec in repository checkout.'; exit 1; }
pyinstaller --clean --noconfirm build.spec

- name: Prepare Windows release asset
if: runner.os == 'Windows'
shell: pwsh
run: |
Copy-Item -Path "kvaser-bridge/dist/kvaser-bridge.exe" -Destination "kvaser-bridge/dist/kvaser-bridge-windows.exe" -Force

- name: Package macOS app bundle
if: runner.os == 'macOS'
shell: bash
run: |
cd kvaser-bridge/dist
[[ -d kvaser-bridge.app ]] || { echo 'Expected macOS app bundle not found.'; exit 1; }
ditto -c -k --sequesterRsrc --keepParent kvaser-bridge.app kvaser-bridge-macos.app.zip

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: kvaser-bridge/dist/${{ matrix.release_asset }}
if-no-files-found: error
retention-days: 30

publish-rolling-release:
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')
runs-on: ubuntu-latest
needs: build-binaries
permissions:
contents: write

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: release-assets

- name: Select rolling release metadata
id: meta
shell: bash
run: |
if [[ "${GITHUB_REF_NAME}" == "main" ]]; then
echo "tag=kvaser-bridge-main-latest" >> "$GITHUB_OUTPUT"
echo "name=Kvaser Bridge - Main Latest" >> "$GITHUB_OUTPUT"
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "tag=kvaser-bridge-dev-latest" >> "$GITHUB_OUTPUT"
echo "name=Kvaser Bridge - Dev Latest" >> "$GITHUB_OUTPUT"
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi

- name: Move rolling tag to current commit
shell: bash
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -f "${{ steps.meta.outputs.tag }}" "${GITHUB_SHA}"
git push origin "refs/tags/${{ steps.meta.outputs.tag }}" --force

- name: Create or update rolling release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
notes="Automated binary build from ${GITHUB_REF_NAME} at ${GITHUB_SHA}."
if gh release view "${{ steps.meta.outputs.tag }}" >/dev/null 2>&1; then
gh release edit "${{ steps.meta.outputs.tag }}" \
--title "${{ steps.meta.outputs.name }}" \
--notes "$notes"
else
if [[ "${{ steps.meta.outputs.prerelease }}" == "true" ]]; then
gh release create "${{ steps.meta.outputs.tag }}" \
--target "${GITHUB_SHA}" \
--title "${{ steps.meta.outputs.name }}" \
--notes "$notes" \
--prerelease
else
gh release create "${{ steps.meta.outputs.tag }}" \
--target "${GITHUB_SHA}" \
--title "${{ steps.meta.outputs.name }}" \
--notes "$notes"
fi
fi

- name: Upload binaries to rolling release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload "${{ steps.meta.outputs.tag }}" \
release-assets/kvaser-bridge-macos/kvaser-bridge-macos.app.zip \
release-assets/kvaser-bridge-windows/kvaser-bridge-windows.exe \
--clobber

attach-release-assets:
if: (github.event_name == 'release' && !startsWith(github.event.release.tag_name, 'kvaser-bridge-')) || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
runs-on: ubuntu-latest
needs: build-binaries
permissions:
contents: write

steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: release-assets

- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.release_tag }}
files: |
release-assets/kvaser-bridge-macos/kvaser-bridge-macos.app.zip
release-assets/kvaser-bridge-windows/kvaser-bridge-windows.exe
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ MANIFEST
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
!kvaser-bridge/build.spec

# Installer logs
pip-log.txt
Expand Down
54 changes: 54 additions & 0 deletions kvaser-bridge/build.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# PyInstaller spec for kvaser-bridge
# Build:
# Linux: pyinstaller build.spec
# Windows: pyinstaller build.spec
#
# Prerequisites (must be installed separately by the end user):
# - Kvaser CANlib SDK (https://kvaser.com/download/)
#
# Keep this file in git; CI uses it to produce release artifacts.

import platform

is_windows = platform.system() == 'Windows'
is_macos = platform.system() == 'Darwin'

a = Analysis(
['src/main.py'],
pathex=['src'],
datas=[
('src/bridge.crt', '.'),
('src/bridge.key', '.'),
],
hiddenimports=[
'can.interfaces.kvaser',
'can.interfaces.maccan',
],
hookspath=[],
runtime_hooks=[],
excludes=[],
)

pyz = PYZ(a.pure, a.zipped_data)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='kvaser-bridge',
debug=False,
strip=False,
upx=True,
console=not (is_windows or is_macos),
onefile=True,
)

if is_macos:
app = BUNDLE(
exe,
name='kvaser-bridge.app',
icon=None,
bundle_identifier='org.westernformularacing.kvaserbridge',
)
5 changes: 5 additions & 0 deletions kvaser-bridge/src/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
import websockets
from websockets.server import serve


def open_maccan_bus(channel, bitrate: int) -> can.BusABC:
"""Open a CAN bus via the maccan interface (macOS, Peak USB adapter)."""
return can.interface.Bus(interface='maccan', channel=channel, bitrate=bitrate)

import config

# TLS cert/key: bundled with PyInstaller (sys._MEIPASS) or alongside this file
Expand Down
11 changes: 5 additions & 6 deletions universal-telemetry-software/checklist/SETUP_CARD.tex
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ \section*{9 -- TIMESCALE LOGGING (MacBook Only)}
TimescaleDB runs on MacBook's \cmd{macbook-base} stack only. \\
RPi base (\cmd{rpi-base}) does NOT write to DB -- it relays to MacBook via UDP/TCP. \\[1mm]
Connect with \cmd{psql}: \cmd{psql postgresql://wfr:wfr\_password@localhost:5432/wfr} \\
Check tables: \cmd{\textbackslash dt} \quad Check data: \cmd{SELECT COUNT(*) FROM wfr26base;} \\
Check tables: \cmd{\textbackslash dt} \quad Check data: \cmd{SELECT COUNT(*) FROM wfr26test\_base;} \\
Grafana: \cmd{http://localhost:8087} \quad user: \cmd{admin} \quad pass: \cmd{admin} (or as set in \cmd{.env}) \\
\end{tabular}

Expand All @@ -555,7 +555,7 @@ \section*{9 -- TIMESCALE LOGGING (MacBook Only)}
\noindent
\begin{tabular}{p{126mm}}
\cmd{docker logs daq-telemetry $\vert$ grep "table="} \\[1mm]
\textbf{PASS:} \cmd{table=wfr26base} (or as set by \cmd{TIMESCALE\_TABLE}) \\
\textbf{PASS:} \cmd{table=WFR26test\_base} (or as set by \cmd{TIMESCALE\_TABLE}) \\
\textbf{FAIL:} \cmd{ENABLE\_TIMESCALE\_LOGGING=true} not set, or TimescaleDB not reachable \\
\textit{Schema:} wide format -- one row per CAN message, each decoded signal as a column. \\
\textit{Table is created automatically on first write.} \\
Expand Down Expand Up @@ -633,9 +633,9 @@ \section*{10 -- TIMESYNCALEDB BRIDGE (Base Only)}
\begin{tabular}{p{126mm}}
When \cmd{ENABLE\_TIMESCALE\_LOGGING=true}, the \cmd{TimescaleBridge} process starts automatically. \\
It reads decoded CAN frames from Redis and writes them directly to the server stack's \\
TimescaleDB over the network (writes to \cmd{POSTGRES\_DSN}). No local TimescaleDB. \\[1mm]
TimescaleDB over the network (writes to \cmd{POSTGRES\_DSN}). No local InfluxDB. \\[1mm]
Wide format: one row per CAN message, all signals as columns. \\
Table: \cmd{wfr26base} (MacBook) or as set by \cmd{TIMESCALE\_TABLE}. \\
Table: \cmd{WFR26test\_base} (MacBook) or as set by \cmd{TIMESCALE\_TABLE}. \\
\end{tabular}

\vspace{1mm}
Expand Down Expand Up @@ -710,7 +710,6 @@ \section*{14 -- KEY PORTS}
5006 & TCP & Packet retransmission \\
5432 & TCP & TimescaleDB (MacBook only) \\
6379 & TCP & Redis \\
8092 & HTTP & Cloud Sync dashboard (MacBook only) \\
8080 & HTTP & Status monitoring page \\
8087 & HTTP & Grafana dashboards (MacBook) \\
9080 & WebSocket & PECAN telemetry feed (plain WS) \\
Expand Down Expand Up @@ -742,7 +741,7 @@ \section*{15 -- ENV VAR QUICKREF}
ENABLE\_VIDEO & false & Video streaming \\
ENABLE\_AUDIO & false & Audio streaming \\
ENABLE\_TIMESCALE\_LOGGING & false & Log telemetry to TimescaleDB (MacBook only) \\
TIMESCALE\_TABLE & wfr26base & Table name for telemetry writes \\
TIMESCALE\_TABLE & WFR26test\_base & Table name for telemetry writes \\
POSTGRES\_DSN & (local) & TimescaleDB connection string (MacBook: local; RPi: points to MacBook) \\
ENABLE\_WS\_RELAY & false & Start WS relay (ws\_relay.py) on port 9089 \\
RELAY\_UPSTREAM\_WS & ws://127.0.0.1:9080 & Upstream WS URL for relay \\
Expand Down
12 changes: 2 additions & 10 deletions universal-telemetry-software/deploy/.env.macbook
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@
# Usage: docker compose -f deploy/docker-compose.macbook-base.yml --env-file deploy/.env.macbook up -d

REMOTE_IP=10.71.1.10
TIMESCALE_TABLE=wfr26base
DBC_HOST_PATH=../../secret-dbc/WFR25.dbc
DBC_DISPLAY_NAME=WFR25.dbc
TIMESCALE_TABLE=WFR26test
DBC_HOST_PATH=./example.dbc
GRAFANA_ADMIN_PASSWORD=admin

# Cloud Sync dashboard — http://localhost:8092
# Fill in your VPS DSN to enable syncing to the cloud TimescaleDB.
# Example: postgresql://wfr:password@your-vps-ip:5432/wfr
CLOUD_POSTGRES_DSN=postgresql://wfr:wfr_password@100.72.11.60:5432/wfr
CLOUD_TABLE=wfr26
LOCAL_TABLE=wfr26base
17 changes: 1 addition & 16 deletions universal-telemetry-software/deploy/MACBOOK_DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,7 @@ All configuration is done through `deploy/.env.macbook`. Key variables:
| telemetry | Base station receiver — UDP/TCP from car, WebSocket to Pecan |
| redis | Message broker for CAN frames |
| timescaledb | Local TimescaleDB — writes `WFR26test_base` table |
| pecan | Live telemetry dashboard (built from local source) |
| mediamtx | Video relay — accepts RTSP push from car Pi, serves WebRTC to Pecan |

## Video Stack

MediaMTX receives H.264 from the car Pi via RTSP and serves it to Pecan via WebRTC (WHEP).

| Port | Purpose |
|------|---------|
| `8554` TCP/UDP | RTSP — car Pi pushes H.264 here |
| `8889` TCP | WebRTC WHEP — Pecan pulls video here |
| `9997` | MediaMTX API |

Config: `universal-telemetry-software/mediamtx.yml`

To watch the stream directly (without Pecan): `http://localhost:8889/car-camera/`
| pecan | Live telemetry dashboard |

## Common Tasks

Expand Down
9 changes: 4 additions & 5 deletions universal-telemetry-software/deploy/WHICH_ONE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@ docker compose -f deploy/docker-compose.yml up -d

## docker-compose.macbook-base.yml — MacBook full local stack

Full local development stack on MacBook: telemetry + redis + timescaledb + pecan + mediamtx.
Full local development stack on MacBook: telemetry + redis + timescaledb + pecan + grafana.
TimescaleDB persists to `WFR26test` by default. Use this for development and testing
with full telemetry recording and live video.
with full telemetry recording and local dashboards.

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

**Access points:**
- Pecan dashboard: http://localhost:3000
- Video feed (direct): http://localhost:8889/car-camera/
- Grafana: http://localhost:8087
- TimescaleDB: `postgresql://wfr:wfr_password@localhost:5432/wfr`
- MediaMTX API: http://localhost:9997/v3/paths/list

---

Expand Down
Loading
Loading