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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@ pub mod setup;
|
|||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod refresh;
|
pub mod refresh;
|
||||||
pub mod board;
|
pub mod board;
|
||||||
pub mod pin;
|
pub mod pin;
|
||||||
|
pub mod lib;
|
||||||
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
|||||||
use crate::board::pinmap::{
|
use crate::board::pinmap::{
|
||||||
self, BoardPinMap, ALL_CAPABILITIES, ALL_MODES,
|
self, BoardPinMap, ALL_CAPABILITIES, ALL_MODES,
|
||||||
};
|
};
|
||||||
|
use crate::library;
|
||||||
use crate::project::config::{
|
use crate::project::config::{
|
||||||
ProjectConfig, BoardPinConfig, PinAssignment, BusConfig, CONFIG_FILENAME,
|
ProjectConfig, BoardPinConfig, PinAssignment, BusConfig, CONFIG_FILENAME,
|
||||||
};
|
};
|
||||||
@@ -393,16 +394,22 @@ pub fn audit_pins(
|
|||||||
if pc.assignments.is_empty() && pc.buses.is_empty() {
|
if pc.assignments.is_empty() && pc.buses.is_empty() {
|
||||||
println!(" {}", "No pin assignments configured.".bright_black());
|
println!(" {}", "No pin assignments configured.".bright_black());
|
||||||
println!();
|
println!();
|
||||||
println!(" Get started:");
|
|
||||||
println!(
|
// Even with no manual assignments, libraries may need pins
|
||||||
" {}",
|
let has_library_warnings = print_library_pin_warnings(&config, pc);
|
||||||
format!("anvil pin --assign led 13 --board {}", board).bright_black()
|
|
||||||
);
|
if !has_library_warnings {
|
||||||
println!(
|
println!(" Get started:");
|
||||||
" {}",
|
println!(
|
||||||
format!("anvil pin --assign i2c --board {}", board).bright_black()
|
" {}",
|
||||||
);
|
format!("anvil pin --assign led 13 --board {}", board).bright_black()
|
||||||
println!();
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
format!("anvil pin --assign i2c --board {}", board).bright_black()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,6 +519,9 @@ pub fn audit_pins(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Library pin check
|
||||||
|
print_library_pin_warnings(&config, pc);
|
||||||
|
|
||||||
// Wiring checklist
|
// Wiring checklist
|
||||||
println!(" {}", "Wiring Checklist:".bold());
|
println!(" {}", "Wiring Checklist:".bold());
|
||||||
let mut all_wiring: Vec<(u8, String, String)> = Vec::new();
|
let mut all_wiring: Vec<(u8, String, String)> = Vec::new();
|
||||||
@@ -554,6 +564,126 @@ pub fn audit_pins(
|
|||||||
Ok(())
|
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
|
// Init pins from another board
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ pub mod commands;
|
|||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod board;
|
pub mod board;
|
||||||
pub mod templates;
|
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>,
|
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
|
/// View pin maps, assign pins, and audit wiring
|
||||||
Pin {
|
Pin {
|
||||||
/// Capability filter (pwm, analog, spi, i2c, uart, interrupt)
|
/// Capability filter (pwm, analog, spi, i2c, uart, interrupt)
|
||||||
@@ -279,6 +314,19 @@ fn main() -> Result<()> {
|
|||||||
commands::board::list_boards(dir.as_deref())
|
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 {
|
Commands::Pin {
|
||||||
name, pin, assign, remove, audit, brief,
|
name, pin, assign, remove, audit, brief,
|
||||||
generate, capabilities, init_from, mode, cs, board, dir,
|
generate, capabilities, init_from, mode, cs, board, dir,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ pub struct ProjectConfig {
|
|||||||
pub boards: HashMap<String, BoardProfile>,
|
pub boards: HashMap<String, BoardProfile>,
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
pub pins: HashMap<String, BoardPinConfig>,
|
pub pins: HashMap<String, BoardPinConfig>,
|
||||||
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
|
pub libraries: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
@@ -105,6 +107,7 @@ impl ProjectConfig {
|
|||||||
},
|
},
|
||||||
boards,
|
boards,
|
||||||
pins: HashMap::new(),
|
pins: HashMap::new(),
|
||||||
|
libraries: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ include_directories(
|
|||||||
${CMAKE_SOURCE_DIR}/mocks
|
${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
|
# Mock Arduino library -- provides Arduino API on x86_64
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -70,3 +80,21 @@ target_link_libraries(test_system
|
|||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(test_unit)
|
gtest_discover_tests(test_unit)
|
||||||
gtest_discover_tests(test_system)
|
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::{
|
use anvil::project::config::{
|
||||||
ProjectConfig, BoardProfile, CONFIG_FILENAME, set_default_in_file,
|
ProjectConfig, BoardProfile, CONFIG_FILENAME, set_default_in_file,
|
||||||
};
|
};
|
||||||
|
use anvil::library;
|
||||||
|
use anvil::commands;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Template extraction tests
|
// 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 LED_BUILTIN"), "Should define LED_BUILTIN");
|
||||||
assert!(header.contains("#define A0"), "Should define A0");
|
assert!(header.contains("#define A0"), "Should define A0");
|
||||||
|
|
||||||
// Core Arduino functions
|
// Core Arduino functions (declarations use aligned whitespace)
|
||||||
assert!(header.contains("void pinMode("), "Should declare pinMode");
|
assert!(header.contains("pinMode("), "Should declare pinMode");
|
||||||
assert!(header.contains("void digitalWrite("), "Should declare digitalWrite");
|
assert!(header.contains("digitalWrite("), "Should declare digitalWrite");
|
||||||
assert!(header.contains("int digitalRead("), "Should declare digitalRead");
|
assert!(header.contains("digitalRead("), "Should declare digitalRead");
|
||||||
assert!(header.contains("int analogRead("), "Should declare analogRead");
|
assert!(header.contains("analogRead("), "Should declare analogRead");
|
||||||
assert!(header.contains("void analogWrite("), "Should declare analogWrite");
|
assert!(header.contains("analogWrite("), "Should declare analogWrite");
|
||||||
assert!(header.contains("unsigned long millis()"), "Should declare millis");
|
assert!(header.contains("millis()"), "Should declare millis");
|
||||||
assert!(header.contains("void delay("), "Should declare delay");
|
assert!(header.contains("delay("), "Should declare delay");
|
||||||
|
|
||||||
// Serial class
|
// Serial class
|
||||||
assert!(header.contains("class MockSerial"), "Should declare MockSerial");
|
assert!(header.contains("class MockSerial"), "Should declare MockSerial");
|
||||||
@@ -3483,4 +3485,613 @@ fn test_root_test_scripts_exist_and_reference_test_dir() {
|
|||||||
assert!(test_bat.contains("ctest"), "test.bat should invoke ctest");
|
assert!(test_bat.contains("ctest"), "test.bat should invoke ctest");
|
||||||
assert!(test_bat.contains("--unit"), "test.bat should support --unit flag");
|
assert!(test_bat.contains("--unit"), "test.bat should support --unit flag");
|
||||||
assert!(test_bat.contains("--system"), "test.bat should support --system 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