Testable Embedded Rust

What if we could unit-test our LED animations on the laptop before ever touching the real hardware?

The first experiments had minutes-long visual feedback loops, which felt frustratingly slow.

Note: This is not the "blink an LED" tutorials you've seen a hundred times, but real-world patterns for building maintainable, testable embedded systems.

We'll start with a project that challenges a common assumption: Embedded development requires hardware at every step.

The Project: An ESP32-S3 RGB Clock

The rustyfarian-rgb-clock is exactly what it sounds like:

An ESP32-C6 RGB LED clock that displays time using 12 WS2812 NeoPixel LEDs arranged in a clock face. Time is received via MQTT from an external source.

But the interesting part isn't what it does — it's how the codebase is structured.

The Problem with Embedded "in-house" Libraries

If you've worked with such LEDs in Rust before, you've probably noticed a pattern: The color math, the timing logic, and the hardware driver are all tangled together. Want to test that your rainbow animation cycles correctly? Better plug in your dev board.

This wasn't the way we used to develop (In non embedded projects). Writing code, flash it, watch it fail, adjust, flash again. The feedback loop is measured in minutes, not milliseconds.

It started as a PoC for faster development

The RGB clock project is built on two library crates that take a different approach:

rustyfarian-ws2812 splits LED control into three layers:

  1. ws2812-core — Pure color math and conversion logic. No hardware dependencies. Runs on your laptop, fully unit-testable.
  2. led-effects — A StatusLed trait that decouples your application from any specific LED implementation
  3. esp32-ws2812-rmt — The ESP32-specific driver using the RMT peripheral

rustyfarian-network applies the same pattern to Wi-Fi connectivity—abstracting the network layer so business logic doesn't hardcode ESP-specific APIs.

Why This Matters

The StatusLed trait is deceptively simple:

pub trait StatusLed {
    type Error;
    fn set_color(&mut self, color: RGB8) -> Result<(), Self::Error>;
}

The clock code depends on this trait, not a concrete driver. In production, it's backed by real WS2812s. In tests, it's a mock that records every color change. Same code, different environments.

The core crates are fully no_std with zero allocations. The ESP driver uses fixed-size stack arrays ([Pulse; 48]) instead of Vec, avoiding heap fragmentation in long-running applications.

The DevOps Angle

This architecture enables: A CI pipeline that runs meaningful tests. Not just "does it compile for the target"—actual unit tests that verify color calculations, timing sequences, and state machine transitions.

When a test fails in CI, we know the problem is in the logic, not a loose wire on the breadboard


Grab the code and put some rust onto your ESP!