# 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](https://nxlearn.net) 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: ```bash 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](#building-from-source) below. ### Create a project ```bash 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: ```bash ./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: ```bash 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: ```bash 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. ```bash 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: ```bash anvil lib --available # all libraries in the registry anvil lib # libraries installed in this project ``` Remove a library: ```bash anvil remove tmp36 ``` Each library installs to `lib/drivers//` 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. ```bash 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: ```bash anvil pin --generate ``` Audit your wiring against library requirements: ```bash 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: ```bash anvil board --add mega anvil board --add nano --baud 57600 anvil board --default mega ``` Each board gets its own `[boards.]` section in `.anvil.toml` with FQBN, baud rate, and independent pin assignments. Scripts use the default board unless you pass `--board`: ```bash ./upload.sh --board nano ``` List available presets: ```bash anvil new --list-boards ``` --- ## Device Detection Anvil's scripts auto-detect your board, but you can pin a specific device: ```bash 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: ```bash 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 ```bash 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 # 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` completes to `anvil refresh`, and `anvil pin --` shows all available flags. --- ## Configuration ### .anvil.toml (tracked by git) ```toml [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) ```toml 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: ```bash # 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](https://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: ```bash ./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 ```bash cargo test ``` 615 tests (137 unit + 478 integration), ~4 seconds, zero warnings. --- ## License MIT -- see [LICENSE](LICENSE).