Skip to main content

colorlight/
pixel.rs

1//! Pixel rows, the latch (sync) frame, and brightness: the frames sent every
2//! refresh.
3//!
4//! Layouts follow FPP's ColorLight-5a-75 output, byte-checked against the
5//! vendor DLL (`docs/pixel-protocol.md` §1.3). The type is one byte at frame
6//! offset 12 and data starts at 13, in the second EtherType byte; data
7//! shifted by one byte made the panel a 5 Hz strobe on the bench.
8
9use super::{write_header, HEADER_LEN};
10
11/// FPP's `CL_MAX_PIXL_PER_PACKET`, also hard-coded in the vendor DLL.
12pub const MAX_PIXELS_PER_PACKET: usize = 497;
13
14/// Latch frame: type 0x01, first data byte 0x07.
15///
16/// Latches the rows sent since the last one; master brightness at offset 35,
17/// three channel gains at 38..41. Callers send three per refresh: one never
18/// starts the display, two decay into noise, three hold (`docs/rendering.md`).
19#[must_use]
20pub fn sync(brightness: u8) -> [u8; 112] {
21    // The vendor derives the three gains from its brightness block by an
22    // unresolved rule (docs/pixel-protocol.md §2.2); they follow the master here.
23    sync_gains(brightness, [brightness; 3])
24}
25
26/// [`sync`] with the three channel gains at 38..41 given separately, in the
27/// order the vendor writes its brightness block.
28#[must_use]
29pub fn sync_gains(brightness: u8, gains: [u8; 3]) -> [u8; 112] {
30    let mut f = [0u8; 112];
31    write_header(&mut f, [0x01, 0x07]);
32    f[35] = brightness;
33    f[36] = 0x05;
34    f[38..41].copy_from_slice(&gains);
35    f
36}
37
38/// Brightness frame: type 0x0a, data `[b, b, b, 0xff]` from offset 13.
39#[must_use]
40pub fn brightness(b: u8) -> [u8; 77] {
41    let mut f = [0u8; 77];
42    write_header(&mut f, [0x0a, b]);
43    f[14] = b;
44    f[15] = b;
45    f[16] = 0xff;
46    f
47}
48
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub enum ColorOrder {
51    Rgb,
52    Bgr,
53    Grb,
54}
55
56impl ColorOrder {
57    const NAMES: [(&'static str, Self); 3] = [("rgb", Self::Rgb), ("bgr", Self::Bgr), ("grb", Self::Grb)];
58
59    /// Index into an `[r, g, b]` pixel for each wire channel.
60    const fn permutation(self) -> [usize; 3] {
61        match self {
62            Self::Rgb => [0, 1, 2],
63            Self::Bgr => [2, 1, 0],
64            Self::Grb => [1, 0, 2],
65        }
66    }
67}
68
69impl std::str::FromStr for ColorOrder {
70    type Err = String;
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        Self::NAMES
73            .iter()
74            .find(|(name, _)| name.eq_ignore_ascii_case(s))
75            .map(|&(_, order)| order)
76            .ok_or_else(|| format!("unknown color order {s:?} (rgb|bgr|grb)"))
77    }
78}
79
80const ROW_PIXELS_AT: usize = HEADER_LEN + 7;
81
82/// Pixel row frame: type 0x55, then from offset 13: row u16, pixel offset
83/// u16, count u16 (all BE), 0x08, 0x88, pixels.
84#[must_use]
85pub fn pixel_row(row: u16, pixel_offset: u16, rgb: &[[u8; 3]], order: ColorOrder) -> Vec<u8> {
86    let mut f = Vec::new();
87    pixel_row_into(&mut f, row, pixel_offset, rgb, order);
88    f
89}
90
91/// [`pixel_row`] into a reused buffer (cleared first), so a refresh loop
92/// allocates nothing per packet.
93pub fn pixel_row_into(buf: &mut Vec<u8>, row: u16, pixel_offset: u16, rgb: &[[u8; 3]], order: ColorOrder) {
94    let count = rgb.len() as u16;
95    buf.clear();
96    buf.resize(ROW_PIXELS_AT + rgb.len() * 3, 0);
97    write_header(buf, [0x55, (row >> 8) as u8]);
98    buf[14] = (row & 0xff) as u8;
99    buf[15..17].copy_from_slice(&pixel_offset.to_be_bytes());
100    buf[17..19].copy_from_slice(&count.to_be_bytes());
101    buf[19] = 0x08;
102    buf[20] = 0x88;
103    let [a, b, c] = order.permutation();
104    let (dst_px, _) = buf[ROW_PIXELS_AT..].as_chunks_mut::<3>();
105    for (dst, px) in dst_px.iter_mut().zip(rgb) {
106        dst[0] = px[a];
107        dst[1] = px[b];
108        dst[2] = px[c];
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn pixel_rows_follow_the_fpp_layout() {
118        let px = [[1u8, 2, 3], [4, 5, 6]];
119        let f = pixel_row(0x0102, 5, &px, ColorOrder::Rgb);
120        assert_eq!(f[12], 0x55, "type byte");
121        assert_eq!(&f[13..15], &[0x01, 0x02], "row u16 BE starting at offset 13");
122        assert_eq!(&f[15..17], &5u16.to_be_bytes());
123        assert_eq!(&f[17..19], &2u16.to_be_bytes());
124        assert_eq!(&f[19..21], &[0x08, 0x88]);
125        assert_eq!(&f[21..27], &[1, 2, 3, 4, 5, 6]);
126    }
127
128    #[test]
129    fn colour_order_reorders_the_channels() {
130        let px = [[1u8, 2, 3]];
131        let bgr = pixel_row(0, 0, &px, ColorOrder::Bgr);
132        assert_eq!(&bgr[21..24], &[3, 2, 1]);
133        let grb = pixel_row(0, 0, &px, ColorOrder::Grb);
134        assert_eq!(&grb[21..24], &[2, 1, 3]);
135    }
136
137    #[test]
138    fn a_reused_buffer_gives_the_same_frame() {
139        let px: Vec<[u8; 3]> = (0..MAX_PIXELS_PER_PACKET as u16)
140            .map(|i| [i as u8, (i >> 3) as u8, !(i as u8)])
141            .collect();
142        let mut buf = vec![0xeeu8; 4096];
143        for order in [ColorOrder::Rgb, ColorOrder::Bgr, ColorOrder::Grb] {
144            pixel_row_into(&mut buf, 0x0203, 497, &px, order);
145            assert_eq!(buf, pixel_row(0x0203, 497, &px, order));
146            assert_eq!(buf.len(), 21 + 3 * MAX_PIXELS_PER_PACKET);
147        }
148    }
149
150    #[test]
151    fn colour_order_parses_case_insensitively() {
152        assert_eq!("BGR".parse::<ColorOrder>(), Ok(ColorOrder::Bgr));
153        assert_eq!("rgb".parse::<ColorOrder>(), Ok(ColorOrder::Rgb));
154        assert_eq!("grb".parse::<ColorOrder>(), Ok(ColorOrder::Grb));
155        assert!("rbg".parse::<ColorOrder>().is_err());
156    }
157
158    #[test]
159    fn sync_frame_matches_fpp_byte_for_byte() {
160        // FPP: brightness at data[22] and data[25..28], 0x05 at data[23].
161        let f = sync(0x7f);
162        assert_eq!(f.len(), 112);
163        assert_eq!(&f[12..14], &[0x01, 0x07]);
164        assert_eq!(f[35], 0x7f);
165        assert_eq!(f[36], 0x05);
166        assert_eq!(&f[38..41], &[0x7f; 3]);
167    }
168
169    #[test]
170    fn sync_gains_differ_from_sync_only_at_the_gain_bytes() {
171        let f = sync_gains(0x7f, [10, 20, 30]);
172        let plain = sync(0x7f);
173        assert_eq!(&f[38..41], &[10, 20, 30]);
174        assert_eq!(&f[..38], &plain[..38]);
175        assert_eq!(&f[41..], &plain[41..]);
176        assert_eq!(sync_gains(0x40, [0x40; 3]), sync(0x40));
177    }
178
179    #[test]
180    fn brightness_frame_matches_fpp() {
181        let f = brightness(0x40);
182        assert_eq!(f.len(), 77);
183        assert_eq!(f[12], 0x0a);
184        assert_eq!(&f[13..17], &[0x40, 0x40, 0x40, 0xff]);
185    }
186}