Blinksy π₯π©π¦
- Define your LED
layoutin 1D, 2D, or 3D space - Create your visual
pattern(effect), or choose from our built-inpatternslibrary- The pattern will compute colors for each LED based on its position in space
- Setup a [
driver] to send each frame of colors to your [leds]
- 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
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!
If you want help to port a pattern from FastLED / WLED to Rust, make an issue!
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!
These are ready-to-go LED controllers with board support crates to make things even easier:
- Gledopto: A great LED controller available on AliExpress: Gledopto GL-C-016WL-D
- (TODO) QuinLED: The best DIY and pre-assembled LED controller boards
If you want help to support a new target, make an issue!
To quickstart a project, see:
To start using the library, see control.
blinksy:blinksy-desktop:blinksy-esp:gledopto:
For all examples, see:
- Desktop examples in
./blinksy-desktop/examples - Embedded (with Gledopto) examples in
./esp/gledopto/examples
cube-demo-2025-06-27-1080p.mp4
Click to see code
#![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();
}
}Screencast.2025-05-19.11.59.44.avi.mp4
Click to see code
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));
}
});
}VID20250327160955.mp4
Click to see code (Blocking)
#![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();
}
}Click to see code (Async)
#![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();
}
}VID20250401221726.mp4
Click to see code (Blocking)
#![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();
}
}Click to see code (Async)
#![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();
}
}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.
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.
