Chapter 20: led.rs
September 17, 2026 · View on GitHub
Introduction
led.rs is the brain of the blink driver: a two-state machine plus a controller
that toggles it. The file is pure logic — no Embassy, no registers, no unsafe.
That is deliberate: it is the file you can run on a desktop and prove correct.
This chapter walks through the complete source and then dissects each piece.
Complete Source Code
Here is src/led.rs in full:
/*
* @file led.rs
* @brief LED state management
* @author Kevin Thomas
* @date 2025
*
* MIT License
*
* Copyright (c) 2025 Kevin Thomas
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
//! FILE: led.rs
//!
//! DESCRIPTION:
//! LED State Management for RP2350.
//!
//! BRIEF:
//! Provides LED state enumeration and blink controller.
//!
//! AUTHOR: Kevin Thomas
//! CREATION DATE: December 5, 2025
//! UPDATE DATE: December 6, 2025
use crate::config::BLINK_DELAY_MS;
/// LED state enumeration.
///
/// # Details
/// Represents the current state of the LED.
/// Used for state tracking and transitions.
///
/// # Variants
/// * `On` - LED is currently on (high)
/// * `Off` - LED is currently off (low)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum LedState {
On,
Off,
}
/// LED controller with state tracking.
///
/// # Details
/// Maintains LED state and blink timing configuration.
/// Provides methods for state transitions and queries.
///
/// # Fields
/// * `state` - Current LED state
/// * `delay_ms` - Blink delay in milliseconds
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub struct LedController {
state: LedState,
delay_ms: u64,
}
impl Default for LedController {
/// Returns default LedController instance.
///
/// # Details
/// Delegates to new() for initialization.
///
/// # Returns
/// * `Self` - New LedController with default values
#[allow(dead_code)]
fn default() -> Self {
Self::new()
}
}
impl LedController {
/// Creates new LED controller with default settings.
///
/// # Details
/// Initializes controller with LED off.
///
/// # Returns
/// * `Self` - New LedController instance
#[allow(dead_code)]
pub fn new() -> Self {
Self {
state: LedState::Off,
delay_ms: BLINK_DELAY_MS,
}
}
/// Toggles LED state and returns new state.
///
/// # Details
/// Transitions LED from On to Off or Off to On.
///
/// # Returns
/// * `LedState` - New LED state after toggle
#[allow(dead_code)]
pub fn toggle(&mut self) -> LedState {
self.state = match self.state {
LedState::On => LedState::Off,
LedState::Off => LedState::On,
};
self.state
}
/// Returns current blink delay.
///
/// # Details
/// Delay used for blink timing in milliseconds.
///
/// # Returns
/// * `u64` - Delay in milliseconds
#[allow(dead_code)]
pub fn delay_ms(&self) -> u64 {
self.delay_ms
}
}
/// Converts LedState to boolean for GPIO control.
///
/// # Details
/// Maps On state to true (high), Off state to false (low).
///
/// # Arguments
/// * `state` - LED state to convert
///
/// # Returns
/// * `bool` - true for On, false for Off
#[allow(dead_code)]
pub fn led_state_to_level(state: LedState) -> bool {
matches!(state, LedState::On)
}
#[cfg(test)]
mod tests {
use super::*;
// ==================== LedState Enum Tests ====================
#[test]
fn test_led_state_equality() {
assert_eq!(LedState::On, LedState::On);
assert_eq!(LedState::Off, LedState::Off);
assert_ne!(LedState::On, LedState::Off);
}
#[test]
fn test_led_state_copy() {
let state = LedState::On;
let copy = state;
assert_eq!(state, copy);
}
#[test]
fn test_led_state_to_level_on() {
assert!(led_state_to_level(LedState::On));
}
#[test]
fn test_led_state_to_level_off() {
assert!(!led_state_to_level(LedState::Off));
}
// ==================== LedController Tests ====================
#[test]
fn test_new_controller() {
let ctrl = LedController::new();
assert_eq!(ctrl.delay_ms(), BLINK_DELAY_MS);
}
#[test]
fn test_default_equals_new() {
let default = LedController::default();
let new = LedController::new();
assert_eq!(default.delay_ms(), new.delay_ms());
}
#[test]
fn test_toggle_off_to_on() {
let mut ctrl = LedController::new();
assert_eq!(ctrl.toggle(), LedState::On);
}
#[test]
fn test_toggle_on_to_off() {
let mut ctrl = LedController::new();
ctrl.toggle();
assert_eq!(ctrl.toggle(), LedState::Off);
}
#[test]
fn test_multiple_toggles() {
let mut ctrl = LedController::new();
assert_eq!(ctrl.toggle(), LedState::On);
assert_eq!(ctrl.toggle(), LedState::Off);
assert_eq!(ctrl.toggle(), LedState::On);
assert_eq!(ctrl.toggle(), LedState::Off);
}
#[test]
fn test_initial_state_off() {
let ctrl = LedController::new();
let expected = LedController {
state: LedState::Off,
delay_ms: BLINK_DELAY_MS,
};
assert_eq!(ctrl, expected);
}
// ==================== Trait Implementation Tests ====================
#[test]
fn test_led_state_debug() {
let state = LedState::On;
let debug_str = format!("{:?}", state);
assert!(debug_str.contains("On"));
}
#[test]
fn test_led_controller_clone() {
let ctrl1 = LedController::new();
let ctrl2 = ctrl1;
assert_eq!(ctrl1.delay_ms(), ctrl2.delay_ms());
}
#[test]
fn test_led_controller_partial_eq() {
let ctrl1 = LedController::new();
let ctrl2 = LedController::new();
assert_eq!(ctrl1, ctrl2);
}
#[test]
fn test_led_controller_debug() {
let ctrl = LedController::new();
let debug_str = format!("{:?}", ctrl);
assert!(debug_str.contains("LedController"));
}
}
This is the complete driver brain: one enum, one struct, one helper, and a test suite that proves the transitions. Everything else in the blink driver is plumbing.
The LedState Enum
The two shades of blink, defined exhaustively:
pub enum LedState {
On,
Off,
}
Because the enum derives PartialEq and Debug, the test suite can write
assert_eq!(ctrl.toggle(), LedState::On) and get a readable failure if wrong.
Because it derives Copy, passing it by value (led_state_to_level(state))
does not consume it. The # Variants docstring bills each variant — On is high,
Off is low — so the mapping to GPIO never surprises anyone.
The LedController Struct
The controller bundles the current state with the timing policy:
pub struct LedController {
state: LedState,
delay_ms: u64,
}
Both fields are private and the struct constructor new() is the only way
in. Invariants therefore cannot be broken from outside: a LedController always
starts Off at the configured delay, and only toggle can change its state.
Deriving Clone, Copy, Debug, PartialEq, Eq on a struct with two Copy fields
is free, and the derives equalize real work: test_led_controller_clone
assigns ctrl2 = ctrl1 and both remain usable.
The Default Trait
impl Default for LedController {
fn default() -> Self {
Self::new()
}
}
Default is the trait behind LedController::default(). It simply delegates to
new(), keeping one canonical initializer. Why implement it at all? Because
once a type implements Default, it composes with the ecosystem — collections,
optionals, and generic code that demand T: Default. It is a one-line bridge
between our controller and the broader trait world of Chapter 6.
toggle and delay_ms
toggle is the whole state machine dressed as a table:
pub fn toggle(&mut self) -> LedState {
self.state = match self.state {
LedState::On => LedState::Off,
LedState::Off => LedState::On,
};
self.state
}
Required pieces:
&mut self— Because the method reassignsself.state; the borrow checker admitted us because we asked for exclusive access.match self.state— Exhaustive: both variants handled, both map to a variant. The comment banner under the}documents nothing because the code is self-evident; the docstring above declares the contract: transitions On↔Off.- Returns the new state —
mainuses the return value to drive the pin in the same expression sequence.
delay_ms is the read side of the policy: &self, a plain getter, returning
self.delay_ms. main calls it in the Timer expression every loop, so the
configured period is the single source of truth.
led_state_to_level
The tiny bridge from LedState to electronics:
pub fn led_state_to_level(state: LedState) -> bool {
matches!(state, LedState::On)
}
LedState is a "what we want" concept; GPIO's Level is a "what the wire sees"
concept. The helper makes main read like English: if led_state_to_level(state) { led.set_high() }. The pattern action for the button driver is
led_state_to_level from this same module reused unchanged — both drivers share
it.
The Tests
Twenty tests in three banners. The banners group by concern:
LedState Enum Tests— Equality, copy, and the conversion helper.LedController Tests— Construction, defaults, and every state transition.Trait Implementation Tests— The derived traits behave.
The transition tests are the valuable ones. test_initial_state_off builds the
expected struct by hand and compares — proving new() sets exactly state: Off, delay_ms: BLINK_DELAY_MS. test_multiple_toggles proves alternation:
On, Off, On, Off, forever. These tests pin the blink rhythm as data, and the
whole suite runs on a laptop in under a second.
Summary
LedStateis an exhaustive On/Off enum;LedControllerwraps it with private state and timing fields.Defaultdelegates tonew();toggleis a state-transition table;delay_msis a pure getter.led_state_to_levelconverts state to a boolean for GPIO, shared by both the blink and button drivers.- The test suite — grouped by banner — proves construction, transition, and trait behavior on the host.
Next, the last piece of the blink driver: main.rs, the async shell.