blinksy-esp 0.11.0

no-std, no-alloc LED control library for 1D, 2D, and 3D layouts
Documentation

GitHub Repo stars GitHub Sponsors Chat License

How Blinksy works

  • Define your LED layout in 1D, 2D, or 3D space
  • Create your visual pattern (effect), or choose from our built-in patterns library
    • The pattern will compute colors for each LED based on its position
  • Setup a driver to send each frame of colors to your LEDs, using our built-in drivers library.

Features

  • No-std, no-alloc: Designed for embedded targets.
  • Spatial in 1D, 2D, or 3D: Map out the shape of your LEDs in space.
  • Async support: Supports blocking or asynchronous execution.
  • Full color support: Supports modern and classic color spaces.
  • Global settings: Control overall brightness and color correction.
  • Desktop simulation: Simulate your LEDs on your desktop to play with ideas.
  • RGB+W support: Supports RGB + White color channels

LED Support

Clockless: One-wire (only data, no clock)

Clocked: Two-wire (data and clock)

  • APA102: High-FPS RGB LED, aka DotStar

If you want help to support a new LED chipset, make an issue!

Pattern (Effect) Library:

  • Rainbow: A basic scrolling rainbow
  • Noise: A flow through random noise functions

If you want help to port a pattern from FastLED / WLED to Rust, make an issue!

Microcontroller Family Support

Clocked LED support (e.g. APA102):

Micro HAL Blinksy Recommended Driver Backup Driver
ALL embedded-hal blinksy Spi Delay

Clockless LED support (e.g. WS2812)

Micro HAL Blinksy Recommended Driver Backup Driver
ALL embedded-hal blinksy - TODO Spi #12
ESP32 esp-hal blinksy-esp Rmt -
RP (2040 or 2350) rp-hal TODO TODO #36 -
STM32 stm32-hal TODO TODO #78 -
nRF nrf-hal TODO TODO #77 -
atsamd atsamd TODO TODO #67 -
AVR (Arduino) avr-hal TODO TODO #79 -
CH32 ch32-hal TODO TODO #80 -
??? - - - -

If you want help to support a new microcontroller family, make an issue!

Board Support

These are ready-to-go LED controllers with board support crates to make things even easier:

If you want help to support a new target, make an issue!

Quick Start

To quickstart a project, see:

To start using the library, see control.

Modules

CI status

Examples

For all examples, see:

Embedded Gledopto: 3D Cube with Noise Pattern

https://github.com/user-attachments/assets/36a2c6ad-7ae6-4498-85b3-ed76d0b62264

#![no_std]
#![no_main]

use blinksy::{
    layout::{Layout3d, Shape3d, Vec3},
    layout3d,
    leds::Ws2812,
    patterns::noise::{noise_fns, Noise3d, NoiseParams},
    ControlBuilder,
};
use gledopto::{board, bootloader, elapsed, main, ws2812};

bootloader!();

