Skip to content

Commit 5d45f24

Browse files
authored
Merge pull request #1 from Aharoni-Lab/feat/full-build-out
Add full HIL bench controller (all 5 milestones)
2 parents 2b9e5d6 + cd81b1c commit 5d45f24

48 files changed

Lines changed: 2796 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
lint-and-test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.11", "3.12"]
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: actions/setup-python@v5
19+
with:
20+
python-version: ${{ matrix.python-version }}
21+
22+
- name: Install dependencies
23+
run: |
24+
pip install -e ".[dev]"
25+
26+
- name: Ruff check
27+
run: ruff check src/ tests/
28+
29+
- name: Ruff format check
30+
run: ruff format --check src/ tests/
31+
32+
- name: Mypy
33+
run: mypy src/hilbench/ --ignore-missing-imports
34+
35+
- name: Pytest
36+
run: pytest --cov=hilbench --cov-report=term-missing -m "not hardware"

.gitignore

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.egg-info/
6+
*.egg
7+
dist/
8+
build/
9+
*.whl
10+
11+
# Virtual environments
12+
.venv/
13+
venv/
14+
env/
15+
16+
# IDE
17+
.vscode/
18+
.idea/
19+
*.swp
20+
*.swo
21+
*~
22+
23+
# Testing
24+
.pytest_cache/
25+
.coverage
26+
htmlcov/
27+
.mypy_cache/
28+
29+
# OS
30+
.DS_Store
31+
Thumbs.db
32+
33+
# Project-specific
34+
/configs/config.yaml
35+
*.bin
36+
*.hex
37+
*.elf

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# HIL Bench Controller
2+
3+
Automated Hardware-in-the-Loop (HIL) firmware testing on Raspberry Pi 5 for the Aharoni Lab.
4+
5+
Turns a Raspberry Pi 5 into a self-hosted GitHub Actions runner that can flash MCU firmware, observe serial output, toggle GPIO pins, and report results — enabling automated firmware testing in CI pipelines.
6+
7+
## Features
8+
9+
- **Flash firmware** via Atmel-ICE (edbg) or OpenOCD
10+
- **Serial communication** — listen, send, and pattern-match (`expect`)
11+
- **GPIO control** — set, get, pulse pins (libgpiod/gpiod, Pi 5 compatible)
12+
- **Health monitoring** — probe, serial, GPIO, and runner status checks
13+
- **CI integration** — org-level GitHub Actions runner with example workflows
14+
- **Idempotent bootstrap** — single script to provision a fresh Pi
15+
16+
## Quick Start
17+
18+
### 1. Bootstrap a Pi
19+
20+
```bash
21+
git clone https://github.com/Aharoni-Lab/hil-bench-controller.git
22+
cd hil-bench-controller
23+
sudo ./bootstrap/bootstrap_pi.sh aharoni-samd51-bench-01 <github-org-token>
24+
```
25+
26+
### 2. Edit config
27+
28+
```bash
29+
sudo vim /etc/hil-bench/config.yaml
30+
benchctl config validate
31+
```
32+
33+
### 3. Flash and test
34+
35+
```bash
36+
benchctl flash --firmware build/firmware.bin --target samd51
37+
benchctl serial expect --target samd51 --pattern "BOOT OK" --timeout 10
38+
benchctl gpio get --pin fault --target samd51
39+
benchctl health
40+
```
41+
42+
## CLI Reference
43+
44+
```
45+
benchctl [--config PATH] [--verbose] [--dry-run]
46+
flash --firmware PATH --target NAME [--verify] [--power-cycle]
47+
serial listen|send|expect --target NAME [--timeout N]
48+
gpio set|get|pulse --pin NAME|NUM --value high|low
49+
health [--json]
50+
config show|validate|generate
51+
```
52+
53+
## CI Integration
54+
55+
Add HIL testing to any firmware repo. See [`examples/firmware-ci.yml`](examples/firmware-ci.yml) for a complete 2-job workflow (build on GitHub → test on bench).
56+
57+
```yaml
58+
hil-test:
59+
runs-on: [self-hosted, linux, ARM64, hil, samd51, bench01]
60+
steps:
61+
- uses: actions/download-artifact@v4
62+
with: { name: firmware }
63+
- run: benchctl flash --firmware firmware.bin --target samd51 --verify
64+
- run: benchctl serial expect --target samd51 --pattern "BOOT OK" --timeout 15
65+
```
66+
67+
## Development
68+
69+
```bash
70+
pip install -e ".[dev]"
71+
ruff check src/ tests/
72+
pytest -m "not hardware"
73+
```
74+
75+
## Project Structure
76+
77+
```
78+
src/hilbench/ Python package
79+
config.py Pydantic config models + YAML loader
80+
probe.py edbg/OpenOCD flash abstraction
81+
serial_io.py pyserial wrapper
82+
gpio.py gpiod (libgpiod) wrapper
83+
relay.py Power relay control (stubbed)
84+
health.py Health check logic
85+
artifacts.py Firmware artifact resolution
86+
cli/ Click CLI commands
87+
bootstrap/ Pi provisioning scripts
88+
configs/ Config templates
89+
systemd/ Health check timer
90+
udev/ Device permission rules
91+
```
92+
93+
## License
94+
95+
GPL-3.0-or-later. See [LICENSE](LICENSE).

