Anvil v1.0.0 -- Arduino build tool with HAL and test scaffolding

Single-binary CLI that scaffolds testable Arduino projects, compiles,
uploads, and monitors serial output. Templates embed a hardware
abstraction layer, Google Mock infrastructure, and CMake-based host
tests so application logic can be verified without hardware.

Commands: new, doctor, setup, devices, build, upload, monitor
39 Rust tests (21 unit, 18 integration)
Cross-platform: Linux and Windows
This commit is contained in:
Eric Ratliff
2026-02-15 11:16:17 -06:00
commit 3298844399
41 changed files with 4866 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
#ifndef APP_H
#define APP_H
#include <hal.h>
/*
* BlinkApp -- Testable blink logic, decoupled from hardware.
*
* Blinks an LED and reads a button. When the button is pressed,
* the blink rate doubles (toggles between normal and fast mode).
*
* All hardware access goes through the injected Hal pointer. This
* class has no dependency on Arduino.h and compiles on any host.
*/
class BlinkApp {
public:
static constexpr uint8_t DEFAULT_LED_PIN = LED_BUILTIN; // pin 13
static constexpr uint8_t DEFAULT_BUTTON_PIN = 2;
static constexpr unsigned long SLOW_INTERVAL_MS = 500;
static constexpr unsigned long FAST_INTERVAL_MS = 125;
BlinkApp(Hal* hal,
uint8_t led_pin = DEFAULT_LED_PIN,
uint8_t button_pin = DEFAULT_BUTTON_PIN)
: hal_(hal)
, led_pin_(led_pin)
, button_pin_(button_pin)
, led_state_(LOW)
, fast_mode_(false)
, last_toggle_ms_(0)
, last_button_state_(HIGH) // pulled up, so HIGH = not pressed
{}
// Call once from setup()
void begin() {
hal_->pinMode(led_pin_, OUTPUT);
hal_->pinMode(button_pin_, INPUT_PULLUP);
hal_->serialBegin(115200);
hal_->serialPrintln("BlinkApp started");
last_toggle_ms_ = hal_->millis();
}
// Call repeatedly from loop()
void update() {
handleButton();
handleBlink();
}
// -- Accessors for testing ----------------------------------------------
bool ledState() const { return led_state_ == HIGH; }
bool fastMode() const { return fast_mode_; }
unsigned long interval() const {
return fast_mode_ ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS;
}
private:
void handleButton() {
uint8_t reading = hal_->digitalRead(button_pin_);
// Detect falling edge (HIGH -> LOW = button press with INPUT_PULLUP)
if (last_button_state_ == HIGH && reading == LOW) {
fast_mode_ = !fast_mode_;
hal_->serialPrintln(fast_mode_ ? "FAST" : "SLOW");
}
last_button_state_ = reading;
}
void handleBlink() {
unsigned long now = hal_->millis();
unsigned long target = fast_mode_ ? FAST_INTERVAL_MS : SLOW_INTERVAL_MS;
if (now - last_toggle_ms_ >= target) {
led_state_ = (led_state_ == HIGH) ? LOW : HIGH;
hal_->digitalWrite(led_pin_, led_state_);
last_toggle_ms_ = now;
}
}
Hal* hal_;
uint8_t led_pin_;
uint8_t button_pin_;
uint8_t led_state_;
bool fast_mode_;
unsigned long last_toggle_ms_;
uint8_t last_button_state_;
};
#endif // APP_H

View File

