Add embedded device library registry with full lifecycle management, automated test integration, and pin assignment workflow. Library system: - Library registry embedded in binary via include_dir! (same as templates) - library.toml metadata format: name, version, bus type, pins, provided files - anvil add <name> extracts headers to lib/drivers/<name>/, test files to test/ - anvil remove <name> cleans up all files, config entries, and include paths - anvil lib lists installed libraries with status - anvil lib --available shows registry with student-friendly wiring summary - [libraries] section in .anvil.toml tracks installed libraries and versions - CMake auto-discovers lib/drivers/*/ for include paths at configure time TMP36 analog temperature sensor (reference implementation): - TempSensor abstract interface (readCelsius/readFahrenheit/readRaw) - Tmp36Analog: real hardware impl via Hal::analogRead with configurable Vref - Tmp36Mock: programmable values, read counting, no hardware needed - Tmp36Sim: deterministic noise via seeded LCG for repeatable system tests - test_tmp36.cpp: 21 Google Test cases covering mock, analog, sim, polymorphism - TMP36 formula: voltage_mV = raw * Vref_mV / 1024, temp_C = (mV - 500) / 10 Automated test integration: - Library test files (test_*.cpp) route to test/ during extraction - CMakeLists.txt auto-discovers test_*.cpp via GLOB, builds each as own target - anvil remove cleans up test files alongside driver headers - Zero manual CMake editing: add library, run test --clean, tests appear Pin assignment integration: - anvil add <name> --pin A0 does extract + pin assignment in one step - Without --pin, prints step-by-step wiring guidance with copy-paste commands - anvil pin --audit flags installed libraries with unassigned pins - Audit works even with zero existing pin assignments (fixed early-return bug) - LibraryMeta helpers: wiring_summary(), pin_roles(), default_mode() - Bus-aware guidance: analog pins, I2C bus registration, SPI with CS selection UX improvements: - anvil lib --available shows "Needs: 1 analog pin (e.g. A0)" not raw metadata - anvil add prints app code example, test code example, and next step - anvil pin --audit prints exact commands to resolve missing library pins - anvil remove shows test file deletion in output Files added: - libraries/tmp36/library.toml - libraries/tmp36/src/tmp36.h, tmp36_analog.h, tmp36_mock.h, tmp36_sim.h - libraries/tmp36/src/test_tmp36.cpp - src/library/mod.rs Files modified: - src/lib.rs, src/main.rs, src/commands/mod.rs - src/commands/lib.rs (add/remove/list/list_available with --pin support) - src/commands/pin.rs (audit library pin warnings, print_library_pin_warnings) - src/project/config.rs (libraries HashMap field) - templates/basic/test/CMakeLists.txt.tmpl (driver + test auto-discovery) Tests: 254 total (89 unit + 165 integration) - 12 library/mod.rs unit tests (registry, extraction, helpers) - 2 commands/lib.rs unit tests (class name derivation) - 30+ new integration tests covering library lifecycle, pin integration, audit flows, file routing, CMake discovery, config roundtrips, ASCII compliance, polymorphism contracts, and idempotent add/remove cycles
Anvil
Arduino project generator and build tool -- forges clean embedded projects.
A single binary that scaffolds self-contained Arduino projects with hardware abstraction, Google Mock infrastructure, and a streamlined build/upload/monitor workflow. Works on Linux and Windows.
Generated projects are fully standalone -- they only need arduino-cli in
PATH. The Anvil binary is a scaffolding and diagnostic tool, not a runtime
dependency.
Anvil is a Nexus Workshops project.
Install
Download the latest release binary for your platform:
# Linux
chmod +x anvil
sudo mv anvil /usr/local/bin/
# Windows -- add anvil.exe to a directory in your PATH
Then run first-time setup:
anvil setup
This checks for arduino-cli, installs the arduino:avr core, and verifies
your system is ready. If something is missing, Anvil tells you exactly what
to do.
Your First Project
Create a project, plug in your board, and upload:
anvil new blink
cd blink
On Linux/macOS:
./build.sh # compile only (verify)
./upload.sh # compile + upload to board
./upload.sh --monitor # compile, upload, open serial monitor
./monitor.sh # serial monitor (no compile)
./test/run_tests.sh # host-side unit tests (no board needed)
On Windows:
build REM compile only
upload REM compile + upload
upload --monitor REM compile, upload, open serial monitor
monitor REM serial monitor
test\run_tests REM host-side unit tests
Every script reads its settings from .anvil.toml in the project root.
You edit that file to change the board, baud rate, include paths, or
compiler flags. No Anvil binary required for any of this -- students clone
the repo, plug in a board, and run upload.
Telling Anvil Which Board to Use
When you run upload without specifying a port, Anvil's scripts auto-detect
the board. They prefer USB serial ports over legacy motherboard COM ports,
which is usually the right thing. But if you have multiple boards plugged in,
or you want a consistent setup, you can pin a specific device.
See what's connected
anvil devices
This lists every serial port with its board name, protocol, and -- for USB
devices -- its VID:PID identifier. The VID:PID is a pair of hex codes that
uniquely identifies the USB chip on your board (e.g. 0403:6001 for an FTDI,
1a86:7523 for a CH340).
Save a default device
anvil devices --set COM3 # save a specific port
anvil devices --set # auto-detect the best port and save it
Both forms write a .anvil.local file in your project directory. This file
stores the port name and the VID:PID of the device on that port. It looks
like this:
# Machine-specific Anvil config (not tracked by git)
port = "COM3"
vid_pid = "0403:6001"
The .anvil.local file is gitignored by default. Each machine that works on
the project keeps its own copy with its own port assignment.
Why VID:PID matters
On Windows, if you unplug a USB device and plug it back in to a different
port, it often gets a new COM number (COM3 becomes COM5). The VID:PID
doesn't change -- it's baked into the USB chip. When the scripts see a
VID:PID in .anvil.local, they search for that device on whatever port
it's currently on. If it moved, you'll see:
info Device 0403:6001 found on COM5 (moved from COM3)
No manual intervention needed.
Check what's saved
anvil devices --get
This shows the saved port and VID:PID for the current project, whether the device is currently connected, and whether it has moved to a different port.
Port resolution priority
When the scripts need a port, they check in this order:
-pflag --upload.bat -p COM3always wins- VID:PID from
.anvil.local-- finds the device regardless of port number - Saved port from
.anvil.local-- fallback if VID:PID lookup fails - Auto-detect -- prefers USB serial over legacy COM ports
Specifying a project directory
Both --set and --get default to the current directory, but you can point
them at another project:
anvil devices --set COM3 -d ../other-project
anvil devices --get -d ../other-project
Updating Project Scripts
When you upgrade Anvil, your existing projects still have the old build
scripts. The refresh command updates them to the latest versions without
touching your source code, configuration, or test files:
anvil refresh # check current project (dry run)
anvil refresh --force # overwrite differing scripts
anvil refresh ../other-project # check a different project
Refresh only replaces build infrastructure: build.sh, build.bat,
upload.sh, upload.bat, monitor.sh, monitor.bat, _detect_port.ps1,
and test/run_tests.sh, test/run_tests.bat. It never touches .anvil.toml,
.anvil.local, your .ino files, HAL headers, app code, mocks, or test
sources.
What Anvil Does vs. What the Project Does
| Need Anvil for | Don't need Anvil for |
|---|---|
anvil new (create project) |
./build.sh (compile) |
anvil setup (install core) |
./upload.sh (flash) |
anvil doctor (diagnose) |
./monitor.sh (serial) |
anvil devices (port scan) |
./test/run_tests.sh (test) |
anvil refresh (update scripts) |
Once a project is created, Anvil is optional for daily work.
Commands
| Command | Description |
|---|---|
anvil new NAME |
Create a new project with HAL and test scaffold |
anvil setup |
Install arduino-cli and AVR core |
anvil doctor |
Check system prerequisites |
anvil devices |
List connected boards and serial ports |
anvil devices --set [PORT] |
Save default port + VID:PID to .anvil.local |
anvil devices --get |
Show saved port for this project |
anvil refresh [DIR] [--force] |
Update project scripts to latest templates |
Project Structure
Every generated project follows the same layout:
your-project/
your-project/your-project.ino Entry point (setup + loop)
lib/
hal/
hal.h Hardware abstraction interface
hal_arduino.h Real hardware implementation
app/
your-project_app.h Application logic (testable)
test/
mocks/
mock_hal.h Google Mock HAL
sim_hal.h Stateful simulator HAL
test_unit.cpp Unit tests
CMakeLists.txt Test build system
run_tests.sh / .bat Test runner
build.sh / build.bat Compile sketch
upload.sh / upload.bat Compile + upload to board
monitor.sh / monitor.bat Serial monitor
_detect_port.ps1 Port detection helper (Windows)
.anvil.toml Project config (tracked by git)
.anvil.local Machine-specific config (gitignored)
All hardware access goes through the Hal interface in lib/hal/hal.h.
Application code in lib/app/ depends only on Hal, never on Arduino.h
directly. This means the app logic compiles and runs on the host for testing
with Google Mock -- no board required.
Configuration
.anvil.toml (tracked by git)
Shared project settings. Edit this to change the board, baud rate, or compiler flags:
[project]
name = "blink"
anvil_version = "1.0.0"
[build]
fqbn = "arduino:avr:uno"
warnings = "more"
include_dirs = ["lib/hal", "lib/app"]
extra_flags = ["-Werror"]
[monitor]
baud = 115200
.anvil.local (not tracked by git)
Machine-specific settings, created by anvil devices --set:
port = "COM3"
vid_pid = "0403:6001"
Building from Source
cargo build --release
The release binary is at target/release/anvil (Linux) or
target\release\anvil.exe (Windows).
License
MIT -- see LICENSE.