Arduino CLI build system with HAL-based test architecture

Build and upload tool (arduino-build.sh):
- Compile, upload, and monitor via arduino-cli
- Device discovery with USB ID identification (--devices)
- Persistent reconnecting serial monitor (--watch)
- Split compile/upload workflow (--verify, --upload-only)
- First-time setup wizard (--setup)
- Comprehensive --help with troubleshooting and RedBoard specs

Testable application architecture:
- Hardware abstraction layer (lib/hal/) decouples logic from Arduino API
- Google Mock HAL for unit tests (exact call verification)
- Simulated HAL for system tests (GPIO state, virtual clock, I2C devices)
- Example I2C temperature sensor simulator (TMP102)
- Host-side test suite via CMake + Google Test (21 tests)

Example sketch:
- blink/ -- LED blink with button-controlled speed, wired through HAL
This commit is contained in:
Eric Ratliff
2026-02-14 09:25:49 -06:00
committed by Eric Ratliff
commit 991b9a8ee9
19 changed files with 2438 additions and 0 deletions

201
test/test_blink_system.cpp Normal file
View File

@@ -0,0 +1,201 @@
#include <gtest/gtest.h>
#include "hal.h"
#include "sim_hal.h"
#include "blink_app.h"
// ============================================================================
// System Tests -- exercise full app behavior against simulated hardware
// ============================================================================
class BlinkAppSystemTest : public ::testing::Test {
protected:
SimHal sim_;
};
// -----------------------------------------------------------------------
// Basic blink behavior
// -----------------------------------------------------------------------
TEST_F(BlinkAppSystemTest, LedBlinksAtSlowRate) {
BlinkApp app(&sim_);
app.begin();
// Run for 2 seconds, calling update every 10ms (like a real loop)
for (int i = 0; i < 200; ++i) {
sim_.advanceMillis(10);
app.update();
}
// At 500ms intervals over 2000ms, we expect ~4 toggles
// (0->500: toggle, 500->1000: toggle, 1000->1500: toggle, 1500->2000: toggle)
int highs = sim_.countWrites(LED_BUILTIN, HIGH);
int lows = sim_.countWrites(LED_BUILTIN, LOW);
EXPECT_GE(highs, 2);
EXPECT_GE(lows, 2);
EXPECT_LE(highs + lows, 6); // should not over-toggle
}
TEST_F(BlinkAppSystemTest, LedBlinksAtFastRateAfterButtonPress) {
BlinkApp app(&sim_, LED_BUILTIN, 2);
app.begin();
// Button starts HIGH (INPUT_PULLUP)
EXPECT_EQ(sim_.getPin(2), HIGH);
// Run 1 second in slow mode
for (int i = 0; i < 100; ++i) {
sim_.advanceMillis(10);
app.update();
}
int slow_toggles = sim_.gpioLog().size();
// Press button
sim_.setPin(2, LOW);
sim_.advanceMillis(10);
app.update();
EXPECT_TRUE(app.fastMode());
// Release button
sim_.setPin(2, HIGH);
sim_.advanceMillis(10);
app.update();
// Clear log and run another second in fast mode
sim_.clearGpioLog();
for (int i = 0; i < 100; ++i) {
sim_.advanceMillis(10);
app.update();
}
int fast_toggles = sim_.gpioLog().size();
// Fast mode (125ms) should produce roughly 4x more toggles than slow (500ms)
EXPECT_GT(fast_toggles, slow_toggles * 2);
}
// -----------------------------------------------------------------------
// Button edge detection
// -----------------------------------------------------------------------
TEST_F(BlinkAppSystemTest, ButtonDebounceOnlyTriggersOnFallingEdge) {
BlinkApp app(&sim_, LED_BUILTIN, 2);
app.begin();
// Rapid button noise: HIGH-LOW-HIGH-LOW-HIGH
uint8_t sequence[] = {HIGH, LOW, HIGH, LOW, HIGH};
int mode_changes = 0;
bool was_fast = false;
for (uint8_t val : sequence) {
sim_.setPin(2, val);
sim_.advanceMillis(10);
app.update();
if (app.fastMode() != was_fast) {
mode_changes++;
was_fast = app.fastMode();
}
}
// Each HIGH->LOW transition is a falling edge, so 2 edges
// (positions 0->1 and 2->3), toggling fast->slow->fast or slow->fast->slow
EXPECT_EQ(mode_changes, 2);
}
TEST_F(BlinkAppSystemTest, ButtonHeldDoesNotRepeatToggle) {
BlinkApp app(&sim_, LED_BUILTIN, 2);
app.begin();
// Press and hold for 50 update cycles
sim_.setPin(2, LOW);
for (int i = 0; i < 50; ++i) {
sim_.advanceMillis(10);
app.update();
}
// Should have toggled exactly once (to fast mode)
EXPECT_TRUE(app.fastMode());
// Release and hold released for 50 cycles
sim_.setPin(2, HIGH);
for (int i = 0; i < 50; ++i) {
sim_.advanceMillis(10);
app.update();
}
// Still fast -- release alone should not toggle
EXPECT_TRUE(app.fastMode());
}
// -----------------------------------------------------------------------
// Serial output verification
// -----------------------------------------------------------------------
TEST_F(BlinkAppSystemTest, PrintsStartupMessage) {
BlinkApp app(&sim_);
app.begin();
EXPECT_NE(sim_.serialOutput().find("BlinkApp started"), std::string::npos);
}
TEST_F(BlinkAppSystemTest, PrintsModeChangeMessages) {
BlinkApp app(&sim_, LED_BUILTIN, 2);
app.begin();
sim_.clearSerialOutput();
// Press button
sim_.setPin(2, LOW);
sim_.advanceMillis(10);
app.update();
EXPECT_NE(sim_.serialOutput().find("FAST"), std::string::npos);
sim_.setPin(2, HIGH);
sim_.advanceMillis(10);
app.update();
sim_.clearSerialOutput();
// Press again
sim_.setPin(2, LOW);
sim_.advanceMillis(10);
app.update();
EXPECT_NE(sim_.serialOutput().find("SLOW"), std::string::npos);
}
// -----------------------------------------------------------------------
// GPIO log / timing verification
// -----------------------------------------------------------------------
TEST_F(BlinkAppSystemTest, LedToggleTimingIsCorrect) {
BlinkApp app(&sim_);
app.begin();
// Run for 3 seconds at 1ms resolution
for (int i = 0; i < 3000; ++i) {
sim_.advanceMillis(1);
app.update();
}
const auto& log = sim_.gpioLog();
ASSERT_GE(log.size(), 4u);
// Check intervals between consecutive toggles on pin 13
for (size_t i = 1; i < log.size(); ++i) {
if (log[i].pin == LED_BUILTIN && log[i - 1].pin == LED_BUILTIN) {
unsigned long delta = log[i].timestamp_ms - log[i - 1].timestamp_ms;
// Should be approximately 500ms (+/- 10ms for loop granularity)
EXPECT_NEAR(delta, 500, 10)
<< "Toggle " << i << " at t=" << log[i].timestamp_ms;
}
}
}
TEST_F(BlinkAppSystemTest, PinModesAreConfiguredCorrectly) {
BlinkApp app(&sim_, LED_BUILTIN, 2);
app.begin();
EXPECT_EQ(sim_.getPinMode(LED_BUILTIN), OUTPUT);
EXPECT_EQ(sim_.getPinMode(2), INPUT_PULLUP);
}