1use serde::{Deserialize, Serialize};
7
8pub const LEDS_PER_RING: usize = 12;
9pub type RingFrame = [Rgb; LEDS_PER_RING];
10
11#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Rgb {
14 pub r: u8,
15 pub g: u8,
16 pub b: u8,
17}
18
19impl Rgb {
20 pub const fn new(r: u8, g: u8, b: u8) -> Self {
21 Self { r, g, b }
22 }
23
24 pub const BLACK: Self = Self::new(0, 0, 0);
25
26 pub fn scale(self, factor: u8) -> Self {
28 Self {
29 r: ((self.r as u16 * factor as u16) / 255) as u8,
30 g: ((self.g as u16 * factor as u16) / 255) as u8,
31 b: ((self.b as u16 * factor as u16) / 255) as u8,
32 }
33 }
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum Renderer {
39 Off,
40 Solid(Rgb),
42 Fill(Rgb, u8),
44 Heatmap(u8),
46 Single(Rgb, u8),
48 Dots(Rgb, u8),
50}
51
52#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
54pub enum Modifier {
55 #[default]
57 Solid,
58 Glow,
60 Blink,
62 Pulse,
64 Rotate,
66 ColorCycle,
68}
69
70#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct RingAnimation {
73 pub renderer: Renderer,
74 pub modifier: Modifier,
75}
76
77impl Default for RingAnimation {
78 fn default() -> Self {
79 Self {
80 renderer: Renderer::Off,
81 modifier: Modifier::Solid,
82 }
83 }
84}
85
86impl RingAnimation {
87 pub const fn off() -> Self {
88 Self {
89 renderer: Renderer::Off,
90 modifier: Modifier::Solid,
91 }
92 }
93
94 pub const fn solid(color: Rgb) -> Self {
95 Self {
96 renderer: Renderer::Solid(color),
97 modifier: Modifier::Solid,
98 }
99 }
100
101 pub const fn glow(color: Rgb) -> Self {
102 Self {
103 renderer: Renderer::Solid(color),
104 modifier: Modifier::Glow,
105 }
106 }
107}
108
109#[derive(Copy, Clone)]
111pub struct LedRing {
112 pub animation: RingAnimation,
113 rotation: u8,
114}
115
116impl LedRing {
117 pub const fn new(rotation: u8) -> Self {
118 Self {
119 animation: RingAnimation {
120 renderer: Renderer::Off,
121 modifier: Modifier::Solid,
122 },
123 rotation,
124 }
125 }
126
127 pub fn set(&mut self, anim: RingAnimation) {
128 self.animation = anim;
129 }
130
131 pub fn render(&self, tick: u16) -> RingFrame {
133 let base = self.render_spatial();
134 self.apply_modifier(base, tick)
135 }
136
137 fn render_spatial(&self) -> RingFrame {
138 match self.animation.renderer {
139 Renderer::Off => [Rgb::BLACK; LEDS_PER_RING],
140 Renderer::Solid(c) => [c; LEDS_PER_RING],
141 Renderer::Fill(c, count) => {
142 let mut frame = [Rgb::BLACK; LEDS_PER_RING];
143 for i in 0..(count as usize).min(LEDS_PER_RING) {
144 frame[(self.rotation as usize + LEDS_PER_RING - i) % LEDS_PER_RING] = c;
145 }
146 frame
147 }
148 Renderer::Heatmap(fill) => {
149 const ARC_HOURS: [usize; 11] = [7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5];
150 let mut frame = [Rgb::BLACK; LEDS_PER_RING];
151 let lit = ((fill as usize) * ARC_HOURS.len() / 12).min(ARC_HOURS.len());
152 for i in 0..lit {
153 frame[CLOCK[ARC_HOURS[i]]] = heatmap_color(i, ARC_HOURS.len());
154 }
155 frame
156 }
157 Renderer::Single(c, pos) => {
158 let mut frame = [Rgb::BLACK; LEDS_PER_RING];
159 let p = (pos as usize) % 12;
160 frame[CLOCK[p]] = c;
161 frame[CLOCK[(p + 11) % 12]] = c; frame[CLOCK[(p + 1) % 12]] = c; frame
164 }
165 Renderer::Dots(c, count) => {
166 let mut frame = [Rgb::BLACK; LEDS_PER_RING];
167 let n = (count as usize).clamp(1, 12);
168 let spacing = LEDS_PER_RING / n;
169 for i in 0..n {
170 frame[CLOCK[(i * spacing) % 12]] = c;
171 }
172 frame
173 }
174 }
175 }
176
177 fn apply_modifier(&self, mut frame: RingFrame, tick: u16) -> RingFrame {
178 match self.animation.modifier {
179 Modifier::Solid => frame,
180 Modifier::Glow => {
181 for px in frame.iter_mut() {
182 *px = px.scale(32); }
184 frame
185 }
186 Modifier::Blink => {
187 if (tick / 25) % 2 == 1 {
189 [Rgb::BLACK; LEDS_PER_RING]
190 } else {
191 frame
192 }
193 }
194 Modifier::Pulse => {
195 let pos = tick % 50;
198 let factor = sine_u8(pos, 50);
199 for px in &mut frame {
200 *px = px.scale(factor);
201 }
202 frame
203 }
204 Modifier::Rotate => {
205 let shift = (tick / 25) as usize % LEDS_PER_RING;
207 let mut rotated = [Rgb::BLACK; LEDS_PER_RING];
208 for i in 0..LEDS_PER_RING {
209 rotated[(i + shift) % LEDS_PER_RING] = frame[i];
210 }
211 rotated
212 }
213 Modifier::ColorCycle => {
214 let hue = (tick * 3) as u8;
215 let color = hue_to_rgb(hue);
216 for px in frame.iter_mut() {
217 if *px != Rgb::BLACK {
218 *px = color;
219 }
220 }
221 frame
222 }
223 }
224 }
225}
226
227impl Default for LedRing {
228 fn default() -> Self {
229 Self::new(8)
230 }
231}
232
233const CLOCK: [usize; 12] = [
238 3, 2, 1, 0, 11, 10, 9, 8, 7, 6, 5, 4, ];
251
252fn heatmap_color(pos: usize, max: usize) -> Rgb {
254 if max <= 1 {
255 return Rgb::new(0, 0, 255);
256 }
257 let t = (pos * 255) / (max - 1);
258 if t < 128 {
259 let g = (t * 2) as u8;
260 Rgb::new(0, g, 255 - g)
261 } else {
262 let r = ((t - 128) * 2) as u8;
263 Rgb::new(r, 255 - r, 0)
264 }
265}
266
267fn hue_to_rgb(h: u8) -> Rgb {
269 let region = h / 43;
270 let remainder = (h % 43) * 6;
271 match region {
272 0 => Rgb::new(255, remainder, 0),
273 1 => Rgb::new(255 - remainder, 255, 0),
274 2 => Rgb::new(0, 255, remainder),
275 3 => Rgb::new(0, 255 - remainder, 255),
276 4 => Rgb::new(remainder, 0, 255),
277 _ => Rgb::new(255, 0, 255 - remainder),
278 }
279}
280
281fn sine_u8(phase: u16, period: u16) -> u8 {
283 const TABLE: [u8; 16] = [
284 0, 25, 50, 74, 98, 120, 142, 162, 180, 197, 213, 226, 237, 245, 251, 255,
285 ];
286 let pos = phase % period;
287 let half = period / 2;
288 let dist = half.abs_diff(pos);
290 let idx = ((half - dist) as usize * 15) / half.max(1) as usize;
292 TABLE[idx.min(15)]
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn off_renders_black() {
301 let ring = LedRing::default();
302 let frame = ring.render(0);
303 assert!(frame.iter().all(|px| *px == Rgb::BLACK));
304 }
305
306 #[test]
307 fn solid_renders_all_same() {
308 let mut ring = LedRing::default();
309 let c = Rgb::new(255, 0, 0);
310 ring.set(RingAnimation::solid(c));
311 let frame = ring.render(0);
312 assert!(frame.iter().all(|px| *px == c));
313 }
314
315 #[test]
316 fn glow_dims_uniformly() {
317 let mut ring = LedRing::default();
318 ring.set(RingAnimation::glow(Rgb::new(255, 255, 255)));
319 let frame = ring.render(0);
320 assert!(frame
322 .iter()
323 .all(|px| px.r == 32 && px.g == 32 && px.b == 32));
324 }
325
326 #[test]
327 fn blink_alternates() {
328 let mut ring = LedRing::default();
329 let c = Rgb::new(255, 0, 0);
330 ring.set(RingAnimation {
331 renderer: Renderer::Solid(c),
332 modifier: Modifier::Blink,
333 });
334 assert!(ring.render(0).iter().all(|px| *px == c));
336 assert!(ring.render(25).iter().all(|px| *px == Rgb::BLACK));
338 }
339
340 #[test]
341 fn wings_2_lights_opposing() {
342 let mut ring = LedRing::default();
343 let c = Rgb::new(0, 255, 0);
344 ring.set(RingAnimation {
345 renderer: Renderer::Dots(c, 2),
346 modifier: Modifier::Solid,
347 });
348 let frame = ring.render(0);
349 let lit = frame.iter().filter(|px| **px != Rgb::BLACK).count();
350 assert_eq!(lit, 2);
351 }
352
353 #[test]
354 fn wings_3_lights_three() {
355 let mut ring = LedRing::default();
356 ring.set(RingAnimation {
357 renderer: Renderer::Dots(Rgb::new(0, 0, 255), 3),
358 modifier: Modifier::Solid,
359 });
360 let frame = ring.render(0);
361 let lit = frame.iter().filter(|px| **px != Rgb::BLACK).count();
362 assert_eq!(lit, 3);
363 }
364
365 #[test]
366 fn heatmap_full_lights_11() {
367 let mut ring = LedRing::default();
368 ring.set(RingAnimation {
369 renderer: Renderer::Heatmap(12),
370 modifier: Modifier::Solid,
371 });
372 let frame = ring.render(0);
373 let lit = frame.iter().filter(|px| **px != Rgb::BLACK).count();
374 assert_eq!(lit, 11);
375 }
376
377 #[test]
378 fn rotate_shifts_single() {
379 let mut ring = LedRing::default();
380 let c = Rgb::new(255, 255, 0);
381 ring.set(RingAnimation {
382 renderer: Renderer::Single(c, 0),
383 modifier: Modifier::Rotate,
384 });
385 let f0 = ring.render(0);
386 let f1 = ring.render(25);
387 assert_ne!(f0, f1);
389 }
390
391 #[test]
392 fn pulse_starts_dim() {
393 let mut ring = LedRing::default();
394 ring.set(RingAnimation {
395 renderer: Renderer::Solid(Rgb::new(255, 255, 255)),
396 modifier: Modifier::Pulse,
397 });
398 let frame = ring.render(0);
400 assert!(frame[0].r < 30);
401 }
402
403 #[test]
404 #[test]
405 fn scale_zero_is_black() {
406 assert_eq!(Rgb::new(255, 128, 64).scale(0), Rgb::BLACK);
407 }
408
409 #[test]
410 fn scale_255_is_identity() {
411 let c = Rgb::new(200, 100, 50);
412 assert_eq!(c.scale(255), c);
413 }
414
415 #[test]
416 fn struct_sizes() {
417 assert_eq!(core::mem::size_of::<Rgb>(), 3);
418 assert_eq!(core::mem::size_of::<RingAnimation>(), 6);
419 assert!(core::mem::size_of::<LedRing>() <= 8);
420 }
421}