bootstrap/bootstrap_pi.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
# Master bootstrap script for HIL bench Raspberry Pi 5.
3+
# Idempotent — safe to re-run.
4+
#
5+
# Usage: sudo ./bootstrap_pi.sh <bench-name> [github-org-token]
6+
# Example: sudo ./bootstrap_pi.sh aharoni-samd51-bench-01 ghp_xxxx
7+
8+
set -euo pipefail
9+
10+
BENCH_NAME="${1:?Usage: $0 <bench-name> [github-org-token]}"
11+
GITHUB_TOKEN="${2:-}"
12+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13+
REPO_DIR="$(dirname "$SCRIPT_DIR")"
14+
15+
echo "=== HIL Bench Bootstrap: ${BENCH_NAME} ==="
16+
echo "Repo: ${REPO_DIR}"
17+
echo ""
18+
19+
# Run each subscript in order
20+
"${SCRIPT_DIR}/configure_hostname.sh" "$BENCH_NAME"
21+
"${SCRIPT_DIR}/install_system_packages.sh"
22+
"${SCRIPT_DIR}/install_udev_rules.sh" "$REPO_DIR"
23+
"${SCRIPT_DIR}/install_python_env.sh" "$REPO_DIR"
24+
"${SCRIPT_DIR}/generate_local_config.sh" "$BENCH_NAME" "$REPO_DIR"
25+
"${SCRIPT_DIR}/install_health_timer.sh" "$REPO_DIR"
26+
27+
if [[ -n "$GITHUB_TOKEN" ]]; then
28+
"${SCRIPT_DIR}/install_runner.sh" "$BENCH_NAME" "$GITHUB_TOKEN"
29+
else
30+
echo "--- Skipping runner install (no token provided) ---"
31+
echo "Run manually: sudo ${SCRIPT_DIR}/install_runner.sh $BENCH_NAME <token>"
32+
fi
33+
34+
echo ""
35+
echo "=== Bootstrap complete for ${BENCH_NAME} ==="
36+
echo "Config: /etc/hil-bench/config.yaml"
37+
echo "Venv: /opt/hil-bench/venv"
38+
echo "Test: /opt/hil-bench/venv/bin/benchctl --help"

bootstrap/configure_hostname.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
# Set the Pi hostname to match the bench name.
3+
set -euo pipefail
4+
5+
BENCH_NAME="${1:?Usage: $0 <bench-name>}"
6+
7+
echo "--- Configuring hostname ---"
8+
9+
CURRENT=$(hostname)
10+
if [[ "$CURRENT" == "$BENCH_NAME" ]]; then
11+
echo "Hostname already set to $BENCH_NAME"
12+
else
13+
hostnamectl set-hostname "$BENCH_NAME"
14+
echo "Hostname: $CURRENT$BENCH_NAME"
15+
fi
16+
17+
echo "--- Hostname done ---"

bootstrap/generate_local_config.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
# Generate /etc/hil-bench/config.yaml from template.
3+
set -euo pipefail
4+
5+
BENCH_NAME="${1:?Usage: $0 <bench-name> <repo-dir>}"
6+
REPO_DIR="${2:?Usage: $0 <bench-name> <repo-dir>}"
7+
8+
CONFIG_DIR="/etc/hil-bench"
9+
CONFIG_FILE="${CONFIG_DIR}/config.yaml"
10+
TEMPLATE="${REPO_DIR}/configs/config.template.yaml"
11+
12+
echo "--- Generating local config ---"
13+
14+
mkdir -p "$CONFIG_DIR"
15+
16+
if [[ -f "$CONFIG_FILE" ]]; then
17+
echo "Config already exists: $CONFIG_FILE (not overwriting)"
18+
echo "To regenerate, delete it first: rm $CONFIG_FILE"
19+
else
20+
sed \
21+
-e "s/my-bench-01/${BENCH_NAME}/g" \
22+
"$TEMPLATE" > "$CONFIG_FILE"
23+
echo "Generated: $CONFIG_FILE"
24+
echo "Edit to match your hardware, then validate:"
25+
echo " benchctl config validate --config $CONFIG_FILE"
26+
fi
27+
28+
echo "--- Config generation done ---"

bootstrap/install_health_timer.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
# Install and enable the HIL bench health check systemd timer.
3+
set -euo pipefail
4+
5+
REPO_DIR="${1:?Usage: $0 <repo-dir>}"
6+
SYSTEMD_SRC="${REPO_DIR}/systemd"
7+
SYSTEMD_DST="/etc/systemd/system"
8+
9+
echo "--- Installing health check timer ---"
10+
11+
for unit in hil-bench-health.service hil-bench-health.timer; do
12+
src="${SYSTEMD_SRC}/${unit}"
13+
if [[ ! -f "$src" ]]; then
14+
echo "ERROR: Unit file not found: $src"
15+
exit 1
16+
fi
17+
cp "$src" "${SYSTEMD_DST}/${unit}"
18+
echo "Installed: ${SYSTEMD_DST}/${unit}"
19+
done
20+
21+
systemctl daemon-reload
22+
systemctl enable --now hil-bench-health.timer
23+
24+
echo "Health timer enabled (runs 1 min after boot, then every 5 min)"
25+
echo "--- Health timer install done ---"

