Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Coverage report
run: |
output=$(gcovr --root . --filter 'src/' --exclude 'src/SnipsController\.ino' --exclude 'src/oled\.cpp' --print-summary --fail-under-line 90)
output=$(gcovr --root . --filter 'src/' --exclude 'src/SnipsController\.ino' --exclude 'src/oled\.cpp' --exclude 'src/rgb_led\.cpp' --print-summary --fail-under-line 90)
status=$?
{
echo '### Coverage (src/, excluding SnipsController.ino and hardware adapters)'
Expand Down
20 changes: 20 additions & 0 deletions include/rgb_led.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <Adafruit_NeoPixel.h>

#include "pin_assignment.h"
#include "status_led.h"

// Thin hardware adapter over Adafruit_NeoPixel — drives the single
// SK6812 status LED (RMT-driven). No state->color decisions happen here;
// that's StatusLedController's job (status_led.h), which stays
// hardware-agnostic and unit tested. This file is excluded from native
// build/coverage (see platformio.ini's [env:native] build_src_filter).
class RgbLed {
public:
void begin();
void show(const RgbColor &color);

private:
Adafruit_NeoPixel pixel_{1, PinAssignment::kRgbLedData, NEO_GRB + NEO_KHZ800};
};
25 changes: 25 additions & 0 deletions include/status_led.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include <cstdint>

// Pure logic mapping overall system state to an RGB status color. Knows
// nothing about the real LED hardware — src/rgb_led.cpp is the thin
// adapter that actually drives it.
enum class SystemState {
kBooting,
kConnected,
kDisconnected,
kCharging,
kError,
};

struct RgbColor {
uint8_t r;
uint8_t g;
uint8_t b;
};

class StatusLedController {
public:
RgbColor colorFor(SystemState state) const;
};
6 changes: 4 additions & 2 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ monitor_speed = 115200
lib_deps =
adafruit/Adafruit SSD1306@^2.5.13
adafruit/Adafruit GFX Library@^1.11.11
adafruit/Adafruit NeoPixel@^1.15.4

; Native (host) environment for unit tests — no hardware required.
; SnipsController.ino pulls in Arduino.h, so it's excluded here; everything
; else in src/ is hardware-agnostic and gets linked into the test binary,
; EXCEPT a small, explicitly-named list of genuinely hardware-touching
; adapter files (thin wrappers over I2C/SPI/ADC/RMT/NVS), added here
; alongside as new subsystems land — see CLAUDE.md's testing-approach note.
; Currently: oled.cpp (Adafruit_SSD1306/Wire).
; Currently: oled.cpp (Adafruit_SSD1306/Wire), rgb_led.cpp
; (Adafruit_NeoPixel/RMT).
[env:native]
platform = native
test_build_src = yes
build_src_filter = +<*> -<*.ino> -<oled.cpp>
build_src_filter = +<*> -<*.ino> -<oled.cpp> -<rgb_led.cpp>
test_framework = unity
build_flags = --coverage
extra_scripts = pre:scripts/native_coverage_linkflags.py
21 changes: 21 additions & 0 deletions src/SnipsController.ino
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
#include "oled.h"
#include "pin_assignment.h"
#include "power_latch.h"
#include "rgb_led.h"
#include "screen.h"
#include "status_led.h"

// Pure entry point — wiring only. All real logic lives in dedicated
// subsystem files under src/ + include/; this file just owns real
Expand All @@ -15,6 +17,8 @@ namespace {
PowerOffDetector powerOffDetector;
ButtonPanel buttonPanel;
OledDisplay oledDisplay;
RgbLed rgbLed;
StatusLedController statusLedController;
bool lastReportedPressed[Buttons::kCount] = {};

const char *buttonName(size_t index) {
Expand Down Expand Up @@ -74,6 +78,23 @@ void setup() {
} else {
Serial.println("OLED not found at boot.");
}

// Bring-up check per PCB/README.md's recommended order: cycle through
// every status color once to prove RMT output on the real LED. Real
// state (connected/charging/error) gets driven by later PRs once there's
// an XBee link and charge-status reading to base it on; for now this
// just settles on "disconnected," which is accurate today.
rgbLed.begin();
const SystemState bringUpSequence[] = {
SystemState::kBooting, SystemState::kConnected,
SystemState::kDisconnected, SystemState::kCharging,
SystemState::kError,
};
for (SystemState state : bringUpSequence) {
rgbLed.show(statusLedController.colorFor(state));
delay(300);
}
rgbLed.show(statusLedController.colorFor(SystemState::kDisconnected));
}

void loop() {
Expand Down
11 changes: 11 additions & 0 deletions src/rgb_led.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include "rgb_led.h"

void RgbLed::begin() {
pixel_.begin();
pixel_.show(); // off until told otherwise
}

void RgbLed::show(const RgbColor &color) {
pixel_.setPixelColor(0, pixel_.Color(color.r, color.g, color.b));
pixel_.show();
}
17 changes: 17 additions & 0 deletions src/status_led.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include "status_led.h"

RgbColor StatusLedController::colorFor(SystemState state) const {
switch (state) {
case SystemState::kBooting:
return {0, 0, 255}; // blue
case SystemState::kConnected:
return {0, 255, 0}; // green
case SystemState::kDisconnected:
return {255, 255, 0}; // yellow
case SystemState::kCharging:
return {255, 165, 0}; // orange
case SystemState::kError:
return {255, 0, 0}; // red
}
return {0, 0, 0};
}
67 changes: 67 additions & 0 deletions test/test_status_led/test_status_led.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include <unity.h>

#include "status_led.h"

void setUp(void) {}
void tearDown(void) {}

void test_booting_is_blue() {
StatusLedController controller;
RgbColor color = controller.colorFor(SystemState::kBooting);
TEST_ASSERT_EQUAL_UINT8(0, color.r);
TEST_ASSERT_EQUAL_UINT8(0, color.g);
TEST_ASSERT_EQUAL_UINT8(255, color.b);
}

void test_connected_is_green() {
StatusLedController controller;
RgbColor color = controller.colorFor(SystemState::kConnected);
TEST_ASSERT_EQUAL_UINT8(0, color.r);
TEST_ASSERT_EQUAL_UINT8(255, color.g);
TEST_ASSERT_EQUAL_UINT8(0, color.b);
}

void test_disconnected_is_yellow() {
StatusLedController controller;
RgbColor color = controller.colorFor(SystemState::kDisconnected);
TEST_ASSERT_EQUAL_UINT8(255, color.r);
TEST_ASSERT_EQUAL_UINT8(255, color.g);
TEST_ASSERT_EQUAL_UINT8(0, color.b);
}

void test_charging_is_orange() {
StatusLedController controller;
RgbColor color = controller.colorFor(SystemState::kCharging);
TEST_ASSERT_EQUAL_UINT8(255, color.r);
TEST_ASSERT_EQUAL_UINT8(165, color.g);
TEST_ASSERT_EQUAL_UINT8(0, color.b);
}

void test_error_is_red() {
StatusLedController controller;
RgbColor color = controller.colorFor(SystemState::kError);
TEST_ASSERT_EQUAL_UINT8(255, color.r);
TEST_ASSERT_EQUAL_UINT8(0, color.g);
TEST_ASSERT_EQUAL_UINT8(0, color.b);
}

void test_unknown_state_is_off() {
StatusLedController controller;
// A value outside the enum's defined range — the defensive fallback for
// whatever calls colorFor() with something invalid.
RgbColor color = controller.colorFor(static_cast<SystemState>(99));
TEST_ASSERT_EQUAL_UINT8(0, color.r);
TEST_ASSERT_EQUAL_UINT8(0, color.g);
TEST_ASSERT_EQUAL_UINT8(0, color.b);
}

int main(int argc, char **argv) {
UNITY_BEGIN();
RUN_TEST(test_booting_is_blue);
RUN_TEST(test_connected_is_green);
RUN_TEST(test_disconnected_is_yellow);
RUN_TEST(test_charging_is_orange);
RUN_TEST(test_error_is_red);
RUN_TEST(test_unknown_state_is_off);
return UNITY_END();
}
Loading