1#![no_std]
19
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Rgb {
25 pub r: u8,
26 pub g: u8,
27 pub b: u8,
28}
29
30impl Rgb {
31 pub const fn new(r: u8, g: u8, b: u8) -> Self {
32 Self { r, g, b }
33 }
34
35 pub const BLACK: Self = Self::new(0, 0, 0);
36
37 pub fn scale(self, factor: u8) -> Self {
39 Self {
40 r: ((self.r as u16 * factor as u16) / 255) as u8,
41 g: ((self.g as u16 * factor as u16) / 255) as u8,
42 b: ((self.b as u16 * factor as u16) / 255) as u8,
43 }
44 }
45}
46
47#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub enum Renderer {
50 Off,
51 Solid(Rgb),
53 Fill(Rgb, u8),
55 Heatmap(u8),
57 Single(Rgb, u8),
59 Dots(Rgb, u8),
61}
62
63#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub enum Modifier {
66 #[default]
68 Solid,
69 Glow,
71 Blink,
73 Pulse,
75 Rotate,
77 ColorCycle,
79}
80
81#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct RingAnimation {
84 pub renderer: Renderer,
85 pub modifier: Modifier,
86}
87
88impl Default for RingAnimation {
89 fn default() -> Self {
90 Self {
91 renderer: Renderer::Off,
92 modifier: Modifier::Solid,
93 }
94 }
95}
96
97impl RingAnimation {
98 pub const fn off() -> Self {
99 Self {
100 renderer: Renderer::Off,
101 modifier: Modifier::Solid,
102 }
103 }
104
105 pub const fn solid(color: Rgb) -> Self {
106 Self {
107 renderer: Renderer::Solid(color),
108 modifier: Modifier::Solid,
109 }
110 }
111
112 pub const fn glow(color: Rgb) -> Self {
113 Self {
114 renderer: Renderer::Solid(color),
115 modifier: Modifier::Glow,
116 }
117 }
118}
119
120pub const IDENTITY_MAP: [usize; 24] = [
122 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
123];
124
125#[derive(Copy, Clone)]
130pub struct LedRing<const N: usize = 12> {
131 pub animation: RingAnimation,
132 rotation: u8,
133 clock_map: [usize; N],
134}
135
136impl<const N: usize> LedRing<N> {
137 pub const fn new(clock_map: &[usize; N]) -> Self {
141 Self {
142 animation: RingAnimation::off(),
143 rotation: 0,
144 clock_map: *clock_map,
145 }
146 }
147
148 pub const fn with_rotation(clock_map: &[usize; N], rotation: u8) -> Self {
150 Self {
151 animation: RingAnimation::off(),
152 rotation,
153 clock_map: *clock_map,
154 }
155 }
156
157 pub fn set(&mut self, anim: RingAnimation) {
158 self.animation = anim;
159 }
160
161 pub fn render(&self, tick: u16) -> [Rgb; N] {
163 let base = self.render_spatial();
164 self.apply_modifier(base, tick)
165 }
166
167 fn render_spatial(&self) -> [Rgb; N] {
168 let mut frame = [Rgb::BLACK; N];
169 match self.animation.renderer {
170 Renderer::Off => {}
171 Renderer::Solid(c) => {
172 for px in &mut frame {
173 *px = c;
174 }
175 }
176 Renderer::Fill(c, count) => {
177 for i in 0..(count as usize).min(N) {
178 let pos = (self.rotation as usize + N - i) % N;
179 frame[self.clock_map[pos]] = c;
180 }
181 }
182 Renderer::Heatmap(fill) => {
183 let arc_len = N - 1;
185 let lit = ((fill as usize) * arc_len / N.max(1)).min(arc_len);
186 for i in 0..lit {
187 let hour = (N * 7 / 12 + i) % N;
189 frame[self.clock_map[hour]] = heatmap_color(i, arc_len);
190 }
191 }
192 Renderer::Single(c, pos) => {
193 let p = (pos as usize) % N;
194 frame[self.clock_map[p]] = c;
195 if N > 2 {
197 frame[self.clock_map[(p + N - 1) % N]] = c;
198 frame[self.clock_map[(p + 1) % N]] = c;
199 }
200 }
201 Renderer::Dots(c, count) => {
202 let n = (count as usize).clamp(1, N);
203 let spacing = N / n;
204 for i in 0..n {
205 frame[self.clock_map[(i * spacing) % N]] = c;
206 }
207 }
208 }
209 frame
210 }
211
212 fn apply_modifier(&self, mut frame: [Rgb; N], tick: u16) -> [Rgb; N] {
213 match self.animation.modifier {
214 Modifier::Solid => frame,
215 Modifier::Glow => {
216 for px in frame.iter_mut() {
217 *px = px.scale(32);
218 }
219 frame
220 }
221 Modifier::Blink => {
222 if (tick / 25) % 2 == 1 {
223 [Rgb::BLACK; N]
224 } else {
225 frame
226 }
227 }
228 Modifier::Pulse => {
229 let factor = sine_u8(tick % 50, 50);
230 for px in &mut frame {
231 *px = px.scale(factor);
232 }
233 frame
234 }
235 Modifier::Rotate => {
236 let shift = (tick / 25) as usize % N;
237 let mut rotated = [Rgb::BLACK; N];
238 for i in 0..N {
239 rotated[(i + shift) % N] = frame[i];
240 }
241 rotated
242 }
243 Modifier::ColorCycle => {
244 let color = hue_to_rgb((tick * 3) as u8);
245 for px in frame.iter_mut() {
246 if *px != Rgb::BLACK {
247 *px = color;
248 }
249 }
250 frame
251 }
252 }
253 }
254}
255
256impl Default for LedRing<12> {
257 fn default() -> Self {
258 Self::with_rotation(&PEDALBOARD_CLOCK_MAP, 8)
259 }
260}
261
262pub const PEDALBOARD_CLOCK_MAP: [usize; 12] = [
265 3, 2, 1, 0, 11, 10, 9, 8, 7, 6, 5, 4, ];
278
279fn heatmap_color(pos: usize, max: usize) -> Rgb {
283 if max <= 1 {
284 return Rgb::new(0, 0, 255);
285 }
286 let t = (pos * 255) / (max - 1);
287 if t < 128 {
288 let g = (t * 2) as u8;
289 Rgb::new(0, g, 255 - g)
290 } else {
291 let r = ((t - 128) * 2) as u8;
292 Rgb::new(r, 255 - r, 0)
293 }
294}
295
296pub fn hue_to_rgb(h: u8) -> Rgb {
298 let region = h / 43;
299 let remainder = (h % 43) * 6;
300 match region {
301 0 => Rgb::new(255, remainder, 0),
302 1 => Rgb::new(255 - remainder, 255, 0),
303 2 => Rgb::new(0, 255, remainder),
304 3 => Rgb::new(0, 255 - remainder, 255),
305 4 => Rgb::new(remainder, 0, 255),
306 _ => Rgb::new(255, 0, 255 - remainder),
307 }
308}
309
310fn sine_u8(phase: u16, period: u16) -> u8 {
312 const TABLE: [u8; 16] = [
313 0, 25, 50, 74, 98, 120, 142, 162, 180, 197, 213, 226, 237, 245, 251, 255,
314 ];
315 let pos = phase % period;
316 let half = period / 2;
317 let dist = half.abs_diff(pos);
318 let idx = ((half - dist) as usize * 15) / half.max(1) as usize;
319 TABLE[idx.min(15)]
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 fn ring12() -> LedRing<12> {
327 LedRing::with_rotation(&PEDALBOARD_CLOCK_MAP, 8)
328 }
329
330 #[test]
331 fn off_renders_black() {
332 let ring = ring12();
333 let frame = ring.render(0);
334 assert!(frame.iter().all(|px| *px == Rgb::BLACK));
335 }
336
337 #[test]
338 fn solid_renders_all_same() {
339 let mut ring = ring12();
340 let c = Rgb::new(255, 0, 0);
341 ring.set(RingAnimation::solid(c));
342 let frame = ring.render(0);
343 assert!(frame.iter().all(|px| *px == c));
344 }
345
346 #[test]
347 fn glow_dims_uniformly() {
348 let mut ring = ring12();
349 ring.set(RingAnimation::glow(Rgb::new(255, 255, 255)));
350 let frame = ring.render(0);
351 assert!(frame.iter().all(|px| px.r == 32 && px.g == 32 && px.b == 32));
352 }
353
354 #[test]
355 fn blink_alternates() {
356 let mut ring = ring12();
357 let c = Rgb::new(255, 0, 0);
358 ring.set(RingAnimation {
359 renderer: Renderer::Solid(c),
360 modifier: Modifier::Blink,
361 });
362 assert!(ring.render(0).iter().all(|px| *px == c));
363 assert!(ring.render(25).iter().all(|px| *px == Rgb::BLACK));
364 }
365
366 #[test]
367 fn dots_2_lights_opposing() {
368 let mut ring = ring12();
369 let c = Rgb::new(0, 255, 0);
370 ring.set(RingAnimation {
371 renderer: Renderer::Dots(c, 2),
372 modifier: Modifier::Solid,
373 });
374 let frame = ring.render(0);
375 let lit = frame.iter().filter(|px| **px != Rgb::BLACK).count();
376 assert_eq!(lit, 2);
377 }
378
379 #[test]
380 fn heatmap_full() {
381 let mut ring = ring12();
382 ring.set(RingAnimation {
383 renderer: Renderer::Heatmap(12),
384 modifier: Modifier::Solid,
385 });
386 let frame = ring.render(0);
387 let lit = frame.iter().filter(|px| **px != Rgb::BLACK).count();
388 assert_eq!(lit, 11);
389 }
390
391 #[test]
392 fn rotate_shifts() {
393 let mut ring = ring12();
394 ring.set(RingAnimation {
395 renderer: Renderer::Single(Rgb::new(255, 255, 0), 0),
396 modifier: Modifier::Rotate,
397 });
398 let f0 = ring.render(0);
399 let f1 = ring.render(25);
400 assert_ne!(f0, f1);
401 }
402
403 #[test]
404 fn pulse_starts_dim() {
405 let mut ring = ring12();
406 ring.set(RingAnimation {
407 renderer: Renderer::Solid(Rgb::new(255, 255, 255)),
408 modifier: Modifier::Pulse,
409 });
410 let frame = ring.render(0);
411 assert!(frame[0].r < 30);
412 }
413
414 #[test]
415 fn scale_zero_is_black() {
416 assert_eq!(Rgb::new(255, 128, 64).scale(0), Rgb::BLACK);
417 }
418
419 #[test]
420 fn scale_255_is_identity() {
421 let c = Rgb::new(200, 100, 50);
422 assert_eq!(c.scale(255), c);
423 }
424
425 #[test]
426 fn struct_sizes() {
427 assert_eq!(core::mem::size_of::<Rgb>(), 3);
428 assert_eq!(core::mem::size_of::<RingAnimation>(), 6);
429 assert!(core::mem::size_of::<LedRing<12>>() <= 112);
431 }
432
433 #[test]
434 fn generic_ring_16_leds() {
435 let map: [usize; 16] = core::array::from_fn(|i| i);
436 let mut ring = LedRing::<16>::new(&map);
437 ring.set(RingAnimation::solid(Rgb::new(0, 0, 255)));
438 let frame = ring.render(0);
439 assert_eq!(frame.len(), 16);
440 assert!(frame.iter().all(|px| *px == Rgb::new(0, 0, 255)));
441 }
442
443 #[test]
444 fn generic_ring_24_leds() {
445 let map: [usize; 24] = core::array::from_fn(|i| i);
446 let mut ring = LedRing::<24>::new(&map);
447 ring.set(RingAnimation {
448 renderer: Renderer::Fill(Rgb::new(255, 0, 0), 12),
449 modifier: Modifier::Solid,
450 });
451 let frame = ring.render(0);
452 let lit = frame.iter().filter(|px| **px != Rgb::BLACK).count();
453 assert_eq!(lit, 12);
454 }
455}