published: 2024.11.10
author: David Cosby
tags:
- misc
Introducing Sled, a Rust Library for Creating Spatial LED Strip Lighting Effects
I am excited to announce the stable 0.1 release of spatial_led, my open-source rust library that lets you use LED strips in a totally new way.
Typically, when we think of individually addressable LED strips, we see them as an array of color values, like this:
Index | Color |
---|---|
0 | #5C3F4F |
1 | #59464C |
2 | #55544A |
3 | #48584E |
4 | #3F5865 |
The beauty of this setup is in its simplicity. To change the color of the 3rd LED in a strip, we'd write something like this:
leds[2] = Rgb::new(1.0, 0.0, 0.0);
The idea of Spatial LED (abbreviated as Sled), is to consider the physical position of each LED in 2d space, and then expose that information in a way that is valuable to a person designing lighting effects.
Under the hood, a Sled configuration looks a little more like this:
Index | Color | Segment | Position | Angle | Distance |
---|---|---|---|---|---|
0 | #5C3F4F | 0 | |||
1 | #59464C | 0 | |||
2 | #55544A | 0 | |||
3 | #48584E | 0 | |||
4 | #3F5865 | 0 | |||
150 | #293136 | 1 | |||
151 | #333C43 | 1 | |||
152 | #3A464C | 1 | |||
153 | #434F55 | 1 | |||
154 | #4D5960 | 1 | |||
Of course, properties like Position
and Angle
aren't super easily indexible, so lets talk about the API.
A Sled struct is most easily constructed by passing in some data via a configuration file.
use spatial_led::<Sled>;
fn main() {
let mut sled = Sled::new("/path/to/config.yap").unwrap();
}
Our config.yap
file will look something like this:
center: (0.0, 0.5)
density: 30.0
--segments--
(-2, 0) --> (0.5, -1) --> (3.5, 0) -->
(2, 2) --> (-2, 2) --> (-2, 0)
Where:
center
is a 2d point to which all pre-calculated angles will be relative to, anddensity
is the number of LEDs per meter (or whatever unit of measurement you're using)Once we've provided all that information, the constructor will map out the shape of our LED strips in 2D space and precalculate all that information you see in the table above.
The official documentation provides several examples, but let me give a taste of what this library enables you to do.
To set all LEDs 2 Units away from the center
to red:
sled.set_at_dist(2.0, Rgb::new(1.0, 0.0, 0.0));
// or relative to any other point using:
sled.set_at_dist_from(DISTANCE, POSITION, COLOR)
Similar method like set_at_angle(angle, color)
or set_at_dir(direction, color)
, work by running a simple intersection test between line segments and a ray.
Imagine if we declare a function that would take an input spatial property (like distance, position, angle, etc) and map it to an output color.
A mathematical notation for a mapping of 2D direction from the reference point
Expressing that in Sled is super easy.
sled.map_by_dir_from(Vec2::new(2.0, 1.0), |dir| {
let red = (dir.x + 1.0) * 0.5;
let green = (dir.y + 1.0) * 0.5;
Rgb::new(red, green, 0.5)
});
Where a mapping function replaces the old color with a new one given some color rule function, a modulation function lets you operate on the old color value to produce a modified color.
sled.modulate_segment(3, |led| led.color * 0.25)?;
It's worth noting that Sled doesn't interact directly with the GPIO pins of whatever hardware you're running this on; it is solely responsible for color computation. Whenever you need your colors back in a simple one dimensional array like we talked about earlier, you can call one use one of Sled's several output methods.
let colors = sled.colors();
for color in colors {
println!(
"{}, {}, {}",
color.red, color.green, color.blue
);
}
If you're using this library, odds are you'll want to use it to design some cool time-driven effects rather than just static images. For that reason, some optional extra tools are thrown into the library for your convenience.
A Driver is used to encapsulate all logic you might need to drive an effect into one structure. There's a lot of convenience to this design pattern, including the ease of swapping between different effects.
fn main() {
let sled = Sled::new("path/to/config.yap").unwrap();
let mut driver = Driver::new();
driver.set_startup_commands(startup); // startup is a function
driver.set_draw_commands(draw); // same here
driver.mount(sled);
let mut scheduler = Scheduler::new(240.0); // 240hz
scheduler.loop_forever(|| {
driver.step();
let colors = driver.colors();
// display our colors somehow
});
}
Our startup
and draw
commands could be defined as follows:
Startup
#[startup_commands]
fn startup(buffers: &mut BufferContainer) -> SledResult {
let colors = buffers.create_buffer::<Rgb>("colors");
colors.extend([
Rgb::new(1.0, 0.0, 0.0),
Rgb::new(0.0, 1.0, 0.0),
Rgb::new(0.0, 0.0, 1.0),
]);
Ok(())
}
#[draw_commands]
fn draw(
sled: &mut Sled,
buffers: &BufferContainer,
time_info: &TimeInfo
) -> SledResult {
let elapsed = time_info.elapsed.as_secs_f32();
let colors = buffers.get_buffer::<Rgb>("colors")?;
let num_colors = colors.len();
// clear our canvas each frame
sled.set_all(Rgb::new(0.0, 0.0, 0.0));
for i in 0..num_colors {
let alpha = i as f32 / num_colors as f32;
let angle = elapsed + (std::f32::consts::TAU * alpha);
sled.set_at_angle(angle, colors[i]);
}
Ok(())
}
Again, consult the documentation for more details, but it's worth explaining what all that BufferContainer yap is about.
A buffer, under the hood, is just an array of values. You can create them for any type that you might need and store them in your driver for use in your effects. The benefit of storing our red, green, and blue colors in the example above (rather than just hard-coding them into our effect) is the ability drivers give us to modify buffer values from outside our driver.
So if we had some super cool dashboard to control our effects and the user wanted to change the color of our red spinning LED at runtime, we could do something like this:
driver
.buffers_mut()
.set_buffer_item::<Rgb>("colors", 0, user_requested_color)
.unwrap(); // will error if there's no buffer of Rgb values called 'colors'.
Here are a handful of cool effects that I've whipped up using this library. I'm rendering these using ratatui so you can see from a top-down perspective what's going on. The room shape is also pretty unusual, but again that's just so you better see how Sled interacts with the shape of the room.
Simulates a straight line sweeping through the room at random angles, whose color gradually progresses through the rainbow over time.
Click here if the video player isn't loading.Animates a rigid multi-fractal noise pattern across the room, mapping the noise output to a color ramp.
Click here if the video player isn't loading.Simulates growing rings of color at random points in your room.
Click here if the video player isn't loading.Simulates stars zooming past you, almost like you're traveling in a space ship at light speed. All directions are relative to the center point declared in your config file.
Click here if the video player isn't loading.I'm super happy with this release, but there's still a handful of improvements I'd like to make in future editions.
Further, I'd love to hear from all of you. This is my first time publishing an open source library and I'm sure there's plenty more I can learn from the experience. I'd love to see where you all run into friction using Sled, or any gripes you might have with the design.
Feel free to open up a discussion or pull request on the GitHub repository for this project. I'd love to hear from you all!