Skip to content
Closed
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
15 changes: 10 additions & 5 deletions .claude/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@ pio run # if platformio.ini exists, all default envs
## 3. simavr

```bash
make sim-test # run the firmware on an emulated ATmega328P, compare to baseline
make sim-test-update # regenerate test/uno-simavr.txt after intentional trace changes
make sim-test # run the firmware on an emulated ATmega328P, compare serial trace to baseline
make sim-test-update # regenerate test/uno-simavr.txt after intentional trace changes
make sim-display-test # render the emulated SSD1306 framebuffer, compare pixels to baseline
make sim-display-test-update # regenerate test/uno-display.txt after intentional display changes
```

This catches boot crashes and memory exhaustion that compilation cannot (it already
caught a display-buffer OOM and an out-of-bounds ADC mode). Requires simavr and
PlatformIO (both in the devcontainer).
`sim-test` catches boot crashes and memory exhaustion that compilation cannot (it
already caught a display-buffer OOM and an out-of-bounds ADC mode). `sim-display-test`
attaches a virtual SSD1306 over TWI and compares the rendered VRAM (ASCII art)
pixel-for-pixel against `test/uno-display.txt`, catching rendering regressions the serial
trace misses. Both require PlatformIO and simavr; `sim-display-test` also needs
`libsimavr-dev`, `libelf-dev`, and `gcc` (all in the devcontainer).

## 4. Hardware caveat

Expand Down
3 changes: 3 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
make \
curl \
gcc \
python3-pip \
python3-venv \
simavr \
libsimavr-dev \
libelf-dev \
udev \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/platformio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ jobs:
- name: Install simavr
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends simavr
- name: Run firmware under simavr
sudo apt-get install -y --no-install-recommends \
simavr libsimavr-dev libelf-dev gcc
- name: Run firmware under simavr (serial trace)
run: make sim-test
- name: Run firmware under simavr (OLED framebuffer)
run: make sim-display-test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ build/

# PlatformIO
.pio/

# simavr display test host binary (built by `make sim-display-test`)
test/sim_display/sim_display
24 changes: 22 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ tiny_scope/
├── .devcontainer/ # Dev container: arduino-cli, PlatformIO, simavr
├── .claude/skills/ # Project skills for agent work (building, verify)
├── .github/workflows/ # CI: arduino.yml + platformio.yml build matrices + simavr
├── test/ # simavr smoke test: runner script + golden baseline
├── test/ # simavr tests:
│ # simavr-run.sh + uno-simavr.txt (serial smoke test)
│ # sim-display-run.sh + uno-display.txt (OLED render test)
│ # sim_display/ (render runner + vendored GPL SSD1306 part)
├── Makefile # arduino-cli build automation
├── arduino-cli.yaml # arduino-cli config (board indexes, .arduino dirs)
├── platformio.ini # PlatformIO envs: uno, teensy31, teensylc,
Expand Down Expand Up @@ -98,10 +101,27 @@ buffers) and runs it under simavr via `test/simavr-run.sh`. The comparison strip
so value drift doesn't fail the test but OK/FAIL/YES/NO verdict flips do. The trace code
in `tiny_scope.ino` is compiled out of production builds.

### simavr display test

```bash
make sim-display-test # render the emulated OLED framebuffer, compare to golden
make sim-display-test-update # regenerate test/uno-display.txt after intentional changes
```

Runs the same `uno_sim` firmware under simavr with a **virtual SSD1306 OLED attached
over TWI**, waits for the `sim: TESTS COMPLETE` marker, then dumps the display VRAM as
ASCII art (64x128, `#`/`.`) and compares it **pixel-for-pixel** (no digit stripping)
against `test/uno-display.txt`. This catches rendering regressions the serial smoke test
cannot. The runner is `test/sim_display/sim_display.c`; it links `ssd1306_virt.c/.h`,
**vendored GPLv3 from simavr** and used as test tooling only (never in the firmware) —
see `test/sim_display/README.md`. Needs `libsimavr-dev`, `libelf-dev`, and `gcc` (all in
the devcontainer); the Makefile builds the host binary.