@@ -0,0 +1,67 @@
#ifndef HAL_H
#define HAL_H
#include <stdint.h>
// Pin modes (match Arduino constants)
#ifndef INPUT
#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2
#endif
#ifndef LOW
#define LOW 0x0
#define HIGH 0x1
#endif
// LED_BUILTIN for host builds
#ifndef LED_BUILTIN
#define LED_BUILTIN 13
#endif
/*
* Hardware Abstraction Layer
*
* Abstract interface over GPIO, timing, serial, and I2C. Sketch logic
* depends on this interface only -- never on Arduino.h directly.
*
* Two implementations:
* hal_arduino.h -- real hardware (included by .ino files)
* mock_hal.h -- Google Mock (included by test files)
*/
class Hal {
public:
virtual ~Hal() = default;
// -- GPIO ---------------------------------------------------------------
virtual void pinMode(uint8_t pin, uint8_t mode) = 0;
virtual void digitalWrite(uint8_t pin, uint8_t value) = 0;
virtual uint8_t digitalRead(uint8_t pin) = 0;
virtual int analogRead(uint8_t pin) = 0;
virtual void analogWrite(uint8_t pin, int value) = 0;
// -- Timing -------------------------------------------------------------
virtual unsigned long millis() = 0;
virtual unsigned long micros() = 0;
virtual void delay(unsigned long ms) = 0;
virtual void delayMicroseconds(unsigned long us) = 0;
// -- Serial -------------------------------------------------------------
virtual void serialBegin(unsigned long baud) = 0;
virtual void serialPrint(const char* msg) = 0;
virtual void serialPrintln(const char* msg) = 0;
virtual int serialAvailable() = 0;
virtual int serialRead() = 0;
// -- I2C ----------------------------------------------------------------
virtual void i2cBegin() = 0;
virtual void i2cBeginTransmission(uint8_t addr) = 0;
virtual size_t i2cWrite(uint8_t data) = 0;
virtual uint8_t i2cEndTransmission() = 0;
virtual uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) = 0;
virtual int i2cAvailable() = 0;
virtual int i2cRead() = 0;
};
#endif // HAL_H

View File

@@ -0,0 +1,93 @@
#ifndef HAL_ARDUINO_H
#define HAL_ARDUINO_H
/*
* Real hardware implementation of the HAL.
*
* This file includes Arduino.h and Wire.h, so it can only be compiled
* by avr-gcc (via arduino-cli). It is included by .ino files only.
*
* Every method is a trivial passthrough to the real Arduino function.
* The point is not to add logic here -- it is to keep Arduino.h out
* of your application code so that code can compile on the host.
*/
#include <Arduino.h>
#include <Wire.h>
#include <hal.h>
class ArduinoHal : public Hal {
public:
// -- GPIO ---------------------------------------------------------------
void pinMode(uint8_t pin, uint8_t mode) override {
::pinMode(pin, mode);
}
void digitalWrite(uint8_t pin, uint8_t value) override {
::digitalWrite(pin, value);
}
uint8_t digitalRead(uint8_t pin) override {
return ::digitalRead(pin);
}
int analogRead(uint8_t pin) override {
return ::analogRead(pin);
}
void analogWrite(uint8_t pin, int value) override {
::analogWrite(pin, value);
}
// -- Timing -------------------------------------------------------------
unsigned long millis() override {
return ::millis();
}
unsigned long micros() override {
return ::micros();
}
void delay(unsigned long ms) override {
::delay(ms);
}
void delayMicroseconds(unsigned long us) override {
::delayMicroseconds(us);
}
// -- Serial -------------------------------------------------------------
void serialBegin(unsigned long baud) override {
Serial.begin(baud);
}
void serialPrint(const char* msg) override {
Serial.print(msg);
}
void serialPrintln(const char* msg) override {
Serial.println(msg);
}
int serialAvailable() override {
return Serial.available();
}
int serialRead() override {
return Serial.read();
}
// -- I2C ----------------------------------------------------------------
void i2cBegin() override {
Wire.begin();
}
void i2cBeginTransmission(uint8_t addr) override {
Wire.beginTransmission(addr);
}
size_t i2cWrite(uint8_t data) override {
return Wire.write(data);
}
uint8_t i2cEndTransmission() override {
return Wire.endTransmission();
}
uint8_t i2cRequestFrom(uint8_t addr, uint8_t count) override {
return Wire.requestFrom(addr, count);
}
int i2cAvailable() override {
return Wire.available();
}
int i2cRead() override {
return Wire.read();
}
};
#endif // HAL_ARDUINO_H