bootstrap/install_python_env.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
# Create Python venv and install hil-bench-controller.
3+
set -euo pipefail
4+
5+
REPO_DIR="${1:?Usage: $0 <repo-dir>}"
6+
VENV_DIR="/opt/hil-bench/venv"
7+
8+
echo "--- Setting up Python environment ---"
9+
10+
mkdir -p /opt/hil-bench
11+
12+
if [[ ! -d "$VENV_DIR" ]]; then
13+
python3 -m venv "$VENV_DIR"
14+
echo "Created venv at $VENV_DIR"
15+
else
16+
echo "Venv already exists at $VENV_DIR"
17+
fi
18+
19+
"$VENV_DIR/bin/pip" install --upgrade pip setuptools wheel -q
20+
"$VENV_DIR/bin/pip" install -e "$REPO_DIR" -q
21+
22+
echo "benchctl installed: $($VENV_DIR/bin/benchctl --version)"
23+
echo "--- Python environment done ---"

bootstrap/install_runner.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
# Download and configure GitHub Actions runner at org scope (Aharoni-Lab).
3+
set -euo pipefail
4+
5+
BENCH_NAME="${1:?Usage: $0 <bench-name> <github-token>}"
6+
GITHUB_TOKEN="${2:?Usage: $0 <bench-name> <github-token>}"
7+
8+
RUNNER_DIR="/opt/hil-bench/actions-runner"
9+
RUNNER_VERSION="2.321.0"
10+
ORG="Aharoni-Lab"
11+
12+
echo "--- Installing GitHub Actions runner (org: ${ORG}) ---"
13+
14+
mkdir -p "$RUNNER_DIR"
15+
16+
# Download runner if not present
17+
if [[ ! -f "$RUNNER_DIR/run.sh" ]]; then
18+
ARCH=$(dpkg --print-architecture)
19+
case "$ARCH" in
20+
arm64|aarch64) RUNNER_ARCH="arm64" ;;
21+
amd64|x86_64) RUNNER_ARCH="x64" ;;
22+
*) echo "Unsupported arch: $ARCH"; exit 1 ;;
23+
esac
24+
25+
TARBALL="actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz"
26+
URL="https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/${TARBALL}"
27+
28+
echo "Downloading runner ${RUNNER_VERSION} (${RUNNER_ARCH})..."
29+
curl -sL "$URL" | tar xz -C "$RUNNER_DIR"
30+
fi
31+
32+
# Configure runner at org level (idempotent — skips if .runner exists)
33+
if [[ ! -f "$RUNNER_DIR/.runner" ]]; then
34+
echo "Configuring runner for org ${ORG}..."
35+
"$RUNNER_DIR/config.sh" \
36+
--url "https://github.com/${ORG}" \
37+
--token "$GITHUB_TOKEN" \
38+
--name "$BENCH_NAME" \
39+
--labels "self-hosted,linux,ARM64,hil,$(echo "$BENCH_NAME" | tr '-' ',')" \
40+
--work /opt/hil-bench/_work \
41+
--unattended \
42+
--replace
43+
else
44+
echo "Runner already configured"
45+
fi
46+
47+
# Install and start systemd service
48+
if ! systemctl is-enabled "actions.runner.${ORG}.${BENCH_NAME}.service" &>/dev/null; then
49+
cd "$RUNNER_DIR"
50+
./svc.sh install
51+
./svc.sh start
52+
echo "Runner service installed and started"
53+
else
54+
echo "Runner service already running"
55+
fi
56+
57+
echo "--- Runner install done ---"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
# Install system-level dependencies for HIL bench.
3+
set -euo pipefail
4+
5+
echo "--- Installing system packages ---"
6+
7+
export DEBIAN_FRONTEND=noninteractive
8+
apt-get update -qq
9+
apt-get install -y -qq \
10+
libgpiod2 \
11+
libgpiod-dev \
12+
python3-venv \
13+
python3-dev \
14+
openocd \
15+
git \
16+
build-essential \
17+
usbutils
18+
19+
# Build edbg from source if not already installed
20+
if ! command -v edbg &>/dev/null; then
21+
echo "Building edbg from source..."
22+
EDBG_DIR=$(mktemp -d)
23+
git clone --depth 1 https://github.com/ataradov/edbg.git "$EDBG_DIR"
24+
make -C "$EDBG_DIR" all
25+
install -m 755 "$EDBG_DIR/edbg" /usr/local/bin/edbg
26+
rm -rf "$EDBG_DIR"
27+
echo "edbg installed: $(edbg --version 2>&1 || true)"
28+
else
29+
echo "edbg already installed: $(edbg --version 2>&1 || true)"
30+
fi
31+
32+
echo "--- System packages done ---"

0 commit comments

Comments
 (0)