### Verification expectations

Before declaring a change done, build for **at least** `arduino:avr:uno` and one ARM
target, and run `make sim-test` — see `.claude/skills/verify`. `--warnings all` is on; do not introduce new
target, and run `make sim-test` and `make sim-display-test` — see `.claude/skills/verify`.
`--warnings all` is on; do not introduce new
warnings. Display/ADC behavior can only be fully confirmed on hardware; call out
runtime-affecting changes (display init, ADC timing, pin changes) in the PR description
so the maintainer can flash-test.
Expand Down
28 changes: 27 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,30 @@ sim-test-update: # Run under simavr and regenerate the golden baseline
pio run -e uno_sim
test/simavr-run.sh --update

.PHONY: clean %.hex all core setup setup-pio sim-test sim-test-update
# simavr display test: renders the emulated SSD1306 framebuffer to ASCII art
# and compares it pixel-for-pixel to the golden baseline. Needs libsimavr-dev
# and libelf-dev (in the devcontainer). ssd1306_virt.c is GPLv3 test tooling
# vendored from simavr - see test/sim_display/README.md.
SIM_DISPLAY_INC ?= /usr/include/simavr
SIM_DISPLAY_DIR = test/sim_display

# Our runner is held to -Wall -Wextra; the vendored simavr part needs
# -Wno-unused-parameter (its IRQ hooks ignore the irq/param args), so compile
# the two as separate objects.
$(SIM_DISPLAY_DIR)/sim_display: $(SIM_DISPLAY_DIR)/sim_display.c $(SIM_DISPLAY_DIR)/ssd1306_virt.c $(SIM_DISPLAY_DIR)/ssd1306_virt.h
gcc -O2 -Wall -Wextra -I$(SIM_DISPLAY_INC) -c -o $(SIM_DISPLAY_DIR)/sim_display.o $(SIM_DISPLAY_DIR)/sim_display.c
gcc -O2 -Wall -Wextra -Wno-unused-parameter -I$(SIM_DISPLAY_INC) -c -o $(SIM_DISPLAY_DIR)/ssd1306_virt.o $(SIM_DISPLAY_DIR)/ssd1306_virt.c
gcc -o $@ $(SIM_DISPLAY_DIR)/sim_display.o $(SIM_DISPLAY_DIR)/ssd1306_virt.o -lsimavr -lelf

sim-display-test: # Render the emulated OLED framebuffer and compare to the golden baseline
sim-display-test: test/sim_display/sim_display
pio run -e uno_sim
test/sim-display-run.sh

sim-display-test-update: # Render the emulated OLED framebuffer and regenerate the golden baseline
sim-display-test-update: test/sim_display/sim_display
pio run -e uno_sim
test/sim-display-run.sh --update

.PHONY: clean %.hex all core setup setup-pio sim-test sim-test-update \
sim-display-test sim-display-test-update
85 changes: 85 additions & 0 deletions test/sim-display-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
#
# sim-display-run.sh - Run the tiny_scope firmware on a simulated ATmega328P
# with a virtual SSD1306 OLED attached over TWI, render its framebuffer to
# ASCII art, and compare it pixel-for-pixel against a golden baseline.
#
# Usage:
# test/sim-display-run.sh [firmware.elf] Run and compare against the golden file.
# test/sim-display-run.sh --update Run and (re)write the golden file instead
# of comparing (also: UPDATE=1 env var).
#
# The default firmware path is .pio/build/uno_sim/firmware.elf (built with
# `pio run -e uno_sim`). The runner binary (test/sim_display/sim_display) is
# built by the Makefile - run `make sim-display-test` rather than this script
# directly, or build the runner first.
#
# Unlike the serial smoke test (simavr-run.sh) this is a pixel test: the ASCII
# art is compared verbatim, with no digit stripping.
#
# Environment variables:
# RUNNER sim_display binary (default: test/sim_display/sim_display)
# TIMEOUT max simulated seconds (default: 30)

set -u

FIRMWARE="${1:-.pio/build/uno_sim/firmware.elf}"
TIMEOUT="${TIMEOUT:-30}"

