Skip to content
Merged
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
159 changes: 159 additions & 0 deletions .github/workflows/relay.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: relay
run-name: Triggered by ${{ github.event_name }} to ${{ github.ref }} by @${{ github.actor }}

on:
push:
branches:
- "**"
tags:
- "**"

jobs:
build:
runs-on: ${{ matrix.runner }}
name: Build ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm

permissions:
contents: read
packages: write

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

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Generate platform pair
id: platform
run: |
platform=${{ matrix.platform }}
echo "pair=${platform//\//-}" >> $GITHUB_OUTPUT

- name: Build and push by digest
id: build
uses: docker/build-push-action@v5
with:
context: relay
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/gaucho-racing/tcm-987/relay,push-by-digest=true,name-canonical=true,push=true
# Per-ref cache scopes so simultaneous main+tag pushes from a
# release commit can't race on the same cache namespace and
# poison each other's layers. Tag/feature builds fall back to
# main's cache so they're not cold from scratch.
cache-from: |
type=gha,scope=build-${{ steps.platform.outputs.pair }}-${{ github.ref_name }}
type=gha,scope=build-${{ steps.platform.outputs.pair }}-main
cache-to: type=gha,scope=build-${{ steps.platform.outputs.pair }}-${{ github.ref_name }},mode=max

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ steps.platform.outputs.pair }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge:
runs-on: ubuntu-latest
name: Merge manifests
needs: build

permissions:
contents: read
packages: write

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

- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check if this commit has a release tag
id: release
run: |
tag=$(git tag --points-at HEAD | grep '^v' | head -n1)
if [ -n "$tag" ]; then
echo "Found tag: $tag"
if gh release view "$tag" --json tagName > /dev/null 2>&1; then
echo "release_tag=$tag" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "is_release=false" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Generate tag list
id: tags
shell: bash
run: |
TAGS="type=sha"

if [ "${GITHUB_REF_TYPE}" = "branch" ] && [ "${GITHUB_REF_NAME}" = "main" ]; then
TAGS="${TAGS}\ntype=raw,value=latest"
fi

if [ "${{ steps.release.outputs.is_release }}" = "true" ]; then
CLEAN_TAG=$(echo "${{ steps.release.outputs.release_tag }}" | sed 's/^v//')
TAGS="${TAGS}\ntype=raw,value=${CLEAN_TAG}"
fi

echo -e "tags<<EOF\n$TAGS\nEOF" >> $GITHUB_OUTPUT

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/gaucho-racing/tcm-987/relay
tags: ${{ steps.tags.outputs.tags }}

- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf 'ghcr.io/gaucho-racing/tcm-987/relay@sha256:%s ' *)

- name: Inspect image
run: |
docker buildx imagetools inspect ghcr.io/gaucho-racing/tcm-987/relay:${{ steps.meta.outputs.version }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env
.DS_Store
can_log.csv
50 changes: 50 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: tcm987-dev

# All services run on host networking — socketcan interfaces (can0/vcan0)
# live in the host network namespace, so the relay must share it to see
# them. This is the dev compose: the relay builds from local source under
# air for hot reload, no restart policy so crashes are visible in
# `docker compose logs`. nanomq is included so `mosquitto_sub -t 'p987/#'`
# works locally; production can drop it (leave LOCAL_MQTT_HOST empty).

services:
relay:
build:
context: .
dockerfile: relay/Dockerfile.dev
network_mode: host
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./relay:/app/relay
- relay_go_cache:/go
- relay_data:/data
environment:
ENV: "DEV"
VEHICLE_ID: ${VEHICLE_ID}
VEHICLE_UPLOAD_KEY: ${VEHICLE_UPLOAD_KEY}
DATABASE_PATH: /data/relay.db
LOCAL_MQTT_HOST: localhost
LOCAL_MQTT_PORT: "1883"
CLOUD_MQTT_HOST: ${CLOUD_MQTT_HOST}
CLOUD_MQTT_PORT: ${CLOUD_MQTT_PORT}
CLOUD_MQTT_USER: ${CLOUD_MQTT_USER}
CLOUD_MQTT_PASSWORD: ${CLOUD_MQTT_PASSWORD}
CAN_INTERFACES: ${CAN_INTERFACES}
VIRTUAL_CAN_PORTS: "8100"
LOCAL_PUBLISH_INTERVAL: "20"
CLOUD_PUBLISH_INTERVAL: "100"
PING_INTERVAL: "5000"
DB_QUEUE_SIZE: "50000"
DB_BATCH_SIZE: "5000"
RETENTION_HOURS: "72"
depends_on:
- nanomq

nanomq:
image: emqx/nanomq:latest
network_mode: host

volumes:
relay_go_cache:
relay_data:
18 changes: 18 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copy to `.env`. The dev compose inlines all other config — only the
# values below are read from .env at runtime.

# Per-vehicle identity
VEHICLE_ID="cayman"
VEHICLE_UPLOAD_KEY="0"

# socketcan interfaces to read, as iface:bus_label pairs
# ("can0:pcan,can1:kcan"). Leave empty on hosts without CAN hardware —
# the virtual UDP port on 8100 still works for injecting test frames.
CAN_INTERFACES="can0:pcan"

# Cloud MQTT broker — leave CLOUD_MQTT_HOST empty to disable cloud publish.
# To test against a Mapache stack on this host, use host.docker.internal.
CLOUD_MQTT_HOST=""
CLOUD_MQTT_PORT="1883"
CLOUD_MQTT_USER="changeme"
CLOUD_MQTT_PASSWORD="changeme"
21 changes: 21 additions & 0 deletions relay/.air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
root = "."
tmp_dir = "tmp"

[build]
bin = "./tmp/main"
cmd = "go mod tidy && go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["tmp", "vendor"]
exclude_regex = ["_test.go"]
include_ext = ["go", "toml"]
kill_delay = "0s"
send_interrupt = false
poll = true
poll_interval = 500
stop_on_error = true

[log]
time = false

[misc]
clean_on_exit = true
18 changes: 18 additions & 0 deletions relay/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.env
tmp/

# Local SQLite database (WAL mode leaves sidecar files)
*.db
*.db-wal
*.db-shm

# Go build artifacts
*.exe
*.test
*.out
coverage.html
go.work

.vscode/
.idea/
.DS_Store
30 changes: 30 additions & 0 deletions relay/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY . ./
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /tcm_relay

##
## Deploy
##
FROM alpine:3.21

RUN apk --no-cache add ca-certificates tzdata
ENV TZ=UTC

COPY --from=builder /tcm_relay /tcm_relay

# A relative DATABASE_PATH (including the built-in default) resolves
# against the workdir — put it inside the volume so buffered telemetry
# survives a container recreate.
WORKDIR /data
VOLUME /data

ENTRYPOINT ["/tcm_relay"]
7 changes: 7 additions & 0 deletions relay/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM golang:1.26-bookworm

RUN go install github.com/air-verse/air@latest

WORKDIR /app/relay

CMD ["air", "-c", ".air.toml"]
11 changes: 11 additions & 0 deletions relay/config/banner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package config

import "github.com/fatih/color"

func PrintStartupBanner() {
banner := color.New(color.Bold, color.FgHiMagenta).PrintlnFunc()
banner("TCM-987 Relay")
version := color.New(color.Bold, color.FgMagenta).PrintlnFunc()
version("Running v" + Version + " [ENV: " + Env + "]")
println()
}
Loading
Loading