11 KiB
Anvil
Forge clean, testable Arduino projects from a single command.
Anvil generates self-contained Arduino projects with hardware abstraction, test infrastructure, sensor libraries, and a complete build/upload/test workflow. Once generated, the project stands alone -- Anvil is a scaffolding tool, not a runtime dependency.
Anvil is a Nexus Workshops project, built for FTC robotics teams and embedded systems students.
Getting Started
Install
Download the release binary for your platform and add it to your PATH. Then run first-time setup:
anvil setup
This installs arduino-cli and the arduino:avr core. If something is
already installed, Anvil skips it. Run anvil doctor at any time to
check your environment.
Create a project
anvil new blink
cd blink
That's it. You have a complete project with build scripts, a HAL interface, mock infrastructure, and a starter test file. Plug in your board and run:
./build.sh # compile (verify it builds)
./upload.sh # compile + upload to board
./monitor.sh # serial monitor
./test.sh # host-side tests (no board needed)
On Windows, use build.bat, upload.bat, monitor.bat, test.bat.
Every script reads settings from .anvil.toml -- no Anvil binary required.
Templates
The default basic template gives you a blank canvas. For a richer starting
point, use a composed template:
anvil new weather_station --template weather --board uno
The weather template builds on basic, adding a WeatherApp with a TMP36
temperature sensor driver, managed example tests demonstrating both mock and
simulator patterns, and student test starters. To see available templates:
anvil new --list-templates
Templates are pure data -- each is a directory with a template.toml
declaring its base, required libraries, and per-board pin defaults. Adding
a new template requires zero Rust code changes.
Libraries
Anvil ships sensor and actuator libraries, each with four files: an abstract interface, a hardware implementation, a test mock, and a deterministic simulator.
anvil add tmp36 --pin A0 # analog temperature sensor
anvil add button --pin 2 # digital pushbutton with debounce sim
See what's available and what's installed:
anvil lib --available # all libraries in the registry
anvil lib # libraries installed in this project
Remove a library:
anvil remove tmp36
Each library installs to lib/drivers/<name>/ with its test file in test/.
The CMakeLists.txt auto-discovers driver directories, so adding a library
immediately makes it available to your tests.
The mock/sim split
Every library provides two test doubles:
- Mock -- Returns exact values you set. Use in unit tests to verify your application logic calls the sensor correctly and responds to specific values.
- Simulator -- Returns realistic values with configurable noise, bounce, or drift. Use in system tests to verify your code handles real-world sensor behavior (jitter, debounce timing, averaging).
This split teaches the difference between interaction testing and behavioral testing -- a concept that transfers directly to professional software development.
Pin Management
Anvil knows the pinout of every supported board. Assignments are validated at the command line, not when you discover a wiring bug at 9 PM.
anvil pin --assign led 13 --mode output
anvil pin --assign tmp36_data A0 --mode analog
anvil pin --assign spi --cs 10 # SPI bus with chip-select
anvil pin --assign i2c # I2C (pins auto-resolved)
Generate a pins.h header with #define constants:
anvil pin --generate
Audit your wiring against library requirements:
anvil pin --audit
Pin assignments are stored per-board in .anvil.toml, so switching between
an Uno and a Mega doesn't lose your wiring for either.
Board Profiles
A single project can target multiple boards:
anvil board --add mega
anvil board --add nano --baud 57600
anvil board --default mega
Each board gets its own [boards.<name>] section in .anvil.toml with FQBN,
baud rate, and independent pin assignments. Scripts use the default board
unless you pass --board:
./upload.sh --board nano
List available presets:
anvil new --list-boards
Device Detection
Anvil's scripts auto-detect your board, but you can pin a specific device:
anvil devices # list connected boards
anvil devices --set # auto-detect and save to .anvil.local
anvil devices --set COM3 # save a specific port
The .anvil.local file stores both the port name and the USB VID:PID. If
your board moves to a different port (common on Windows), the scripts find
it by VID:PID automatically.
Refresh and .anvilignore
When you upgrade Anvil, existing projects still have old infrastructure. Refresh updates managed files without touching your code:
anvil refresh # dry run -- shows what would change
anvil refresh --force # update managed files
What's protected
An .anvilignore file (generated automatically) protects student-authored
files from refresh. The defaults protect:
- Your test files (
test/test_unit.cpp,test/test_system.cpp) - Your application code (
lib/app/*) - Your sketch (
*/*.ino) - Your config (
.anvil.toml) - Your project files (
.gitignore,README.md,.editorconfig, etc.)
Managed infrastructure (build scripts, mock headers, CMakeLists.txt, library
drivers, template example tests) gets updated. Missing files are always
recreated, even without --force.
Fine-grained control
anvil refresh --ignore "test/my_helper.h" # protect a custom file
anvil refresh --unignore "test/test_unit.cpp" # allow refresh to update it
anvil refresh --force --file test/test_unit.cpp # one-time override
Patterns support globs: test/*.cpp, lib/app/*, *.h.
Project Structure
your-project/
your-project/your-project.ino Sketch (thin shell, no logic)
lib/
hal/
hal.h Hardware abstraction (pure virtual)
hal_arduino.h Real Arduino implementation
app/
your-project_app.h Your application logic (testable)
drivers/
tmp36/ Sensor driver (interface/impl/mock/sim)
button/ Actuator driver (same pattern)
test/
mocks/
mock_arduino.h Arduino API shims for host compile
mock_hal.h Google Mock HAL
sim_hal.h Stateful simulator HAL
test_weather.cpp Managed example tests (refreshable)
test_unit.cpp Your unit tests (protected)
test_system.cpp Your system tests (protected)
test_tmp36.cpp Library driver tests
CMakeLists.txt Fetches Google Test, compiles tests
build.sh / build.bat Compile sketch
upload.sh / upload.bat Compile + upload to board
monitor.sh / monitor.bat Serial monitor
test.sh / test.bat Run host-side tests
.anvil.toml Project config (tracked by git)
.anvil.local Machine-specific port (gitignored)
.anvilignore File protection rules for refresh
The key architectural rule: application code in lib/app/ depends only on
the Hal interface, never on Arduino.h. The sketch creates the real HAL
and passes it in. Tests create a mock or simulator HAL instead. This is
constructor injection -- the simplest form of dependency inversion.
Commands
| Command | Description |
|---|---|
anvil new NAME [--template T] [--board B] |
Create a new project |
anvil new --list-templates |
Show available templates |
anvil new --list-boards |
Show available board presets |
anvil setup |
Install arduino-cli and AVR core |
anvil doctor [--fix] |
Check system prerequisites |
anvil devices [--set] [--get] [--clear] |
Manage serial port assignment |
anvil add NAME [--pin P] |
Install a device library |
anvil remove NAME |
Remove a device library |
anvil lib [--available] |
List installed or available libraries |
anvil pin --assign NAME PIN [--mode M] |
Assign a pin |
anvil pin --generate |
Generate pins.h header |
anvil pin --audit [--brief] |
Check wiring against library requirements |
anvil pin --capabilities |
Show board pin capabilities |
anvil pin --init-from BOARD |
Copy pin assignments from another board |
anvil board --add NAME [--id FQBN] [--baud N] |
Add a board profile |
anvil board --remove NAME |
Remove a board profile |
anvil board --default NAME |
Set the default board |
anvil refresh [--force] [--file P] [--ignore P] [--unignore P] |
Update project infrastructure |
anvil completions SHELL |
Generate tab-completion script (bash, zsh, fish, powershell) |
Tab Completion
Anvil can generate shell completion scripts so that pressing Tab completes commands, flags, and arguments:
# Bash (add to ~/.bashrc)
eval "$(anvil completions bash)"
# Zsh (add to ~/.zshrc)
eval "$(anvil completions zsh)"
# Fish
anvil completions fish > ~/.config/fish/completions/anvil.fish
# PowerShell (add to $PROFILE)
anvil completions powershell | Out-String | Invoke-Expression
After setup, anvil ref<Tab> completes to anvil refresh, and
anvil pin --<Tab> shows all available flags.
Configuration
.anvil.toml (tracked by git)
[project]
name = "weather_station"
anvil_version = "1.0.0"
template = "weather"
[build]
default = "uno"
warnings = "more"
include_dirs = ["lib/hal", "lib/app", "lib/drivers/tmp36"]
extra_flags = ["-Werror"]
[boards.uno]
fqbn = "arduino:avr:uno"
baud = 115200
[boards.mega]
fqbn = "arduino:avr:mega:cpu=atmega2560"
baud = 115200
[libraries]
tmp36 = "0.1.0"
[pins.uno]
tmp36_data = { pin = 14, mode = "analog" }
.anvil.local (gitignored)
port = "COM3"
vid_pid = "0403:6001"
Building from Source
cargo build --release
Binary at target/release/anvil (Linux) or target\release\anvil.exe
(Windows). Requires Rust 2021 edition.
The test suite:
cargo test
615 tests (137 unit + 478 integration), ~4 seconds, zero warnings.
License
MIT -- see LICENSE.