UPDATE="${UPDATE:-0}"
if [ "${FIRMWARE}" = "--update" ]; then
UPDATE=1
FIRMWARE=".pio/build/uno_sim/firmware.elf"
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUNNER="${RUNNER:-${SCRIPT_DIR}/sim_display/sim_display}"
GOLDEN="${SCRIPT_DIR}/uno-display.txt"

if [ ! -x "${RUNNER}" ]; then
echo "ERROR: runner not built: ${RUNNER}" >&2
echo "Build it first, e.g. with: make sim-display-test" >&2
exit 1
fi

if [ ! -f "${FIRMWARE}" ]; then
echo "ERROR: firmware not found: ${FIRMWARE}" >&2
echo "Build it first, e.g. with: pio run -e uno_sim" >&2
exit 1
fi

TMP_OUT="$(mktemp)"
cleanup() {
rm -f "${TMP_OUT}"
}
trap cleanup EXIT

if ! "${RUNNER}" "${FIRMWARE}" "${TMP_OUT}" --timeout "${TIMEOUT}"; then
echo "ERROR: sim_display run failed (see output above)." >&2
exit 1
fi

if [ "${UPDATE}" -eq 1 ]; then
cp "${TMP_OUT}" "${GOLDEN}"
echo "Updated golden file: ${GOLDEN}"
cat "${GOLDEN}"
exit 0
fi

if [ ! -f "${GOLDEN}" ]; then
echo "ERROR: golden file not found: ${GOLDEN}" >&2
echo "Run with --update first to generate it." >&2
exit 1
fi

if diff -q "${GOLDEN}" "${TMP_OUT}" >/dev/null; then
echo "PASS: rendered display matches the golden baseline."
exit 0
fi

echo "FAIL: rendered display does not match the golden baseline (${GOLDEN})."
echo "----- diff (golden vs capture) -----"
diff "${GOLDEN}" "${TMP_OUT}"
echo "----- captured display -----"
cat "${TMP_OUT}"
exit 1
41 changes: 41 additions & 0 deletions test/sim_display/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# sim_display - emulated SSD1306 OLED render test

This is a headless display test for tiny_scope. It runs the firmware on an
emulated ATmega328P (via [simavr](https://github.com/buserror/simavr)) with a
**virtual SSD1306 OLED** attached over TWI (I2C), waits for the firmware's
`sim: TESTS COMPLETE` marker on the serial port, then dumps the display's video
RAM as ASCII art (`#` = lit pixel, `.` = dark, 64 rows x 128 columns). That
render is compared pixel-for-pixel against the golden baseline
`test/uno-display.txt`, so it catches display regressions that the serial-only
smoke test (`test/simavr-run.sh`) cannot.

## Files

- `sim_display.c` - the runner (loads the ELF, wires up UART capture and the
virtual OLED, steps the CPU to the marker, writes the ASCII-art dump). This is
our own code.
- `ssd1306_virt.c` / `ssd1306_virt.h` - the virtual SSD1306 part, **vendored
verbatim from simavr v1.6** (`examples/parts/ssd1306_virt.{c,h}`).

## Licensing

> `ssd1306_virt.c` / `ssd1306_virt.h` are **GPLv3** (Copyright Michel Pollet and
> Doug Szumski), unlike the rest of tiny_scope which is MIT. They are **test
> tooling only** and are compiled solely into the `sim_display` host binary -
> they are **never** linked into the firmware. Keep their GPL headers intact.

## Running

Requires `gcc`, `libsimavr-dev`, and `libelf-dev` (all present in the
devcontainer). From the repo root:

```bash
make sim-display-test # build uno_sim + runner, render and compare
make sim-display-test-update # regenerate test/uno-display.txt after an
# intentional display change
```

The Makefile builds the `sim_display` binary
(`gcc ... -I/usr/include/simavr -lsimavr -lelf`) and the `uno_sim` PlatformIO
firmware, then `test/sim-display-run.sh` runs the render and diff. Override the
simulated-seconds timeout with `TIMEOUT=<n>`.
Loading