#[main]
fn main() -> ! {
    let p = board!();

    layout3d!(
        Layout,
        [
            // bottom face
            Shape3d::Grid {
                start: Vec3::new(1., -1., 1.),           // right bottom front
                horizontal_end: Vec3::new(-1., -1., 1.), // left bottom front
                vertical_end: Vec3::new(1., -1., -1.),   // right bottom back
                horizontal_pixel_count: 16,
                vertical_pixel_count: 16,
                serpentine: true,
            },
            // back face
            Shape3d::Grid {
                start: Vec3::new(-1., -1., -1.),         // left bottom back
                horizontal_end: Vec3::new(-1., 1., -1.), // left top back
                vertical_end: Vec3::new(1., -1., -1.),   // right bottom back
                horizontal_pixel_count: 16,
                vertical_pixel_count: 16,
                serpentine: true,
            },
            // right face
            Shape3d::Grid {
                start: Vec3::new(1., 1., -1.),         // right top back
                horizontal_end: Vec3::new(1., 1., 1.), // right top front
                vertical_end: Vec3::new(1., -1., -1.), // right bottom back
                horizontal_pixel_count: 16,
                vertical_pixel_count: 16,
                serpentine: true,
            },
            // front face
            Shape3d::Grid {
                start: Vec3::new(-1., -1., 1.),         // left bottom front
                horizontal_end: Vec3::new(1., -1., 1.), // right bottom front
                vertical_end: Vec3::new(-1., 1., 1.),   // left top front
                horizontal_pixel_count: 16,
                vertical_pixel_count: 16,
                serpentine: true,
            },
            // left face
            Shape3d::Grid {
                start: Vec3::new(-1., 1., -1.),           // left top back
                horizontal_end: Vec3::new(-1., -1., -1.), // left bottom back
                vertical_end: Vec3::new(-1., 1., 1.),     // left top front
                horizontal_pixel_count: 16,
                vertical_pixel_count: 16,
                serpentine: true,
            },
            // top face
            Shape3d::Grid {
                start: Vec3::new(1., 1., 1.),           // right top front
                horizontal_end: Vec3::new(1., 1., -1.), // right top back
                vertical_end: Vec3::new(-1., 1., 1.),   // left top front
                horizontal_pixel_count: 16,
                vertical_pixel_count: 16,
                serpentine: true,
            }
        ]
    );

    let mut control = ControlBuilder::new_3d()
        .with_layout::<Layout, { Layout::PIXEL_COUNT }>()
        .with_pattern::<Noise3d<noise_fns::Perlin>>(NoiseParams {
            ..Default::default()
        })
        .with_driver(ws2812!(p, Layout::PIXEL_COUNT))
        .with_frame_buffer_size::<{ Ws2812::frame_buffer_size(Layout::PIXEL_COUNT) }>()
        .build();

    control.set_brightness(0.2);

    loop {
        let elapsed_in_ms = elapsed().as_millis();
        control.tick(elapsed_in_ms).unwrap();
    }
}

Desktop Simulation: 2D Grid with Noise Pattern

https://github.com/user-attachments/assets/22f388d0-189e-44bd-acbf-186a142b956d

use blinksy::{
    layout::{Layout2d, Shape2d, Vec2},
    layout2d,
    patterns::noise::{noise_fns, Noise2d, NoiseParams},
    ControlBuilder,
};
use blinksy_desktop::{
    driver::{Desktop, DesktopError},
    time::elapsed_in_ms,
};
use std::{thread::sleep, time::Duration};

layout2d!(
    PanelLayout,
    [Shape2d::Grid {
        start: Vec2::new(-1., -1.),
        horizontal_end: Vec2::new(1., -1.),
        vertical_end: Vec2::new(-1., 1.),
        horizontal_pixel_count: 16,
        vertical_pixel_count: 16,
        serpentine: true,
    }]
);

fn main() {
    Desktop::new_2d::<PanelLayout>().start(|driver| {
        let mut control = ControlBuilder::new_2d()
            .with_layout::<PanelLayout, { PanelLayout::PIXEL_COUNT }>()
            .with_pattern::<Noise2d<noise_fns::Perlin>>(NoiseParams {
                ..Default::default()
            })
            .with_driver(driver)
            .with_frame_buffer_size::<{ PanelLayout::PIXEL_COUNT }>()
            .build();

        loop {
            if let Err(DesktopError::WindowClosed) = control.tick(elapsed_in_ms()) {
                break;
            }

            sleep(Duration::from_millis(16));
        }
    });
}

Embedded Gledopto: 1D WS2812 Strip with Rainbow Pattern

https://github.com/user-attachments/assets/703fe31d-e7ca-4e08-ae2b-7829c0d4d52e

#![no_std]
#![no_main]

use blinksy::{
    layout::Layout1d,
    layout1d,
    leds::Ws2812,
    patterns::rainbow::{Rainbow, RainbowParams},
    ControlBuilder,
};
use gledopto::{board, bootloader, elapsed, main, ws2812};

bootloader!();

#[main]
fn main() -> ! {
    let p = board!();

    layout1d!(Layout, 50);

    let mut control = ControlBuilder::new_1d()
        .with_layout::<Layout, { Layout::PIXEL_COUNT }>()
        .with_pattern::<Rainbow>(RainbowParams::default())
        .with_driver(ws2812!(p, Layout::PIXEL_COUNT, buffered))
        .with_frame_buffer_size::<{ Ws2812::frame_buffer_size(Layout::PIXEL_COUNT) }>()
        .build();

    control.set_brightness(0.2);

    loop {
        let elapsed_in_ms = elapsed().as_millis();
        control.tick(elapsed_in_ms).unwrap();
    }
}
#![no_std]
#![no_main]
#![feature(impl_trait_in_assoc_type)]

