use super::{write_header, HEADER_LEN};
pub const MAX_PIXELS_PER_PACKET: usize = 497;
#[must_use]
pub fn sync(brightness: u8) -> [u8; 112] {
sync_gains(brightness, [brightness; 3])
}
#[must_use]
pub fn sync_gains(brightness: u8, gains: [u8; 3]) -> [u8; 112] {
let mut f = [0u8; 112];
write_header(&mut f, [0x01, 0x07]);
f[35] = brightness;
f[36] = 0x05;
f[38..41].copy_from_slice(&gains);
f
}
#[must_use]
pub fn brightness(b: u8) -> [u8; 77] {
let mut f = [0u8; 77];
write_header(&mut f, [0x0a, b]);
f[14] = b;
f[15] = b;
f[16] = 0xff;
f
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ColorOrder {
Rgb,
Bgr,
Grb,
}
impl ColorOrder {
const NAMES: [(&'static str, Self); 3] = [("rgb", Self::Rgb), ("bgr", Self::Bgr), ("grb", Self::Grb)];
const fn permutation(self) -> [usize; 3] {
match self {
Self::Rgb => [0, 1, 2],
Self::Bgr => [2, 1, 0],
Self::Grb => [1, 0, 2],
}
}
}
impl std::str::FromStr for ColorOrder {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::NAMES
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(s))
.map(|&(_, order)| order)
.ok_or_else(|| format!("unknown color order {s:?} (rgb|bgr|grb)"))
}
}
const ROW_PIXELS_AT: usize = HEADER_LEN + 7;
#[must_use]
pub fn pixel_row(row: u16, pixel_offset: u16, rgb: &[[u8; 3]], order: ColorOrder) -> Vec<u8> {
let mut f = Vec::new();
pixel_row_into(&mut f, row, pixel_offset, rgb, order);
f
}
pub fn pixel_row_into(buf: &mut Vec<u8>, row: u16, pixel_offset: u16, rgb: &[[u8; 3]], order: ColorOrder) {
let count = rgb.len() as u16;
buf.clear();
buf.resize(ROW_PIXELS_AT + rgb.len() * 3, 0);
write_header(buf, [0x55, (row >> 8) as u8]);
buf[14] = (row & 0xff) as u8;
buf[15..17].copy_from_slice(&pixel_offset.to_be_bytes());
buf[17..19].copy_from_slice(&count.to_be_bytes());
buf[19] = 0x08;
buf[20] = 0x88;
let [a, b, c] = order.permutation();
let (dst_px, _) = buf[ROW_PIXELS_AT..].as_chunks_mut::<3>();
for (dst, px) in dst_px.iter_mut().zip(rgb) {
dst[0] = px[a];
dst[1] = px[b];
dst[2] = px[c];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pixel_rows_follow_the_fpp_layout() {
let px = [[1u8, 2, 3], [4, 5, 6]];
let f = pixel_row(0x0102, 5, &px, ColorOrder::Rgb);
assert_eq!(f[12], 0x55, "type byte");
assert_eq!(&f[13..15], &[0x01, 0x02], "row u16 BE starting at offset 13");
assert_eq!(&f[15..17], &5u16.to_be_bytes());
assert_eq!(&f[17..19], &2u16.to_be_bytes());
assert_eq!(&f[19..21], &[0x08, 0x88]);
assert_eq!(&f[21..27], &[1, 2, 3, 4, 5, 6]);
}
#[test]
fn colour_order_reorders_the_channels() {
let px = [[1u8, 2, 3]];
let bgr = pixel_row(0, 0, &px, ColorOrder::Bgr);
assert_eq!(&bgr[21..24], &[3, 2, 1]);
let grb = pixel_row(0, 0, &px, ColorOrder::Grb);
assert_eq!(&grb[21..24], &[2, 1, 3]);
}
#[test]
fn a_reused_buffer_gives_the_same_frame() {
let px: Vec<[u8; 3]> = (0..MAX_PIXELS_PER_PACKET as u16)
.map(|i| [i as u8, (i >> 3) as u8, !(i as u8)])
.collect();
let mut buf = vec![0xeeu8; 4096];
for order in [ColorOrder::Rgb, ColorOrder::Bgr, ColorOrder::Grb] {
pixel_row_into(&mut buf, 0x0203, 497, &px, order);
assert_eq!(buf, pixel_row(0x0203, 497, &px, order));
assert_eq!(buf.len(), 21 + 3 * MAX_PIXELS_PER_PACKET);
}
}
#[test]
fn colour_order_parses_case_insensitively() {
assert_eq!("BGR".parse::<ColorOrder>(), Ok(ColorOrder::Bgr));
assert_eq!("rgb".parse::<ColorOrder>(), Ok(ColorOrder::Rgb));
assert_eq!("grb".parse::<ColorOrder>(), Ok(ColorOrder::Grb));
assert!("rbg".parse::<ColorOrder>().is_err());
}
#[test]
fn sync_frame_matches_fpp_byte_for_byte() {
let f = sync(0x7f);
assert_eq!(f.len(), 112);
assert_eq!(&f[12..14], &[0x01, 0x07]);
assert_eq!(f[35], 0x7f);
assert_eq!(f[36], 0x05);
assert_eq!(&f[38..41], &[0x7f; 3]);
}
#[test]
fn sync_gains_differ_from_sync_only_at_the_gain_bytes() {
let f = sync_gains(0x7f, [10, 20, 30]);
let plain = sync(0x7f);
assert_eq!(&f[38..41], &[10, 20, 30]);
assert_eq!(&f[..38], &plain[..38]);
assert_eq!(&f[41..], &plain[41..]);
assert_eq!(sync_gains(0x40, [0x40; 3]), sync(0x40));
}
#[test]
fn brightness_frame_matches_fpp() {
let f = brightness(0x40);
assert_eq!(f.len(), 77);
assert_eq!(f[12], 0x0a);
assert_eq!(&f[13..17], &[0x40, 0x40, 0x40, 0xff]);
}
}