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.
If there is no pre-built binary for your platform, or you want to hack on Anvil itself, see Building from Source below.
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
anvil new clicker --template button --board uno
The weather template adds a WeatherApp with a TMP36 temperature sensor,
managed example tests, and student test starters. The button template adds
a ButtonApp with edge detection that prints "Button pressed!" to the serial
monitor each time you press a button -- no repeated messages from holding it
down. Both templates include mock and simulator patterns. To see all options:
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
If you want to build Anvil yourself -- either because there is no pre-built binary for your platform, or because you want to contribute -- here is what you need.
Prerequisites
Anvil is written in Rust and compiles to a single static binary. You need:
- Rust toolchain (stable, 2021 edition or later)
- A C linker (
gccor equivalent -- Rust uses it under the hood) - MinGW (only if cross-compiling a Windows binary from Linux)
- zip (only for packaging release artifacts)
Linux / WSL from scratch
A fresh Ubuntu or WSL instance needs three commands:
# 1. System packages (C linker + cross-compile + packaging tools)
sudo apt update && sudo apt install build-essential mingw-w64 zip
# 2. Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# 3. Build
cargo build --release
The release binary lands at target/release/anvil. Copy it somewhere in
your PATH.
Windows (native)
Install Rust from rustup.rs, which includes the MSVC toolchain. Then:
cargo build --release
The binary lands at target\release\anvil.exe.
Release builds (Linux + Windows from one machine)
The build-release.sh script at the repo root builds optimized, stripped
binaries for both platforms and packages them into tarballs and zips. It
reads the version from Cargo.toml (the single source of truth) and
accepts an optional suffix for pre-release builds:
./build-release.sh # uses version from Cargo.toml (e.g. 1.0.0)
./build-release.sh beta1 # appends suffix (e.g. 1.0.0-beta1)
./build-release.sh rc1 # appends suffix (e.g. 1.0.0-rc1)
This produces a release-artifacts/ directory with:
anvil-1.0.0-linux-x86_64.tar.gz
anvil-1.0.0-linux-x86_64.zip
anvil-1.0.0-windows-x86_64.zip
SHA256SUMS
Upload these to a Gitea release. The script requires build-essential,
mingw-w64, and zip as described above.
Running the test suite
cargo test
650 tests (137 unit + 506 integration + 7 end-to-end), zero warnings. The e2e tests generate real projects and compile their C++ test suites, catching build-system issues like missing linker flags and include paths. They require cmake and a C++ compiler; if those tools are not installed, the compile tests skip gracefully and everything else still passes.
License
MIT -- see LICENSE.