use blinksy::{
    layout::Layout1d,
    layout1d,
    leds::Ws2812,
    patterns::rainbow::{Rainbow, RainbowParams},
    ControlBuilder,
};
use embassy_executor::Spawner;
use gledopto::{board, bootloader, elapsed, init_embassy, main_embassy, ws2812_async};

bootloader!();

#[main_embassy]
async fn main(_spawner: Spawner) {
    let p = board!();

    init_embassy!(p);

    layout1d!(Layout, 50);

    let mut control = ControlBuilder::new_1d_async()
        .with_layout::<Layout, { Layout::PIXEL_COUNT }>()
        .with_pattern::<Rainbow>(RainbowParams::default())
        .with_driver(ws2812_async!(p, Layout::PIXEL_COUNT, buffered))
        .with_frame_buffer_size::<{ Ws2812::frame_buffer_size(Layout::PIXEL_COUNT) }>()
        .build();

    control.set_brightness(0.2);

    loop {
        let elapsed_in_ms = elapsed().as_millis();
        control.tick(elapsed_in_ms).await.unwrap();
    }
}

Embedded Gledopto: 2D APA102 Grid with Noise Pattern

https://github.com/user-attachments/assets/1c1cf3a2-f65c-4152-b444-29834ac749ee

#![no_std]
#![no_main]

use blinksy::{
    layout::{Layout2d, Shape2d, Vec2},
    layout2d,
    leds::Apa102,
    patterns::noise::{noise_fns, Noise2d, NoiseParams},
    ControlBuilder,
};
use gledopto::{apa102, board, bootloader, elapsed, main};

bootloader!();

#[main]
fn main() -> ! {
    let p = board!();

    layout2d!(
        Layout,
        [Shape2d::Grid {
            start: Vec2::new(-1., -1.),
            horizontal_end: Vec2::new(1., -1.),
            vertical_end: Vec2::new(-1., 1.),
            horizontal_pixel_count: 16,
            vertical_pixel_count: 16,
            serpentine: true,
        }]
    );
    let mut control = ControlBuilder::new_2d()
        .with_layout::<Layout, { Layout::PIXEL_COUNT }>()
        .with_pattern::<Noise2d<noise_fns::Perlin>>(NoiseParams::default())
        .with_driver(apa102!(p))
        .with_frame_buffer_size::<{ Apa102::frame_buffer_size(Layout::PIXEL_COUNT) }>()
        .build();

    control.set_brightness(0.2);

    loop {
        let elapsed_in_ms = elapsed().as_millis();
        control.tick(elapsed_in_ms).unwrap();
    }
}
#![no_std]
#![no_main]
#![feature(impl_trait_in_assoc_type)]

use blinksy::{
    layout::{Layout2d, Shape2d, Vec2},
    layout2d,
    patterns::noise::{noise_fns, Noise2d, NoiseParams},
    ControlBuilder,
};
use embassy_executor::Spawner;
use gledopto::{apa102_async, board, bootloader, elapsed, main_embassy};

bootloader!();

#[main_embassy]
async fn main(_spawner: Spawner) {
    let p = board!();

    layout2d!(
        Layout,
        [Shape2d::Grid {
            start: Vec2::new(-1., -1.),
            horizontal_end: Vec2::new(1., -1.),
            vertical_end: Vec2::new(-1., 1.),
            horizontal_pixel_count: 16,
            vertical_pixel_count: 16,
            serpentine: true,
        }]
    );
    let mut control = ControlBuilder::new_2d_async()
        .with_layout::<Layout, { Layout::PIXEL_COUNT }>()
        .with_pattern::<Noise2d<noise_fns::Perlin>>(NoiseParams::default())
        .with_driver(apa102_async!(p))
        .build();

    control.set_brightness(0.2);

    loop {
        let elapsed_in_ms = elapsed().as_millis();
        control.tick(elapsed_in_ms).await.unwrap();
    }
}

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details.

If you want to help, the best thing to do is use Blinksy for your own LED project, and share about your adventures.

License

Blinksy is licensed under the European Union Public License (EUPL).

You are free to use, modify, and share Blinksy freely. Whether for personal projects, art installations, or commercial products.

Only once you start distributing something based on changes to Blinksy, you must share any improvements back with the community by releasing your source code.

Unlike more viral copyleft licenses, you will not be required to release the source code for your entire project, only changes to Blinksy.