feat: Layer 2 device library system with TMP36 reference driver
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
This commit is contained in:
15
libraries/tmp36/library.toml
Normal file
15
libraries/tmp36/library.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[library]
|
||||
name = "tmp36"
|
||||
version = "0.1.0"
|
||||
description = "TMP36 analog temperature sensor"
|
||||
|
||||
[requires]
|
||||
bus = "analog"
|
||||
pins = ["data"]
|
||||
|
||||
[provides]
|
||||
interface = "tmp36.h"
|
||||
implementation = "tmp36_analog.h"
|
||||
mock = "tmp36_mock.h"
|
||||
simulation = "tmp36_sim.h"
|
||||
test = "test_tmp36.cpp"
|
||||
252
libraries/tmp36/src/test_tmp36.cpp
Normal file
252
libraries/tmp36/src/test_tmp36.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* TMP36 Driver Tests
|
||||
*
|
||||
* Auto-generated by: anvil add tmp36
|
||||
* These tests verify the TMP36 driver mock and simulation without
|
||||
* any hardware. They run alongside your unit and system tests.
|
||||
*
|
||||
* To run: ./test.sh (all tests)
|
||||
* ./test.sh --system (skips these -- use no filter to include)
|
||||
* ./test.sh --unit (skips these -- use no filter to include)
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "mock_arduino.h"
|
||||
#include "sim_hal.h"
|
||||
#include "tmp36.h"
|
||||
#include "tmp36_analog.h"
|
||||
#include "tmp36_mock.h"
|
||||
#include "tmp36_sim.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock: basic functionality
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Tmp36MockTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
mock_arduino_reset();
|
||||
}
|
||||
|
||||
Tmp36Mock sensor;
|
||||
};
|
||||
|
||||
TEST_F(Tmp36MockTest, DefaultsToRoomTemperature) {
|
||||
float temp = sensor.readCelsius();
|
||||
EXPECT_NEAR(temp, 22.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, SetTemperatureReturnsExactValue) {
|
||||
sensor.setTemperature(37.5f);
|
||||
EXPECT_FLOAT_EQ(sensor.readCelsius(), 37.5f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, FahrenheitConversion) {
|
||||
sensor.setTemperature(0.0f);
|
||||
EXPECT_NEAR(sensor.readFahrenheit(), 32.0f, 0.01f);
|
||||
|
||||
sensor.setTemperature(100.0f);
|
||||
EXPECT_NEAR(sensor.readFahrenheit(), 212.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, BoilingPointConversion) {
|
||||
sensor.setTemperature(100.0f);
|
||||
EXPECT_NEAR(sensor.readFahrenheit(), 212.0f, 0.01f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, NegativeTemperature) {
|
||||
sensor.setTemperature(-10.0f);
|
||||
EXPECT_FLOAT_EQ(sensor.readCelsius(), -10.0f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, ReadCountTracking) {
|
||||
EXPECT_EQ(sensor.readCount(), 0);
|
||||
sensor.readCelsius();
|
||||
sensor.readCelsius();
|
||||
sensor.readRaw();
|
||||
EXPECT_EQ(sensor.readCount(), 3);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, ReadCountReset) {
|
||||
sensor.readCelsius();
|
||||
sensor.readCelsius();
|
||||
sensor.resetReadCount();
|
||||
EXPECT_EQ(sensor.readCount(), 0);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36MockTest, SetRawReturnsExactValue) {
|
||||
sensor.setRaw(512);
|
||||
EXPECT_EQ(sensor.readRaw(), 512);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Analog: real implementation via SimHal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Tmp36AnalogTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
mock_arduino_reset();
|
||||
}
|
||||
|
||||
SimHal hal;
|
||||
};
|
||||
|
||||
TEST_F(Tmp36AnalogTest, RoomTemperatureReading) {
|
||||
// 22 C -> 720 mV -> raw = 720 * 1024 / 5000 = 147.5 -> ~148
|
||||
Tmp36Analog sensor(&hal, A0, 5.0f);
|
||||
hal.setAnalog(A0, 148);
|
||||
|
||||
float temp = sensor.readCelsius();
|
||||
// 148 * 5000/1024 = 722.6 mV -> (722.6 - 500) / 10 = 22.26 C
|
||||
EXPECT_NEAR(temp, 22.26f, 0.1f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36AnalogTest, FreezingPoint) {
|
||||
// 0 C -> 500 mV -> raw = 500 * 1024 / 5000 = 102.4 -> ~102
|
||||
Tmp36Analog sensor(&hal, A0, 5.0f);
|
||||
hal.setAnalog(A0, 102);
|
||||
|
||||
float temp = sensor.readCelsius();
|
||||
EXPECT_NEAR(temp, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36AnalogTest, HighTemperature) {
|
||||
// 50 C -> 1000 mV -> raw = 1000 * 1024 / 5000 = 204.8 -> ~205
|
||||
Tmp36Analog sensor(&hal, A0, 5.0f);
|
||||
hal.setAnalog(A0, 205);
|
||||
|
||||
float temp = sensor.readCelsius();
|
||||
EXPECT_NEAR(temp, 50.0f, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36AnalogTest, ThreeVoltReference) {
|
||||
// Same raw value, different ref voltage changes the result
|
||||
Tmp36Analog sensor_5v(&hal, A0, 5.0f);
|
||||
Tmp36Analog sensor_3v(&hal, A0, 3.3f);
|
||||
hal.setAnalog(A0, 200);
|
||||
|
||||
float temp_5v = sensor_5v.readCelsius();
|
||||
float temp_3v = sensor_3v.readCelsius();
|
||||
|
||||
// 3.3V ref reads higher mV per count, so higher temperature
|
||||
// Actually the opposite: lower ref means each count = fewer mV
|
||||
EXPECT_NE(temp_5v, temp_3v);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36AnalogTest, ReadRawReturnsAnalogValue) {
|
||||
Tmp36Analog sensor(&hal, A0, 5.0f);
|
||||
hal.setAnalog(A0, 512);
|
||||
EXPECT_EQ(sensor.readRaw(), 512);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36AnalogTest, DifferentPin) {
|
||||
// Can use any analog pin
|
||||
Tmp36Analog sensor(&hal, A2, 5.0f);
|
||||
hal.setAnalog(A2, 148);
|
||||
|
||||
float temp = sensor.readCelsius();
|
||||
EXPECT_NEAR(temp, 22.26f, 0.1f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation: noise and determinism
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class Tmp36SimTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
mock_arduino_reset();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(Tmp36SimTest, ReturnsValuesNearBase) {
|
||||
Tmp36Sim sensor(25.0f, 1.0f);
|
||||
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
float temp = sensor.readCelsius();
|
||||
EXPECT_GE(temp, 24.0f) << "Reading " << i << " too low";
|
||||
EXPECT_LE(temp, 26.0f) << "Reading " << i << " too high";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(Tmp36SimTest, ZeroNoiseReturnsExact) {
|
||||
Tmp36Sim sensor(25.0f, 0.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(sensor.readCelsius(), 25.0f);
|
||||
EXPECT_FLOAT_EQ(sensor.readCelsius(), 25.0f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36SimTest, DeterministicWithSameSeed) {
|
||||
Tmp36Sim s1(25.0f, 1.0f);
|
||||
Tmp36Sim s2(25.0f, 1.0f);
|
||||
s1.setSeed(99);
|
||||
s2.setSeed(99);
|
||||
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
EXPECT_FLOAT_EQ(s1.readCelsius(), s2.readCelsius())
|
||||
<< "Reading " << i << " should match with same seed";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(Tmp36SimTest, DifferentSeedsDifferentValues) {
|
||||
Tmp36Sim s1(25.0f, 1.0f);
|
||||
Tmp36Sim s2(25.0f, 1.0f);
|
||||
s1.setSeed(1);
|
||||
s2.setSeed(2);
|
||||
|
||||
// At least one reading should differ
|
||||
bool any_differ = false;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
if (s1.readCelsius() != s2.readCelsius()) {
|
||||
any_differ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(any_differ);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36SimTest, BaseTemperatureChange) {
|
||||
Tmp36Sim sensor(20.0f, 0.0f);
|
||||
EXPECT_FLOAT_EQ(sensor.readCelsius(), 20.0f);
|
||||
|
||||
sensor.setBaseTemperature(40.0f);
|
||||
EXPECT_FLOAT_EQ(sensor.readCelsius(), 40.0f);
|
||||
}
|
||||
|
||||
TEST_F(Tmp36SimTest, RawValueMatchesTemperature) {
|
||||
Tmp36Sim sensor(25.0f, 0.0f);
|
||||
|
||||
// 25 C -> 750 mV -> raw = 750 * 1024 / 5000 = 153.6 -> ~153
|
||||
int raw = sensor.readRaw();
|
||||
EXPECT_NEAR(raw, 153, 2);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Polymorphism: all impls work through TempSensor pointer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(Tmp36PolymorphismTest, AllImplsWorkThroughBasePointer) {
|
||||
mock_arduino_reset();
|
||||
SimHal hal;
|
||||
hal.setAnalog(A0, 148);
|
||||
|
||||
Tmp36Analog analog_sensor(&hal, A0, 5.0f);
|
||||
Tmp36Mock mock_sensor;
|
||||
Tmp36Sim sim_sensor(22.0f, 0.0f);
|
||||
|
||||
mock_sensor.setTemperature(22.0f);
|
||||
|
||||
TempSensor* sensors[] = { &analog_sensor, &mock_sensor, &sim_sensor };
|
||||
|
||||
for (auto* s : sensors) {
|
||||
float c = s->readCelsius();
|
||||
float f = s->readFahrenheit();
|
||||
int raw = s->readRaw();
|
||||
|
||||
EXPECT_GT(c, 10.0f);
|
||||
EXPECT_LT(c, 40.0f);
|
||||
EXPECT_GT(f, 50.0f);
|
||||
EXPECT_GE(raw, 0);
|
||||
}
|
||||
}
|
||||
54
libraries/tmp36/src/tmp36.h
Normal file
54
libraries/tmp36/src/tmp36.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#ifndef TMP36_H
|
||||
#define TMP36_H
|
||||
|
||||
/*
|
||||
* TMP36 Temperature Sensor -- Abstract Interface
|
||||
*
|
||||
* This is the contract that your application code depends on. It does
|
||||
* not know or care whether the temperature comes from a real TMP36
|
||||
* wired to an analog pin, a test mock with canned values, or a
|
||||
* simulation with realistic noise.
|
||||
*
|
||||
* Three implementations ship with this driver:
|
||||
* tmp36_analog.h -- Real hardware via Hal::analogRead()
|
||||
* tmp36_mock.h -- Test mock with programmable values
|
||||
* tmp36_sim.h -- Simulation with configurable noise
|
||||
*
|
||||
* Usage in application code:
|
||||
* #include "tmp36.h"
|
||||
*
|
||||
* class ThermostatApp {
|
||||
* public:
|
||||
* ThermostatApp(Hal* hal, TempSensor* sensor)
|
||||
* : hal_(hal), sensor_(sensor) {}
|
||||
*
|
||||
* void update() {
|
||||
* float temp = sensor_->readCelsius();
|
||||
* if (temp > threshold_) {
|
||||
* hal_->digitalWrite(FAN_PIN, HIGH);
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* Generated by Anvil -- https://github.com/nexus-workshops/anvil
|
||||
*/
|
||||
|
||||
class TempSensor {
|
||||
public:
|
||||
virtual ~TempSensor() = default;
|
||||
|
||||
/// Read temperature in degrees Celsius.
|
||||
virtual float readCelsius() = 0;
|
||||
|
||||
/// Read temperature in degrees Fahrenheit.
|
||||
/// Default implementation converts from Celsius.
|
||||
virtual float readFahrenheit() {
|
||||
return readCelsius() * 9.0f / 5.0f + 32.0f;
|
||||
}
|
||||
|
||||
/// Read the raw ADC value (0-1023 for 10-bit).
|
||||
/// Useful for debugging or custom calibration.
|
||||
virtual int readRaw() = 0;
|
||||
};
|
||||
|
||||
#endif // TMP36_H
|
||||
48
libraries/tmp36/src/tmp36_analog.h
Normal file
48
libraries/tmp36/src/tmp36_analog.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#ifndef TMP36_ANALOG_H
|
||||
#define TMP36_ANALOG_H
|
||||
|
||||
#include "tmp36.h"
|
||||
#include "hal.h"
|
||||
|
||||
/*
|
||||
* TMP36 -- Real hardware implementation (analog read).
|
||||
*
|
||||
* The TMP36 outputs a voltage proportional to temperature:
|
||||
* - 10 mV per degree Celsius
|
||||
* - 500 mV offset at 0 C (so 750 mV = 25 C)
|
||||
* - Range: -40 C to +125 C
|
||||
*
|
||||
* Wiring:
|
||||
* TMP36 pin 1 (left) -> 5V (or 3.3V)
|
||||
* TMP36 pin 2 (middle) -> Analog input (A0 by default)
|
||||
* TMP36 pin 3 (right) -> GND
|
||||
*
|
||||
* If your board runs at 3.3V, pass 3.3f as ref_voltage.
|
||||
*/
|
||||
class Tmp36Analog : public TempSensor {
|
||||
public:
|
||||
/// Create a TMP36 sensor on the given analog pin.
|
||||
/// ref_voltage: board reference voltage (5.0 for Uno, 3.3 for Due/ESP).
|
||||
Tmp36Analog(Hal* hal, uint8_t pin, float ref_voltage = 5.0f)
|
||||
: hal_(hal)
|
||||
, pin_(pin)
|
||||
, ref_mv_(ref_voltage * 1000.0f)
|
||||
{}
|
||||
|
||||
float readCelsius() override {
|
||||
int raw = hal_->analogRead(pin_);
|
||||
float mv = (float)raw * ref_mv_ / 1024.0f;
|
||||
return (mv - 500.0f) / 10.0f;
|
||||
}
|
||||
|
||||
int readRaw() override {
|
||||
return hal_->analogRead(pin_);
|
||||
}
|
||||
|
||||
private:
|
||||
Hal* hal_;
|
||||
uint8_t pin_;
|
||||
float ref_mv_;
|
||||
};
|
||||
|
||||
#endif // TMP36_ANALOG_H
|
||||
60
libraries/tmp36/src/tmp36_mock.h
Normal file
60
libraries/tmp36/src/tmp36_mock.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#ifndef TMP36_MOCK_H
|
||||
#define TMP36_MOCK_H
|
||||
|
||||
#include "tmp36.h"
|
||||
|
||||
/*
|
||||
* TMP36 -- Test mock with programmable values.
|
||||
*
|
||||
* Use this in tests to control exactly what temperature your
|
||||
* application sees, without any real hardware.
|
||||
*
|
||||
* Example:
|
||||
* Tmp36Mock sensor;
|
||||
* sensor.setTemperature(30.0f);
|
||||
*
|
||||
* ThermostatApp app(&sim, &sensor);
|
||||
* app.update();
|
||||
*
|
||||
* EXPECT_EQ(sim.getPin(FAN_PIN), HIGH); // fan should be on
|
||||
*
|
||||
* You can also track how many times the sensor was read:
|
||||
* sensor.resetReadCount();
|
||||
* app.update();
|
||||
* EXPECT_EQ(sensor.readCount(), 1);
|
||||
*/
|
||||
class Tmp36Mock : public TempSensor {
|
||||
public:
|
||||
/// Set the temperature that readCelsius() returns.
|
||||
void setTemperature(float celsius) {
|
||||
temp_c_ = celsius;
|
||||
}
|
||||
|
||||
/// Set the raw ADC value that readRaw() returns.
|
||||
void setRaw(int raw) {
|
||||
raw_ = raw;
|
||||
}
|
||||
|
||||
float readCelsius() override {
|
||||
++read_count_;
|
||||
return temp_c_;
|
||||
}
|
||||
|
||||
int readRaw() override {
|
||||
++read_count_;
|
||||
return raw_;
|
||||
}
|
||||
|
||||
/// How many times has the sensor been read?
|
||||
int readCount() const { return read_count_; }
|
||||
|
||||
/// Reset the read counter.
|
||||
void resetReadCount() { read_count_ = 0; }
|
||||
|
||||
private:
|
||||
float temp_c_ = 22.0f; // Default: room temperature
|
||||
int raw_ = 153; // ~22 C at 5V reference
|
||||
int read_count_ = 0;
|
||||
};
|
||||
|
||||
#endif // TMP36_MOCK_H
|
||||
74
libraries/tmp36/src/tmp36_sim.h
Normal file
74
libraries/tmp36/src/tmp36_sim.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#ifndef TMP36_SIM_H
|
||||
#define TMP36_SIM_H
|
||||
|
||||
#include "tmp36.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
/*
|
||||
* TMP36 -- Simulation with realistic noise.
|
||||
*
|
||||
* Unlike the mock (which returns exact values), the simulator adds
|
||||
* configurable noise to a base temperature. This is useful for
|
||||
* system tests that need to verify your code handles real-world
|
||||
* sensor jitter, such as:
|
||||
*
|
||||
* - Averaging/filtering algorithms
|
||||
* - Hysteresis thresholds (so the fan doesn't chatter on/off)
|
||||
* - Out-of-range detection
|
||||
*
|
||||
* Example:
|
||||
* Tmp36Sim sensor(22.0f, 0.5f); // 22 C +/- 0.5 C noise
|
||||
*
|
||||
* // Each read returns a slightly different value
|
||||
* float t1 = sensor.readCelsius(); // maybe 22.3
|
||||
* float t2 = sensor.readCelsius(); // maybe 21.8
|
||||
*
|
||||
* // Simulate outdoor temperature swing
|
||||
* sensor.setBaseTemperature(35.0f);
|
||||
*/
|
||||
class Tmp36Sim : public TempSensor {
|
||||
public:
|
||||
/// Create a simulated sensor.
|
||||
/// base_temp: center temperature in Celsius
|
||||
/// noise: maximum deviation in either direction (Celsius)
|
||||
Tmp36Sim(float base_temp = 22.0f, float noise = 0.5f)
|
||||
: base_temp_(base_temp)
|
||||
, noise_range_(noise)
|
||||
, seed_(42)
|
||||
{}
|
||||
|
||||
/// Change the base temperature (simulates environment change).
|
||||
void setBaseTemperature(float celsius) { base_temp_ = celsius; }
|
||||
|
||||
/// Change the noise amplitude.
|
||||
void setNoise(float range) { noise_range_ = range; }
|
||||
|
||||
/// Seed the random number generator for repeatable tests.
|
||||
void setSeed(unsigned int seed) { seed_ = seed; }
|
||||
|
||||
float readCelsius() override {
|
||||
float noise = noise_range_ * (2.0f * nextRandom() - 1.0f);
|
||||
return base_temp_ + noise;
|
||||
}
|
||||
|
||||
int readRaw() override {
|
||||
float temp_c = readCelsius();
|
||||
float mv = temp_c * 10.0f + 500.0f;
|
||||
return (int)(mv * 1024.0f / 5000.0f);
|
||||
}
|
||||
|
||||
private:
|
||||
float base_temp_;
|
||||
float noise_range_;
|
||||
unsigned int seed_;
|
||||
|
||||
/// Simple LCG random in [0.0, 1.0).
|
||||
/// Deterministic -- same seed gives same sequence.
|
||||
float nextRandom() {
|
||||
seed_ = seed_ * 1103515245 + 12345;
|
||||
return (float)((seed_ >> 16) & 0x7FFF) / 32768.0f;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TMP36_SIM_H
|
||||
523
src/commands/lib.rs
Normal file
523
src/commands/lib.rs
Normal file
@@ -0,0 +1,523 @@
|
||||
use anyhow::{Result, bail};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::library;
|
||||
use crate::project::config::ProjectConfig;
|
||||
|
||||
/// Add a library to the current project.
|
||||
pub fn add_library(name: &str, pin: Option<&str>, project_dir: Option<&str>) -> Result<()> {
|
||||
let meta = library::find_library(name)
|
||||
.ok_or_else(|| {
|
||||
let available: Vec<String> = library::list_available()
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect();
|
||||
anyhow::anyhow!(
|
||||
"Unknown library: '{}'\n Available: {}",
|
||||
name,
|
||||
if available.is_empty() {
|
||||
"(none)".to_string()
|
||||
} else {
|
||||
available.join(", ")
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
let mut config = ProjectConfig::load(&project_root)?;
|
||||
|
||||
// Check if already installed
|
||||
if config.libraries.contains_key(name) {
|
||||
println!(
|
||||
"{} {} is already installed (version {}).",
|
||||
"ok".green(),
|
||||
name.bright_white().bold(),
|
||||
config.libraries[name]
|
||||
);
|
||||
println!(
|
||||
" To reinstall, remove it first: {}",
|
||||
format!("anvil remove {}", name).bright_cyan()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Extract files
|
||||
println!(
|
||||
"Adding {} {}",
|
||||
meta.name.bright_white().bold(),
|
||||
format!("({})", meta.description).bright_black()
|
||||
);
|
||||
|
||||
let written = library::extract_library(name, &project_root)?;
|
||||
for f in &written {
|
||||
println!(" {} {}", "+".bright_green(), f.bright_white());
|
||||
}
|
||||
|
||||
// Add to include_dirs if not present
|
||||
let driver_include = format!("lib/drivers/{}", name);
|
||||
if !config.build.include_dirs.contains(&driver_include) {
|
||||
config.build.include_dirs.push(driver_include);
|
||||
}
|
||||
|
||||
// Track in config
|
||||
config.libraries.insert(name.to_string(), meta.version.clone());
|
||||
config.save(&project_root)?;
|
||||
|
||||
// If --pin was given and this is a simple analog/digital library, assign it now
|
||||
if let Some(pin_str) = pin {
|
||||
if meta.bus == "i2c" || meta.bus == "spi" {
|
||||
println!();
|
||||
println!(
|
||||
" {} --pin is not used for {} libraries (bus pins are fixed).",
|
||||
"note:".bright_yellow(),
|
||||
meta.bus.bright_cyan()
|
||||
);
|
||||
if meta.bus == "i2c" {
|
||||
println!(
|
||||
" Assign the bus: {}",
|
||||
"anvil pin --assign i2c".bright_cyan()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" Assign the bus: {}",
|
||||
"anvil pin --assign spi --cs 10".bright_cyan()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Assign each pin role to the given pin
|
||||
// For single-pin libraries this is straightforward
|
||||
// For multi-pin, the user would need to run multiple commands
|
||||
if meta.pins.len() == 1 {
|
||||
let assign_name = meta.pin_assignment_name(&meta.pins[0]);
|
||||
println!();
|
||||
println!("{}", "Assigning pin:".bright_yellow().bold());
|
||||
// Call pin assignment through the commands module
|
||||
match crate::commands::pin::assign_pin(
|
||||
&assign_name, pin_str,
|
||||
Some(meta.default_mode()),
|
||||
None, project_dir,
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
println!(
|
||||
" {} Pin assignment failed: {}",
|
||||
"!!".red(),
|
||||
e
|
||||
);
|
||||
println!(
|
||||
" You can assign it manually: {}",
|
||||
format!(
|
||||
"anvil pin --assign {} {} --mode {}",
|
||||
assign_name, pin_str, meta.default_mode()
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!();
|
||||
println!(
|
||||
" {} This library needs {} pins. Assign them individually:",
|
||||
"note:".bright_yellow(),
|
||||
meta.pins.len()
|
||||
);
|
||||
for (_role, assign_name) in meta.pin_roles() {
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"anvil pin --assign {} <PIN> --mode {}",
|
||||
assign_name, meta.default_mode()
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print usage help
|
||||
println!();
|
||||
println!("{}", "In your app code:".bright_yellow().bold());
|
||||
println!(
|
||||
" #include \"{}\"",
|
||||
meta.interface.bright_white()
|
||||
);
|
||||
println!(
|
||||
" #include \"{}\"",
|
||||
meta.implementation.bright_white()
|
||||
);
|
||||
match meta.bus.as_str() {
|
||||
"analog" => {
|
||||
let pin_example = pin.unwrap_or("A0");
|
||||
println!(
|
||||
" {} sensor(&hal, {});",
|
||||
impl_class_name(&meta.implementation).bright_cyan(),
|
||||
pin_example
|
||||
);
|
||||
println!(" float temp = sensor.readCelsius();");
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
" {} sensor(&hal);",
|
||||
impl_class_name(&meta.implementation).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "In your tests:".bright_yellow().bold());
|
||||
println!(
|
||||
" #include \"{}\"",
|
||||
meta.mock.bright_white()
|
||||
);
|
||||
println!(
|
||||
" {} sensor;",
|
||||
mock_class_name(&meta.mock).bright_cyan()
|
||||
);
|
||||
println!(" sensor.setTemperature(25.0f);");
|
||||
println!(
|
||||
" // Your app sees 25 C -- no hardware needed"
|
||||
);
|
||||
|
||||
// Next steps
|
||||
println!();
|
||||
if pin.is_none() && meta.bus != "i2c" && meta.bus != "spi" {
|
||||
println!("{}", "Next step:".bright_yellow().bold());
|
||||
print_wiring_guide(&meta, None);
|
||||
} else if pin.is_none() && (meta.bus == "i2c" || meta.bus == "spi") {
|
||||
println!("{}", "Next step:".bright_yellow().bold());
|
||||
print_bus_guide(&meta);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} {} {} added. Run {} to rebuild tests.",
|
||||
"ok".green(),
|
||||
meta.name.bright_white().bold(),
|
||||
meta.version.bright_black(),
|
||||
"./test.sh --clean".bright_cyan()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print wiring guidance for analog/digital libraries.
|
||||
fn print_wiring_guide(meta: &library::LibraryMeta, board: Option<&str>) {
|
||||
let board_flag = board.map(|b| format!(" --board {}", b)).unwrap_or_default();
|
||||
match meta.bus.as_str() {
|
||||
"analog" => {
|
||||
println!(
|
||||
" Wire the {} sensor signal pin to an analog input (e.g. {}).",
|
||||
meta.name.bright_white(),
|
||||
"A0".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" Then tell Anvil which pin you chose:"
|
||||
);
|
||||
println!();
|
||||
for (_role, assign_name) in meta.pin_roles() {
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"anvil pin --assign {} A0 --mode analog{}",
|
||||
assign_name, board_flag
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"Pick any analog pin (A0-A5 on Uno). The pin you choose here"
|
||||
.bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"must match the physical wire AND the pin in your code."
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
"digital" => {
|
||||
println!(
|
||||
" Wire the {} sensor to a digital pin.",
|
||||
meta.name.bright_white()
|
||||
);
|
||||
println!(
|
||||
" Then tell Anvil which pin you chose:"
|
||||
);
|
||||
println!();
|
||||
for (_role, assign_name) in meta.pin_roles() {
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"anvil pin --assign {} <PIN> --mode input{}",
|
||||
assign_name, board_flag
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for (role, assign_name) in meta.pin_roles() {
|
||||
println!(
|
||||
" Assign {} pin: {}",
|
||||
role.bright_white(),
|
||||
format!(
|
||||
"anvil pin --assign {} <PIN>{}",
|
||||
assign_name, board_flag
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Print wiring guidance for I2C/SPI bus libraries.
|
||||
fn print_bus_guide(meta: &library::LibraryMeta) {
|
||||
match meta.bus.as_str() {
|
||||
"i2c" => {
|
||||
println!(
|
||||
" Wire the {} sensor to the I2C bus (SDA + SCL pins).",
|
||||
meta.name.bright_white()
|
||||
);
|
||||
println!(
|
||||
" Then register the bus:"
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"anvil pin --assign i2c".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"SDA and SCL are fixed pins on your board (A4/A5 on Uno)."
|
||||
.bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"Anvil knows which pins they are -- just register the bus."
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
"spi" => {
|
||||
println!(
|
||||
" Wire the {} sensor to the SPI bus (MOSI/MISO/SCK) with a CS pin.",
|
||||
meta.name.bright_white()
|
||||
);
|
||||
println!(
|
||||
" Then register the bus with your chosen CS pin:"
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"anvil pin --assign spi --cs 10".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"MOSI/MISO/SCK are fixed on your board. You pick the CS pin."
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Remove a library from the current project.
|
||||
pub fn remove_library(name: &str, project_dir: Option<&str>) -> Result<()> {
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
let mut config = ProjectConfig::load(&project_root)?;
|
||||
|
||||
if !config.libraries.contains_key(name)
|
||||
&& !library::is_installed_on_disk(name, &project_root)
|
||||
{
|
||||
bail!(
|
||||
"Library '{}' is not installed.\n See installed: anvil lib\n See available: anvil lib --available",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
// Remove files
|
||||
let test_file = project_root.join("test").join(format!("test_{}.cpp", name));
|
||||
let had_test = test_file.exists();
|
||||
library::remove_library_files(name, &project_root)?;
|
||||
println!(
|
||||
" {} lib/drivers/{}/",
|
||||
"-".bright_red(),
|
||||
name.bright_white()
|
||||
);
|
||||
if had_test {
|
||||
println!(
|
||||
" {} test/test_{}.cpp",
|
||||
"-".bright_red(),
|
||||
name.bright_white()
|
||||
);
|
||||
}
|
||||
|
||||
// Remove from include_dirs
|
||||
let driver_include = format!("lib/drivers/{}", name);
|
||||
config.build.include_dirs.retain(|d| d != &driver_include);
|
||||
|
||||
// Remove from tracked libraries
|
||||
config.libraries.remove(name);
|
||||
config.save(&project_root)?;
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{} {} removed.",
|
||||
"ok".green(),
|
||||
name.bright_white().bold()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List installed libraries in the current project.
|
||||
pub fn list_libraries(project_dir: Option<&str>) -> Result<()> {
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
let config = ProjectConfig::load(&project_root)?;
|
||||
|
||||
if config.libraries.is_empty() {
|
||||
println!("No libraries installed.");
|
||||
println!();
|
||||
println!(
|
||||
" Browse available: {}",
|
||||
"anvil lib --available".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" Add one: {}",
|
||||
"anvil add tmp36".bright_cyan()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", "Installed libraries:".bright_yellow().bold());
|
||||
println!();
|
||||
|
||||
for (name, version) in &config.libraries {
|
||||
let on_disk = library::is_installed_on_disk(name, &project_root);
|
||||
let meta = library::find_library(name);
|
||||
|
||||
let desc = meta
|
||||
.as_ref()
|
||||
.map(|m| m.description.as_str())
|
||||
.unwrap_or("(unknown)");
|
||||
|
||||
if on_disk {
|
||||
println!(
|
||||
" {} {} {} {}",
|
||||
"ok".green(),
|
||||
name.bright_white().bold(),
|
||||
version.bright_black(),
|
||||
desc.bright_black()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" {} {} {} {}",
|
||||
"!!".red(),
|
||||
name.bright_white().bold(),
|
||||
version.bright_black(),
|
||||
"files missing -- run: anvil add ".red()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all available libraries in the Anvil registry.
|
||||
pub fn list_available() -> Result<()> {
|
||||
let libs = library::list_available();
|
||||
|
||||
if libs.is_empty() {
|
||||
println!("No libraries available.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", "Available libraries:".bright_yellow().bold());
|
||||
println!();
|
||||
|
||||
for lib in &libs {
|
||||
println!(
|
||||
" {} {} {}",
|
||||
lib.name.bright_white().bold(),
|
||||
lib.version.bright_black(),
|
||||
lib.description
|
||||
);
|
||||
println!(
|
||||
" Needs: {}",
|
||||
lib.wiring_summary().bright_cyan()
|
||||
);
|
||||
// Show inline pin example for simple cases
|
||||
match lib.bus.as_str() {
|
||||
"analog" if lib.pins.len() == 1 => {
|
||||
println!(
|
||||
" Add: {}",
|
||||
format!("anvil add {} --pin A0", lib.name).bright_cyan()
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
" Add: {}",
|
||||
format!("anvil add {}", lib.name).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Derive class name from implementation filename.
|
||||
/// "tmp36_analog.h" -> "Tmp36Analog"
|
||||
fn impl_class_name(filename: &str) -> String {
|
||||
let stem = filename.trim_end_matches(".h");
|
||||
stem.split('_')
|
||||
.map(|word| {
|
||||
let mut chars = word.chars();
|
||||
match chars.next() {
|
||||
Some(c) => {
|
||||
let mut s = c.to_uppercase().to_string();
|
||||
s.push_str(&chars.collect::<String>());
|
||||
s
|
||||
}
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Derive mock class name from mock filename.
|
||||
/// "tmp36_mock.h" -> "Tmp36Mock"
|
||||
fn mock_class_name(filename: &str) -> String {
|
||||
impl_class_name(filename)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_impl_class_name() {
|
||||
assert_eq!(impl_class_name("tmp36_analog.h"), "Tmp36Analog");
|
||||
assert_eq!(impl_class_name("bmp280_i2c.h"), "Bmp280I2c");
|
||||
assert_eq!(impl_class_name("servo.h"), "Servo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mock_class_name() {
|
||||
assert_eq!(mock_class_name("tmp36_mock.h"), "Tmp36Mock");
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,4 @@ pub mod devices;
|
||||
pub mod refresh;
|
||||
pub mod board;
|
||||
pub mod pin;
|
||||
pub mod lib;
|
||||
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
||||
use crate::board::pinmap::{
|
||||
self, BoardPinMap, ALL_CAPABILITIES, ALL_MODES,
|
||||
};
|
||||
use crate::library;
|
||||
use crate::project::config::{
|
||||
ProjectConfig, BoardPinConfig, PinAssignment, BusConfig, CONFIG_FILENAME,
|
||||
};
|
||||
@@ -393,16 +394,22 @@ pub fn audit_pins(
|
||||
if pc.assignments.is_empty() && pc.buses.is_empty() {
|
||||
println!(" {}", "No pin assignments configured.".bright_black());
|
||||
println!();
|
||||
println!(" Get started:");
|
||||
println!(
|
||||
" {}",
|
||||
format!("anvil pin --assign led 13 --board {}", board).bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
format!("anvil pin --assign i2c --board {}", board).bright_black()
|
||||
);
|
||||
println!();
|
||||
|
||||
// Even with no manual assignments, libraries may need pins
|
||||
let has_library_warnings = print_library_pin_warnings(&config, pc);
|
||||
|
||||
if !has_library_warnings {
|
||||
println!(" Get started:");
|
||||
println!(
|
||||
" {}",
|
||||
format!("anvil pin --assign led 13 --board {}", board).bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
format!("anvil pin --assign i2c --board {}", board).bright_black()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -512,6 +519,9 @@ pub fn audit_pins(
|
||||
}
|
||||
}
|
||||
|
||||
// Library pin check
|
||||
print_library_pin_warnings(&config, pc);
|
||||
|
||||
// Wiring checklist
|
||||
println!(" {}", "Wiring Checklist:".bold());
|
||||
let mut all_wiring: Vec<(u8, String, String)> = Vec::new();
|
||||
@@ -554,6 +564,126 @@ pub fn audit_pins(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check installed libraries for unassigned pins and print warnings.
|
||||
/// Returns true if any warnings were printed.
|
||||
fn print_library_pin_warnings(config: &ProjectConfig, pc: &BoardPinConfig) -> bool {
|
||||
if config.libraries.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let assigned_names: Vec<String> = pc.assignments.keys().cloned().collect();
|
||||
let mut missing_any = false;
|
||||
|
||||
for (lib_name, _version) in &config.libraries {
|
||||
if let Some(meta) = library::find_library(lib_name) {
|
||||
// Bus-based libraries (i2c/spi) use bus assignments
|
||||
if meta.bus == "i2c" || meta.bus == "spi" {
|
||||
if !pc.buses.contains_key(&meta.bus) {
|
||||
if !missing_any {
|
||||
println!(" {}", "Library Wiring:".bright_yellow().bold());
|
||||
missing_any = true;
|
||||
}
|
||||
println!(
|
||||
" {} {} needs the {} bus -- not yet assigned",
|
||||
"!!".red().bold(),
|
||||
lib_name.bright_white(),
|
||||
meta.bus.bright_cyan()
|
||||
);
|
||||
match meta.bus.as_str() {
|
||||
"i2c" => {
|
||||
println!(
|
||||
" Wire SDA + SCL, then: {}",
|
||||
"anvil pin --assign i2c".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"(SDA/SCL are fixed pins on your board -- A4/A5 on Uno)"
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
"spi" => {
|
||||
println!(
|
||||
" Wire MOSI/MISO/SCK + CS, then: {}",
|
||||
"anvil pin --assign spi --cs 10".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"(MOSI/MISO/SCK are fixed. You pick the CS pin.)"
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let unassigned = library::unassigned_pins(&meta, &assigned_names);
|
||||
if !unassigned.is_empty() {
|
||||
if !missing_any {
|
||||
println!(" {}", "Library Wiring:".bright_yellow().bold());
|
||||
missing_any = true;
|
||||
}
|
||||
for assign_name in &unassigned {
|
||||
println!(
|
||||
" {} {} needs {} pin \"{}\" -- not yet assigned",
|
||||
"!!".red().bold(),
|
||||
lib_name.bright_white(),
|
||||
meta.bus.bright_cyan(),
|
||||
assign_name.bright_white()
|
||||
);
|
||||
match meta.bus.as_str() {
|
||||
"analog" => {
|
||||
println!(
|
||||
" Wire the sensor to an analog pin (e.g. A0), then:",
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"anvil pin --assign {} A0 --mode analog",
|
||||
assign_name
|
||||
).bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"Any analog pin works (A0-A5 on Uno). Match wire to code."
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
"digital" => {
|
||||
println!(
|
||||
" Wire the sensor to a digital pin, then:",
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"anvil pin --assign {} <PIN> --mode input",
|
||||
assign_name
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
" {}",
|
||||
format!(
|
||||
"anvil pin --assign {} <PIN>",
|
||||
assign_name
|
||||
).bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if missing_any {
|
||||
println!();
|
||||
}
|
||||
|
||||
missing_any
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Init pins from another board
|
||||
// =========================================================================
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod commands;
|
||||
pub mod project;
|
||||
pub mod board;
|
||||
pub mod templates;
|
||||
pub mod library;
|
||||
358
src/library/mod.rs
Normal file
358
src/library/mod.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
use include_dir::{include_dir, Dir};
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use anyhow::{Result, Context};
|
||||
|
||||
static LIBRARY_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/libraries");
|
||||
|
||||
/// Metadata parsed from a library's library.toml.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LibraryMeta {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub bus: String,
|
||||
pub pins: Vec<String>,
|
||||
pub interface: String,
|
||||
pub implementation: String,
|
||||
pub mock: String,
|
||||
pub simulation: Option<String>,
|
||||
pub test: Option<String>,
|
||||
}
|
||||
|
||||
impl LibraryMeta {
|
||||
/// Human-readable wiring requirement (e.g. "1 analog pin (e.g. A0)").
|
||||
pub fn wiring_summary(&self) -> String {
|
||||
match self.bus.as_str() {
|
||||
"analog" => format!(
|
||||
"{} analog pin{} (e.g. A0)",
|
||||
self.pins.len(),
|
||||
if self.pins.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
"i2c" => "I2C bus (SDA + SCL)".to_string(),
|
||||
"spi" => "SPI bus (MOSI + MISO + SCK + CS)".to_string(),
|
||||
"digital" => format!(
|
||||
"{} digital pin{}",
|
||||
self.pins.len(),
|
||||
if self.pins.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
_ => format!("{} pin(s)", self.pins.len()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Default pin mode for assignments derived from bus type.
|
||||
pub fn default_mode(&self) -> &str {
|
||||
match self.bus.as_str() {
|
||||
"analog" => "analog",
|
||||
"digital" => "input",
|
||||
_ => "input",
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the pin assignment name for a given role.
|
||||
/// e.g. tmp36 + "data" -> "tmp36_data"
|
||||
pub fn pin_assignment_name(&self, role: &str) -> String {
|
||||
format!("{}_{}", self.name, role)
|
||||
}
|
||||
|
||||
/// Return all pin roles with their assignment names.
|
||||
/// e.g. [("data", "tmp36_data")]
|
||||
pub fn pin_roles(&self) -> Vec<(&str, String)> {
|
||||
self.pins.iter()
|
||||
.map(|role| (role.as_str(), self.pin_assignment_name(role)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// List all available libraries embedded in Anvil.
|
||||
pub fn list_available() -> Vec<LibraryMeta> {
|
||||
let mut libs = Vec::new();
|
||||
for dir in LIBRARY_DIR.dirs() {
|
||||
if let Some(meta) = parse_library_dir(dir) {
|
||||
libs.push(meta);
|
||||
}
|
||||
}
|
||||
libs.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
libs
|
||||
}
|
||||
|
||||
/// Look up a specific library by name.
|
||||
pub fn find_library(name: &str) -> Option<LibraryMeta> {
|
||||
list_available().into_iter().find(|lib| lib.name == name)
|
||||
}
|
||||
|
||||
/// Extract a library's source files into a project.
|
||||
/// Header files go to lib/drivers/NAME/, test files go to test/.
|
||||
pub fn extract_library(name: &str, project_root: &Path) -> Result<Vec<String>> {
|
||||
let lib_dir = LIBRARY_DIR.get_dir(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Library '{}' not found in registry", name))?;
|
||||
|
||||
let src_dir = lib_dir.get_dir(&format!("{}/src", name))
|
||||
.or_else(|| lib_dir.get_dir("src"))
|
||||
.ok_or_else(|| anyhow::anyhow!("Library '{}' has no src/ directory", name))?;
|
||||
|
||||
let dest_dir = project_root.join("lib").join("drivers").join(name);
|
||||
fs::create_dir_all(&dest_dir)
|
||||
.context(format!("Failed to create {}", dest_dir.display()))?;
|
||||
|
||||
let test_dir = project_root.join("test");
|
||||
// Ensure test dir exists for library test files
|
||||
if src_dir.files().any(|f| {
|
||||
f.path().file_name()
|
||||
.map(|n| {
|
||||
let s = n.to_string_lossy();
|
||||
s.starts_with("test_") && s.ends_with(".cpp")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
fs::create_dir_all(&test_dir)
|
||||
.context("Failed to create test directory")?;
|
||||
}
|
||||
|
||||
let mut written = Vec::new();
|
||||
|
||||
for file in src_dir.files() {
|
||||
let file_name = file.path().file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if file_name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Test files go into test/ directory, everything else into lib/drivers/NAME/
|
||||
if file_name.starts_with("test_") && file_name.ends_with(".cpp") {
|
||||
let dest_path = test_dir.join(&file_name);
|
||||
fs::write(&dest_path, file.contents())
|
||||
.context(format!("Failed to write {}", dest_path.display()))?;
|
||||
written.push(format!("test/{}", file_name));
|
||||
} else {
|
||||
let dest_path = dest_dir.join(&file_name);
|
||||
fs::write(&dest_path, file.contents())
|
||||
.context(format!("Failed to write {}", dest_path.display()))?;
|
||||
written.push(format!("lib/drivers/{}/{}", name, file_name));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
/// Remove a library's files from a project.
|
||||
pub fn remove_library_files(name: &str, project_root: &Path) -> Result<()> {
|
||||
let dir = project_root.join("lib").join("drivers").join(name);
|
||||
if dir.exists() {
|
||||
fs::remove_dir_all(&dir)
|
||||
.context(format!("Failed to remove {}", dir.display()))?;
|
||||
}
|
||||
|
||||
// Remove the library's test file from test/ if present
|
||||
let test_file = project_root.join("test").join(format!("test_{}.cpp", name));
|
||||
if test_file.exists() {
|
||||
fs::remove_file(&test_file)
|
||||
.context(format!("Failed to remove {}", test_file.display()))?;
|
||||
}
|
||||
|
||||
// Clean up empty drivers/ directory
|
||||
let drivers_dir = project_root.join("lib").join("drivers");
|
||||
if drivers_dir.exists() {
|
||||
if fs::read_dir(&drivers_dir)?.next().is_none() {
|
||||
fs::remove_dir(&drivers_dir).ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a library is installed in a project (files exist on disk).
|
||||
pub fn is_installed_on_disk(name: &str, project_root: &Path) -> bool {
|
||||
project_root.join("lib").join("drivers").join(name).is_dir()
|
||||
}
|
||||
|
||||
/// Return library pin roles that are not yet assigned in the config.
|
||||
/// Takes a set of assignment names from the board's pin config.
|
||||
pub fn unassigned_pins(meta: &LibraryMeta, assigned_names: &[String]) -> Vec<String> {
|
||||
meta.pin_roles()
|
||||
.into_iter()
|
||||
.filter(|(_role, assign_name)| !assigned_names.contains(assign_name))
|
||||
.map(|(_role, assign_name)| assign_name)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a library.toml from an embedded directory.
|
||||
fn parse_library_dir(dir: &Dir<'_>) -> Option<LibraryMeta> {
|
||||
// The include_dir structure nests: libraries/tmp36/library.toml
|
||||
// but dir.get_file() uses relative paths from the dir root.
|
||||
let toml_file = dir.files().find(|f| {
|
||||
f.path().file_name()
|
||||
.map(|n| n.to_string_lossy() == "library.toml")
|
||||
.unwrap_or(false)
|
||||
})?;
|
||||
|
||||
let content = std::str::from_utf8(toml_file.contents()).ok()?;
|
||||
let table: toml::Table = content.parse().ok()?;
|
||||
|
||||
let lib = table.get("library")?.as_table()?;
|
||||
let requires = table.get("requires")?.as_table()?;
|
||||
let provides = table.get("provides")?.as_table()?;
|
||||
|
||||
let pins = requires.get("pins")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect();
|
||||
|
||||
Some(LibraryMeta {
|
||||
name: lib.get("name")?.as_str()?.to_string(),
|
||||
version: lib.get("version")?.as_str()?.to_string(),
|
||||
description: lib.get("description")?.as_str()?.to_string(),
|
||||
bus: requires.get("bus")?.as_str()?.to_string(),
|
||||
pins,
|
||||
interface: provides.get("interface")?.as_str()?.to_string(),
|
||||
implementation: provides.get("implementation")?.as_str()?.to_string(),
|
||||
mock: provides.get("mock")?.as_str()?.to_string(),
|
||||
simulation: provides.get("simulation")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
test: provides.get("test")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_list_available_includes_tmp36() {
|
||||
let libs = list_available();
|
||||
assert!(
|
||||
libs.iter().any(|l| l.name == "tmp36"),
|
||||
"Should include tmp36 library"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_library_tmp36() {
|
||||
let meta = find_library("tmp36").expect("tmp36 should exist");
|
||||
assert_eq!(meta.name, "tmp36");
|
||||
assert_eq!(meta.bus, "analog");
|
||||
assert_eq!(meta.pins, vec!["data"]);
|
||||
assert_eq!(meta.interface, "tmp36.h");
|
||||
assert_eq!(meta.implementation, "tmp36_analog.h");
|
||||
assert_eq!(meta.mock, "tmp36_mock.h");
|
||||
assert!(meta.simulation.is_some());
|
||||
assert_eq!(meta.test.as_deref(), Some("test_tmp36.cpp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_library_nonexistent() {
|
||||
assert!(find_library("nonexistent").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_library_creates_files() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let written = extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
assert!(!written.is_empty(), "Should write at least one file");
|
||||
|
||||
// Headers in lib/drivers/tmp36/
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
assert!(driver_dir.exists(), "Driver directory should exist");
|
||||
assert!(driver_dir.join("tmp36.h").exists(), "Interface should exist");
|
||||
assert!(driver_dir.join("tmp36_analog.h").exists(), "Implementation should exist");
|
||||
assert!(driver_dir.join("tmp36_mock.h").exists(), "Mock should exist");
|
||||
assert!(driver_dir.join("tmp36_sim.h").exists(), "Simulation should exist");
|
||||
|
||||
// Test file in test/
|
||||
assert!(
|
||||
tmp.path().join("test/test_tmp36.cpp").exists(),
|
||||
"Test file should be in test/ directory"
|
||||
);
|
||||
assert!(
|
||||
written.iter().any(|w| w == "test/test_tmp36.cpp"),
|
||||
"Written list should include test file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_library_cleans_up() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
assert!(is_installed_on_disk("tmp36", tmp.path()));
|
||||
assert!(tmp.path().join("test/test_tmp36.cpp").exists());
|
||||
remove_library_files("tmp36", tmp.path()).unwrap();
|
||||
assert!(!is_installed_on_disk("tmp36", tmp.path()));
|
||||
assert!(!tmp.path().join("test/test_tmp36.cpp").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_library_files_are_ascii() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
for entry in fs::read_dir(&driver_dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let content = fs::read_to_string(entry.path()).unwrap();
|
||||
for (line_num, line) in content.lines().enumerate() {
|
||||
for (col, ch) in line.chars().enumerate() {
|
||||
assert!(
|
||||
ch.is_ascii(),
|
||||
"Non-ASCII in {} at {}:{}: U+{:04X}",
|
||||
entry.file_name().to_string_lossy(),
|
||||
line_num + 1, col + 1, ch as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wiring_summary_analog() {
|
||||
let meta = find_library("tmp36").unwrap();
|
||||
let summary = meta.wiring_summary();
|
||||
assert!(summary.contains("analog"), "Should mention analog");
|
||||
assert!(summary.contains("A0"), "Should suggest A0 as example");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_assignment_name() {
|
||||
let meta = find_library("tmp36").unwrap();
|
||||
assert_eq!(meta.pin_assignment_name("data"), "tmp36_data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_roles() {
|
||||
let meta = find_library("tmp36").unwrap();
|
||||
let roles = meta.pin_roles();
|
||||
assert_eq!(roles.len(), 1);
|
||||
assert_eq!(roles[0].0, "data");
|
||||
assert_eq!(roles[0].1, "tmp36_data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_mode_analog() {
|
||||
let meta = find_library("tmp36").unwrap();
|
||||
assert_eq!(meta.default_mode(), "analog");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unassigned_pins_all_missing() {
|
||||
let meta = find_library("tmp36").unwrap();
|
||||
let assigned: Vec<String> = vec![];
|
||||
let missing = unassigned_pins(&meta, &assigned);
|
||||
assert_eq!(missing, vec!["tmp36_data"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unassigned_pins_all_assigned() {
|
||||
let meta = find_library("tmp36").unwrap();
|
||||
let assigned = vec!["tmp36_data".to_string()];
|
||||
let missing = unassigned_pins(&meta, &assigned);
|
||||
assert!(missing.is_empty());
|
||||
}
|
||||
}
|
||||
48
src/main.rs
48
src/main.rs
@@ -120,6 +120,41 @@ enum Commands {
|
||||
dir: Option<String>,
|
||||
},
|
||||
|
||||
/// Add a device library to the project
|
||||
Add {
|
||||
/// Library name (e.g. tmp36)
|
||||
name: String,
|
||||
|
||||
/// Assign a pin during add (e.g. --pin A0)
|
||||
#[arg(long, value_name = "PIN")]
|
||||
pin: Option<String>,
|
||||
|
||||
/// Path to project directory (defaults to current directory)
|
||||
#[arg(long, short = 'd', value_name = "DIR")]
|
||||
dir: Option<String>,
|
||||
},
|
||||
|
||||
/// Remove a device library from the project
|
||||
Remove {
|
||||
/// Library name (e.g. tmp36)
|
||||
name: String,
|
||||
|
||||
/// Path to project directory (defaults to current directory)
|
||||
#[arg(long, short = 'd', value_name = "DIR")]
|
||||
dir: Option<String>,
|
||||
},
|
||||
|
||||
/// List installed or available device libraries
|
||||
Lib {
|
||||
/// Show all available libraries (not just installed)
|
||||
#[arg(long)]
|
||||
available: bool,
|
||||
|
||||
/// Path to project directory (defaults to current directory)
|
||||
#[arg(long, short = 'd', value_name = "DIR")]
|
||||
dir: Option<String>,
|
||||
},
|
||||
|
||||
/// View pin maps, assign pins, and audit wiring
|
||||
Pin {
|
||||
/// Capability filter (pwm, analog, spi, i2c, uart, interrupt)
|
||||
@@ -279,6 +314,19 @@ fn main() -> Result<()> {
|
||||
commands::board::list_boards(dir.as_deref())
|
||||
}
|
||||
}
|
||||
Commands::Add { name, pin, dir } => {
|
||||
commands::lib::add_library(&name, pin.as_deref(), dir.as_deref())
|
||||
}
|
||||
Commands::Remove { name, dir } => {
|
||||
commands::lib::remove_library(&name, dir.as_deref())
|
||||
}
|
||||
Commands::Lib { available, dir } => {
|
||||
if available {
|
||||
commands::lib::list_available()
|
||||
} else {
|
||||
commands::lib::list_libraries(dir.as_deref())
|
||||
}
|
||||
}
|
||||
Commands::Pin {
|
||||
name, pin, assign, remove, audit, brief,
|
||||
generate, capabilities, init_from, mode, cs, board, dir,
|
||||
|
||||
@@ -17,6 +17,8 @@ pub struct ProjectConfig {
|
||||
pub boards: HashMap<String, BoardProfile>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub pins: HashMap<String, BoardPinConfig>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub libraries: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -105,6 +107,7 @@ impl ProjectConfig {
|
||||
},
|
||||
boards,
|
||||
pins: HashMap::new(),
|
||||
libraries: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,16 @@ include_directories(
|
||||
${CMAKE_SOURCE_DIR}/mocks
|
||||
)
|
||||
|
||||
# Auto-discover driver libraries (added by: anvil add <driver>)
|
||||
if(IS_DIRECTORY ${LIB_DIR}/drivers)
|
||||
file(GLOB DRIVER_DIRS ${LIB_DIR}/drivers/*)
|
||||
foreach(DRIVER_DIR ${DRIVER_DIRS})
|
||||
if(IS_DIRECTORY ${DRIVER_DIR})
|
||||
include_directories(${DRIVER_DIR})
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Mock Arduino library -- provides Arduino API on x86_64
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -70,3 +80,21 @@ target_link_libraries(test_system
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(test_unit)
|
||||
gtest_discover_tests(test_system)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Driver tests (added automatically by: anvil add <driver>)
|
||||
# --------------------------------------------------------------------------
|
||||
file(GLOB DRIVER_TEST_SOURCES ${CMAKE_SOURCE_DIR}/test_*.cpp)
|
||||
# Exclude test_unit.cpp and test_system.cpp (already handled above)
|
||||
list(FILTER DRIVER_TEST_SOURCES EXCLUDE REGEX "test_unit\\.cpp$")
|
||||
list(FILTER DRIVER_TEST_SOURCES EXCLUDE REGEX "test_system\\.cpp$")
|
||||
|
||||
foreach(TEST_SOURCE ${DRIVER_TEST_SOURCES})
|
||||
get_filename_component(TEST_NAME ${TEST_SOURCE} NAME_WE)
|
||||
add_executable(${TEST_NAME} ${TEST_SOURCE})
|
||||
target_link_libraries(${TEST_NAME}
|
||||
GTest::gtest_main
|
||||
mock_arduino
|
||||
)
|
||||
gtest_discover_tests(${TEST_NAME})
|
||||
endforeach()
|
||||
|
||||
@@ -6,6 +6,8 @@ use anvil::templates::{TemplateManager, TemplateContext};
|
||||
use anvil::project::config::{
|
||||
ProjectConfig, BoardProfile, CONFIG_FILENAME, set_default_in_file,
|
||||
};
|
||||
use anvil::library;
|
||||
use anvil::commands;
|
||||
|
||||
// ============================================================================
|
||||
// Template extraction tests
|
||||
@@ -3309,14 +3311,14 @@ fn test_mock_arduino_header_has_core_api() {
|
||||
assert!(header.contains("#define LED_BUILTIN"), "Should define LED_BUILTIN");
|
||||
assert!(header.contains("#define A0"), "Should define A0");
|
||||
|
||||
// Core Arduino functions
|
||||
assert!(header.contains("void pinMode("), "Should declare pinMode");
|
||||
assert!(header.contains("void digitalWrite("), "Should declare digitalWrite");
|
||||
assert!(header.contains("int digitalRead("), "Should declare digitalRead");
|
||||
assert!(header.contains("int analogRead("), "Should declare analogRead");
|
||||
assert!(header.contains("void analogWrite("), "Should declare analogWrite");
|
||||
assert!(header.contains("unsigned long millis()"), "Should declare millis");
|
||||
assert!(header.contains("void delay("), "Should declare delay");
|
||||
// Core Arduino functions (declarations use aligned whitespace)
|
||||
assert!(header.contains("pinMode("), "Should declare pinMode");
|
||||
assert!(header.contains("digitalWrite("), "Should declare digitalWrite");
|
||||
assert!(header.contains("digitalRead("), "Should declare digitalRead");
|
||||
assert!(header.contains("analogRead("), "Should declare analogRead");
|
||||
assert!(header.contains("analogWrite("), "Should declare analogWrite");
|
||||
assert!(header.contains("millis()"), "Should declare millis");
|
||||
assert!(header.contains("delay("), "Should declare delay");
|
||||
|
||||
// Serial class
|
||||
assert!(header.contains("class MockSerial"), "Should declare MockSerial");
|
||||
@@ -3484,3 +3486,612 @@ fn test_root_test_scripts_exist_and_reference_test_dir() {
|
||||
assert!(test_bat.contains("--unit"), "test.bat should support --unit flag");
|
||||
assert!(test_bat.contains("--system"), "test.bat should support --system flag");
|
||||
}
|
||||
// ==========================================================================
|
||||
// Device Library: anvil add / remove / lib
|
||||
// ==========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_library_registry_lists_tmp36() {
|
||||
let libs = library::list_available();
|
||||
assert!(!libs.is_empty(), "Should have at least one library");
|
||||
|
||||
let tmp36 = libs.iter().find(|l| l.name == "tmp36");
|
||||
assert!(tmp36.is_some(), "TMP36 should be in the registry");
|
||||
|
||||
let meta = tmp36.unwrap();
|
||||
assert_eq!(meta.bus, "analog");
|
||||
assert_eq!(meta.pins, vec!["data"]);
|
||||
assert_eq!(meta.interface, "tmp36.h");
|
||||
assert_eq!(meta.mock, "tmp36_mock.h");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_find_by_name() {
|
||||
assert!(library::find_library("tmp36").is_some());
|
||||
assert!(library::find_library("nonexistent_sensor").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_extract_creates_driver_directory() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
let written = library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
assert!(!written.is_empty(), "Should write files");
|
||||
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
assert!(driver_dir.exists(), "Driver directory should be created");
|
||||
|
||||
// All four files should exist
|
||||
assert!(driver_dir.join("tmp36.h").exists(), "Interface header");
|
||||
assert!(driver_dir.join("tmp36_analog.h").exists(), "Implementation");
|
||||
assert!(driver_dir.join("tmp36_mock.h").exists(), "Mock");
|
||||
assert!(driver_dir.join("tmp36_sim.h").exists(), "Simulation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_extract_files_content_is_valid() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
|
||||
// Interface should define TempSensor class
|
||||
let interface = fs::read_to_string(driver_dir.join("tmp36.h")).unwrap();
|
||||
assert!(interface.contains("class TempSensor"), "Should define TempSensor");
|
||||
assert!(interface.contains("readCelsius"), "Should declare readCelsius");
|
||||
assert!(interface.contains("readFahrenheit"), "Should declare readFahrenheit");
|
||||
assert!(interface.contains("readRaw"), "Should declare readRaw");
|
||||
|
||||
// Implementation should include hal.h
|
||||
let impl_h = fs::read_to_string(driver_dir.join("tmp36_analog.h")).unwrap();
|
||||
assert!(impl_h.contains("hal.h"), "Implementation should use HAL");
|
||||
assert!(impl_h.contains("class Tmp36Analog"), "Should define Tmp36Analog");
|
||||
assert!(impl_h.contains("analogRead"), "Should use analogRead");
|
||||
|
||||
// Mock should have setTemperature
|
||||
let mock_h = fs::read_to_string(driver_dir.join("tmp36_mock.h")).unwrap();
|
||||
assert!(mock_h.contains("class Tmp36Mock"), "Should define Tmp36Mock");
|
||||
assert!(mock_h.contains("setTemperature"), "Mock should have setTemperature");
|
||||
|
||||
// Sim should have noise
|
||||
let sim_h = fs::read_to_string(driver_dir.join("tmp36_sim.h")).unwrap();
|
||||
assert!(sim_h.contains("class Tmp36Sim"), "Should define Tmp36Sim");
|
||||
assert!(sim_h.contains("setNoise"), "Sim should have setNoise");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_remove_cleans_up() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
assert!(library::is_installed_on_disk("tmp36", tmp.path()));
|
||||
|
||||
library::remove_library_files("tmp36", tmp.path()).unwrap();
|
||||
|
||||
assert!(!library::is_installed_on_disk("tmp36", tmp.path()));
|
||||
// drivers/ dir should also be cleaned up if empty
|
||||
assert!(!tmp.path().join("lib/drivers").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_remove_preserves_other_drivers() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
// Fake a second driver
|
||||
let other_dir = tmp.path().join("lib/drivers/bmp280");
|
||||
fs::create_dir_all(&other_dir).unwrap();
|
||||
fs::write(other_dir.join("bmp280.h"), "// placeholder").unwrap();
|
||||
|
||||
library::remove_library_files("tmp36", tmp.path()).unwrap();
|
||||
|
||||
// tmp36 gone, bmp280 still there
|
||||
assert!(!library::is_installed_on_disk("tmp36", tmp.path()));
|
||||
assert!(other_dir.exists(), "Other driver should survive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_files_are_ascii_only() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
for entry in fs::read_dir(&driver_dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let content = fs::read_to_string(entry.path()).unwrap();
|
||||
for (line_num, line) in content.lines().enumerate() {
|
||||
for (col, ch) in line.chars().enumerate() {
|
||||
assert!(
|
||||
ch.is_ascii(),
|
||||
"Non-ASCII in {} at {}:{}: U+{:04X}",
|
||||
entry.file_name().to_string_lossy(),
|
||||
line_num + 1, col + 1, ch as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_libraries_field_roundtrips() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut config = ProjectConfig::new("lib_test");
|
||||
config.libraries.insert("tmp36".to_string(), "0.1.0".to_string());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(loaded.libraries.len(), 1);
|
||||
assert_eq!(loaded.libraries["tmp36"], "0.1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_empty_libraries_not_serialized() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = ProjectConfig::new("no_libs");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap();
|
||||
assert!(
|
||||
!content.contains("[libraries]"),
|
||||
"Empty libraries should not appear in TOML"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_libraries_serialized_when_present() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let mut config = ProjectConfig::new("has_libs");
|
||||
config.libraries.insert("tmp36".to_string(), "0.1.0".to_string());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let content = fs::read_to_string(tmp.path().join(".anvil.toml")).unwrap();
|
||||
assert!(
|
||||
content.contains("[libraries]"),
|
||||
"Non-empty libraries should appear in TOML"
|
||||
);
|
||||
assert!(content.contains("tmp36"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmake_autodiscovers_driver_directories() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "cmake_drv".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
|
||||
let cmake = fs::read_to_string(tmp.path().join("test/CMakeLists.txt")).unwrap();
|
||||
|
||||
assert!(
|
||||
cmake.contains("drivers"),
|
||||
"CMakeLists should reference drivers directory"
|
||||
);
|
||||
assert!(
|
||||
cmake.contains("GLOB DRIVER_DIRS"),
|
||||
"CMakeLists should glob driver directories"
|
||||
);
|
||||
assert!(
|
||||
cmake.contains("include_directories(${DRIVER_DIR})"),
|
||||
"CMakeLists should add each driver to include path"
|
||||
);
|
||||
|
||||
// Driver test auto-discovery
|
||||
assert!(
|
||||
cmake.contains("GLOB DRIVER_TEST_SOURCES"),
|
||||
"CMakeLists should glob driver test files"
|
||||
);
|
||||
assert!(
|
||||
cmake.contains("gtest_discover_tests(${TEST_NAME})"),
|
||||
"CMakeLists should register driver tests with CTest"
|
||||
);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Device Library: end-to-end command-level tests
|
||||
// ==========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_add_library_full_flow() {
|
||||
// Simulates: anvil new mocktest && anvil add tmp36
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "e2e_add".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
ProjectConfig::new("e2e_add").save(tmp.path()).unwrap();
|
||||
|
||||
// Pre-check: no libraries, no drivers dir
|
||||
let config_before = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert!(config_before.libraries.is_empty());
|
||||
assert!(!tmp.path().join("lib/drivers/tmp36").exists());
|
||||
|
||||
// Extract library and update config (mirrors what add_library does)
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let written = library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
assert_eq!(written.len(), 5, "Should write 5 files (4 headers + 1 test)");
|
||||
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
let driver_include = format!("lib/drivers/{}", meta.name);
|
||||
if !config.build.include_dirs.contains(&driver_include) {
|
||||
config.build.include_dirs.push(driver_include.clone());
|
||||
}
|
||||
config.libraries.insert(meta.name.clone(), meta.version.clone());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Post-check: files exist, config updated
|
||||
assert!(tmp.path().join("lib/drivers/tmp36/tmp36.h").exists());
|
||||
assert!(tmp.path().join("lib/drivers/tmp36/tmp36_analog.h").exists());
|
||||
assert!(tmp.path().join("lib/drivers/tmp36/tmp36_mock.h").exists());
|
||||
assert!(tmp.path().join("lib/drivers/tmp36/tmp36_sim.h").exists());
|
||||
assert!(tmp.path().join("test/test_tmp36.cpp").exists(), "Test file in test/");
|
||||
|
||||
let config_after = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config_after.libraries["tmp36"], "0.1.0");
|
||||
assert!(
|
||||
config_after.build.include_dirs.contains(&driver_include),
|
||||
"include_dirs should contain driver path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_library_full_flow() {
|
||||
// Simulates: anvil add tmp36 && anvil remove tmp36
|
||||
let tmp = TempDir::new().unwrap();
|
||||
ProjectConfig::new("e2e_rm").save(tmp.path()).unwrap();
|
||||
|
||||
// Add
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
let driver_include = format!("lib/drivers/{}", meta.name);
|
||||
config.build.include_dirs.push(driver_include.clone());
|
||||
config.libraries.insert(meta.name.clone(), meta.version.clone());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Remove
|
||||
library::remove_library_files("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.build.include_dirs.retain(|d| d != &driver_include);
|
||||
config.libraries.remove("tmp36");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Post-check: files gone, config clean
|
||||
assert!(!tmp.path().join("lib/drivers/tmp36").exists());
|
||||
assert!(!tmp.path().join("test/test_tmp36.cpp").exists(), "Test file should be removed");
|
||||
let config_final = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert!(config_final.libraries.is_empty());
|
||||
assert!(
|
||||
!config_final.build.include_dirs.contains(&driver_include),
|
||||
"include_dirs should not contain driver path after remove"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_remove_readd_idempotent() {
|
||||
// Simulates: anvil add tmp36 && anvil remove tmp36 && anvil add tmp36
|
||||
let tmp = TempDir::new().unwrap();
|
||||
ProjectConfig::new("e2e_idem").save(tmp.path()).unwrap();
|
||||
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let driver_include = format!("lib/drivers/{}", meta.name);
|
||||
|
||||
// Add
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.build.include_dirs.push(driver_include.clone());
|
||||
config.libraries.insert(meta.name.clone(), meta.version.clone());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Remove
|
||||
library::remove_library_files("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.build.include_dirs.retain(|d| d != &driver_include);
|
||||
config.libraries.remove("tmp36");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
assert!(!tmp.path().join("lib/drivers/tmp36").exists());
|
||||
|
||||
// Re-add
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.build.include_dirs.push(driver_include.clone());
|
||||
config.libraries.insert(meta.name.clone(), meta.version.clone());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Everything back to normal
|
||||
assert!(tmp.path().join("lib/drivers/tmp36/tmp36.h").exists());
|
||||
let config_final = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config_final.libraries["tmp36"], "0.1.0");
|
||||
assert!(config_final.build.include_dirs.contains(&driver_include));
|
||||
// No duplicate include_dirs
|
||||
let count = config_final.build.include_dirs.iter()
|
||||
.filter(|d| *d == &driver_include)
|
||||
.count();
|
||||
assert_eq!(count, 1, "Should not duplicate include dir on re-add");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_interface_compiles_against_hal() {
|
||||
// Verify the actual C++ content is structurally correct:
|
||||
// tmp36_analog.h includes hal.h, tmp36_mock.h and tmp36_sim.h are standalone
|
||||
let tmp = TempDir::new().unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
|
||||
let analog = fs::read_to_string(driver_dir.join("tmp36_analog.h")).unwrap();
|
||||
assert!(analog.contains("#include \"hal.h\""), "Analog impl must include hal.h");
|
||||
assert!(analog.contains("#include \"tmp36.h\""), "Analog impl must include interface");
|
||||
assert!(analog.contains("Hal*"), "Analog impl must accept Hal pointer");
|
||||
|
||||
let mock = fs::read_to_string(driver_dir.join("tmp36_mock.h")).unwrap();
|
||||
assert!(!mock.contains("hal.h"), "Mock should NOT depend on hal.h");
|
||||
assert!(mock.contains("#include \"tmp36.h\""), "Mock must include interface");
|
||||
|
||||
let sim = fs::read_to_string(driver_dir.join("tmp36_sim.h")).unwrap();
|
||||
assert!(!sim.contains("hal.h"), "Sim should NOT depend on hal.h");
|
||||
assert!(sim.contains("#include \"tmp36.h\""), "Sim must include interface");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_polymorphism_contract() {
|
||||
// All implementations must inherit from TempSensor
|
||||
let tmp = TempDir::new().unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
let driver_dir = tmp.path().join("lib/drivers/tmp36");
|
||||
|
||||
let interface = fs::read_to_string(driver_dir.join("tmp36.h")).unwrap();
|
||||
assert!(interface.contains("class TempSensor"));
|
||||
assert!(interface.contains("virtual float readCelsius()"));
|
||||
assert!(interface.contains("virtual int readRaw()"));
|
||||
|
||||
// Each impl must extend TempSensor
|
||||
for (file, class) in [
|
||||
("tmp36_analog.h", "Tmp36Analog"),
|
||||
("tmp36_mock.h", "Tmp36Mock"),
|
||||
("tmp36_sim.h", "Tmp36Sim"),
|
||||
] {
|
||||
let content = fs::read_to_string(driver_dir.join(file)).unwrap();
|
||||
assert!(
|
||||
content.contains(&format!("class {} : public TempSensor", class)),
|
||||
"{} should extend TempSensor",
|
||||
file
|
||||
);
|
||||
assert!(
|
||||
content.contains("readCelsius() override"),
|
||||
"{} should override readCelsius",
|
||||
file
|
||||
);
|
||||
assert!(
|
||||
content.contains("readRaw() override"),
|
||||
"{} should override readRaw",
|
||||
file
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Device Library: UX helpers and pin integration
|
||||
// ==========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_library_meta_wiring_summary() {
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let summary = meta.wiring_summary();
|
||||
assert!(summary.contains("analog"), "Should mention analog bus");
|
||||
assert!(summary.contains("A0"), "Should give A0 as example pin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_meta_pin_roles() {
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let roles = meta.pin_roles();
|
||||
assert_eq!(roles.len(), 1);
|
||||
assert_eq!(roles[0].0, "data");
|
||||
assert_eq!(roles[0].1, "tmp36_data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_meta_default_mode() {
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
assert_eq!(meta.default_mode(), "analog");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_unassigned_pins_detects_missing() {
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let assigned: Vec<String> = vec![];
|
||||
let missing = library::unassigned_pins(&meta, &assigned);
|
||||
assert_eq!(missing, vec!["tmp36_data"], "Should flag tmp36_data as unassigned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_library_unassigned_pins_detects_assigned() {
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let assigned = vec!["tmp36_data".to_string()];
|
||||
let missing = library::unassigned_pins(&meta, &assigned);
|
||||
assert!(missing.is_empty(), "Should detect tmp36_data as assigned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_with_pin_creates_assignment() {
|
||||
// Simulates: anvil new test_proj && anvil add tmp36 --pin A0
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "pin_lib".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
|
||||
// Extract library files
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
|
||||
// Update config like add_library does
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
let driver_include = format!("lib/drivers/{}", meta.name);
|
||||
if !config.build.include_dirs.contains(&driver_include) {
|
||||
config.build.include_dirs.push(driver_include);
|
||||
}
|
||||
config.libraries.insert(meta.name.clone(), meta.version.clone());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Simulate --pin A0 by calling assign_pin
|
||||
let assign_name = meta.pin_assignment_name(&meta.pins[0]);
|
||||
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||
commands::pin::assign_pin(
|
||||
&assign_name, "A0",
|
||||
Some(meta.default_mode()),
|
||||
None,
|
||||
Some(&dir_str),
|
||||
).unwrap();
|
||||
|
||||
// Verify the assignment exists
|
||||
let config_after = ProjectConfig::load(tmp.path()).unwrap();
|
||||
let board_pins = config_after.pins.get("uno").unwrap();
|
||||
assert!(
|
||||
board_pins.assignments.contains_key("tmp36_data"),
|
||||
"Should have tmp36_data pin assignment"
|
||||
);
|
||||
let assignment = &board_pins.assignments["tmp36_data"];
|
||||
assert_eq!(assignment.mode, "analog");
|
||||
|
||||
// Verify unassigned_pins now returns empty
|
||||
let assigned: Vec<String> = board_pins.assignments.keys().cloned().collect();
|
||||
let missing = library::unassigned_pins(&meta, &assigned);
|
||||
assert!(missing.is_empty(), "All library pins should be assigned after --pin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_with_library_missing_pin() {
|
||||
// anvil add tmp36 without --pin should leave pin unassigned
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "audit_lib".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
|
||||
// Add library to config but no pin assignment
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.libraries.insert("tmp36".to_string(), "0.1.0".to_string());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Check that unassigned_pins detects it
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let board_pins = config.pins.get("uno");
|
||||
let assigned: Vec<String> = board_pins
|
||||
.map(|bp| bp.assignments.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
let missing = library::unassigned_pins(&meta, &assigned);
|
||||
assert_eq!(missing, vec!["tmp36_data"]);
|
||||
|
||||
// The actual audit command must not crash (this was a bug:
|
||||
// audit used to early-return when no pins assigned, skipping library check)
|
||||
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||
commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_with_library_pin_assigned() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "audit_ok".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
|
||||
// Add library + pin assignment
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.libraries.insert("tmp36".to_string(), "0.1.0".to_string());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||
commands::pin::assign_pin("tmp36_data", "A0", Some("analog"), None, Some(&dir_str)).unwrap();
|
||||
|
||||
// Now check data model
|
||||
let config_after = ProjectConfig::load(tmp.path()).unwrap();
|
||||
let board_pins = config_after.pins.get("uno").unwrap();
|
||||
let assigned: Vec<String> = board_pins.assignments.keys().cloned().collect();
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
let missing = library::unassigned_pins(&meta, &assigned);
|
||||
assert!(missing.is_empty(), "Pin should be satisfied after assignment");
|
||||
|
||||
// And the audit command itself should work with the pin assigned
|
||||
commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_no_pins_no_libraries_does_not_crash() {
|
||||
// Baseline: no pins, no libraries -- audit should still work
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "audit_empty".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
|
||||
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||
commands::pin::audit_pins(None, false, Some(&dir_str)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_remove_pin_assignment_survives() {
|
||||
// When we remove a library, the pin assignment should still exist
|
||||
// (the user might want to reassign it to a different library)
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let ctx = TemplateContext {
|
||||
project_name: "pin_survive".to_string(),
|
||||
anvil_version: "1.0.0".to_string(),
|
||||
board_name: "uno".to_string(),
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
baud: 115200,
|
||||
};
|
||||
TemplateManager::extract("basic", tmp.path(), &ctx).unwrap();
|
||||
|
||||
// Add library and assign pin
|
||||
let meta = library::find_library("tmp36").unwrap();
|
||||
library::extract_library("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.libraries.insert("tmp36".to_string(), "0.1.0".to_string());
|
||||
let driver_include = format!("lib/drivers/{}", meta.name);
|
||||
config.build.include_dirs.push(driver_include.clone());
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let dir_str = tmp.path().to_string_lossy().to_string();
|
||||
commands::pin::assign_pin("tmp36_data", "A0", Some("analog"), None, Some(&dir_str)).unwrap();
|
||||
|
||||
// Remove library
|
||||
library::remove_library_files("tmp36", tmp.path()).unwrap();
|
||||
let mut config = ProjectConfig::load(tmp.path()).unwrap();
|
||||
config.build.include_dirs.retain(|d| d != &driver_include);
|
||||
config.libraries.remove("tmp36");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Pin assignment should still be there
|
||||
let config_final = ProjectConfig::load(tmp.path()).unwrap();
|
||||
let board_pins = config_final.pins.get("uno").unwrap();
|
||||
assert!(
|
||||
board_pins.assignments.contains_key("tmp36_data"),
|
||||
"Pin assignment should survive library removal"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user