
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):
Clockless LED support (e.g. WS2812)
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

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,
[
Shape3d::Grid {
start: Vec3::new(1., -1., 1.), horizontal_end: Vec3::new(-1., -1., 1.), vertical_end: Vec3::new(1., -1., -1.), horizontal_pixel_count: 16,
vertical_pixel_count: 16,
serpentine: true,
},
Shape3d::Grid {
start: Vec3::new(-1., -1., -1.), horizontal_end: Vec3::new(-1., 1., -1.), vertical_end: Vec3::new(1., -1., -1.), horizontal_pixel_count: 16,
vertical_pixel_count: 16,
serpentine: true,
},
Shape3d::Grid {
start: Vec3::new(1., 1., -1.), horizontal_end: Vec3::new(1., 1., 1.), vertical_end: Vec3::new(1., -1., -1.), horizontal_pixel_count: 16,
vertical_pixel_count: 16,
serpentine: true,
},
Shape3d::Grid {
start: Vec3::new(-1., -1., 1.), horizontal_end: Vec3::new(1., -1., 1.), vertical_end: Vec3::new(-1., 1., 1.), horizontal_pixel_count: 16,
vertical_pixel_count: 16,
serpentine: true,
},
Shape3d::Grid {
start: Vec3::new(-1., 1., -1.), horizontal_end: Vec3::new(-1., -1., -1.), vertical_end: Vec3::new(-1., 1., 1.), horizontal_pixel_count: 16,
vertical_pixel_count: 16,
serpentine: true,
},
Shape3d::Grid {
start: Vec3::new(1., 1., 1.), horizontal_end: Vec3::new(1., 1., -1.), vertical_end: Vec3::new(-1., 1., 1.), 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.