Files
anvil/README.md
Eric Ratliff 34d6a765b0
Some checks failed
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows MSVC) (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
Updated readme
2026-02-23 08:34:10 -06:00

13 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 terminal demo

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
    build.sh / build.bat / build.ps1    Compile sketch (bat wraps ps1)
    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 (gcc or 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

The test suite covers unit, integration, and end-to-end scenarios. 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.