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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Host regression tests (ASan + UBSan)
run: make sanitize

compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: arduino/setup-arduino-cli@v2
- name: Install pinned ESP32 core and NimBLE-Arduino
run: make deps
- name: Compile firmware
run: make compile
31 changes: 0 additions & 31 deletions .github/workflows/compile.yml

This file was deleted.

20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Instructions for AI agents working on TrainerBridge

TrainerBridge is ESP32 Arduino firmware (C++17) that proxies one FTMS smart trainer to
several BLE clients. Read [docs/how-it-works.md](docs/how-it-works.md) first.

- Run `make sanitize` before finishing. `make compile` needs `arduino-cli` and `make deps`;
if it isn't installed, CI runs it on the pull request.
- Pinned versions: ESP32 Arduino core 3.3.12, NimBLE-Arduino 2.5.1 (Makefile, CI, README).
Bump them together.
- Never write to the trainer from a NimBLE callback: queue the command and let
`processQueuedCommands()` in `loop()` send it.
- Control Point transaction state (`cpTxState`, `pendingCp`, `pendingCpSince`) is shared
with the NimBLE host task. Touch it only inside `cpTxMux`, and make BLE/Serial calls after
leaving the critical section.
- Control Point responses go only to the originating connection handle. Never indicate to
all clients: Garmin drops sensors that send unexpected indications.
- Add a named test to `tests/firmware_test.cpp` for any behaviour you change. Extend the
stubs in `tests/stubs/` only as far as the firmware uses the real API.
- Nothing here replaces a ride on real hardware: say so in the PR and point to the
checklist in [docs/testing.md](docs/testing.md).
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Changelog

All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- Host regression tests: the real firmware against Arduino/NimBLE test doubles, with
sanitizers in CI (`make test`, `make sanitize`).
- `Makefile` with `check`, `deps`, `compile`, `upload` and `monitor`.
- `docs/how-it-works.md`, `docs/testing.md` with a hardware checklist, `AGENTS.md`,
`CONTRIBUTING.md`.

### Changed
- ESP32 Arduino core 3.3.12 and NimBLE-Arduino 2.5.1.
- Start/Resume is forwarded to the trainer as well as answered locally.
- The command queue merges repeated updates only from the same client, so every app gets a
response.

### Fixed
- Request Control is actually sent to the trainer on connection (it was skipped).
- Trainer responses must match the pending command's opcode.
- A trainer response arriving before the write returned was dropped, and the command then
timed out.
- On trainer disconnect, pending and queued commands fail immediately instead of leaving
apps waiting.
- Failure responses with no sender (serial console commands, or an app that left) were
indicated to every client, including Garmin.
- Control Point transaction state is locked between the Bluetooth task and the main loop.
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Contributing

Issues and PRs welcome, especially reports from trainers other than the Van Rysel D100.

## Development

```bash
make test # host tests, needs only g++
make check # host tests with sanitizers + firmware build (needs arduino-cli, `make deps` once)
make upload # build and flash, PORT=/dev/ttyUSB0 by default
make monitor # serial console
```

The host tests run the real firmware code against Arduino and NimBLE test doubles, so most
logic can be tested without a board. See [docs/testing.md](docs/testing.md) for what they
cover and the hardware checklist.

## Guidelines

- Keep the firmware warning-free: CI builds with `--warnings all`.
- Bluetooth callbacks must not block. Writes to the trainer go through the command queue.
- Add or update a named test in `tests/firmware_test.cpp` for behaviour changes.
- Change user-facing settings in `Config.h` and the table in the README together.
- Mention in the PR whether you tested on hardware, with which trainer, app and Garmin.

## Reporting a trainer that doesn't work

Set `DEBUG_LOG 1` in `Config.h`, flash, and attach the serial log from boot until the
problem, plus the trainer model and the app or Garmin you used.
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FQBN ?= esp32:esp32:esp32
PORT ?= /dev/ttyUSB0
ESP32_CORE := 3.3.12
NIMBLE := 2.5.1
ESP32_INDEX := https://espressif.github.io/arduino-esp32/package_esp32_index.json

.PHONY: check test sanitize deps compile upload monitor

check: sanitize compile ## Host tests with sanitizers, then the real firmware build

test: ## Host regression tests (C++17 compiler only)
bash tests/run.sh

sanitize: ## Host tests with AddressSanitizer + UBSan
SANITIZE=1 bash tests/run.sh

deps: ## Install the pinned ESP32 core and NimBLE-Arduino
arduino-cli core update-index --additional-urls $(ESP32_INDEX)
arduino-cli core install esp32:esp32@$(ESP32_CORE) --additional-urls $(ESP32_INDEX)
arduino-cli lib install NimBLE-Arduino@$(NIMBLE)

compile: ## Build the firmware with all warnings
arduino-cli compile --warnings all --fqbn $(FQBN) TrainerBridge

upload: compile ## Build and flash (PORT=/dev/ttyUSB0)
arduino-cli upload --port $(PORT) --fqbn $(FQBN) TrainerBridge

monitor: ## Serial console at 115200 baud
arduino-cli monitor --port $(PORT) --config baudrate=115200
Loading
Loading