use crate::color::Color;
use crate::error::{Error, Result};
use crate::gamma::degamma;
use crate::types::{BootMode, Led, PatternLine, PlayState, StartupParams};
use std::time::Duration;
pub(crate) const REPORT_ID: u8 = 1;
pub(crate) const BUF_SIZE: usize = 9;
pub(crate) type Report = [u8; BUF_SIZE];
pub(crate) fn ticks(d: Duration) -> u16 {
(d.as_millis() / 10).min(u16::MAX as u128) as u16
}
fn millis(hi: u8, lo: u8) -> Duration {
Duration::from_millis(u16::from_be_bytes([hi, lo]) as u64 * 10)
}
fn chan(c: Color, gamma: bool) -> (u8, u8, u8) {
if gamma {
(degamma(c.r), degamma(c.g), degamma(c.b))
} else {
(c.r, c.g, c.b)
}
}
pub(crate) fn fade(c: Color, dur: Duration, led: Led, gamma: bool) -> Report {
let (r, g, b) = chan(c, gamma);
let [hi, lo] = ticks(dur).to_be_bytes();
[REPORT_ID, b'c', r, g, b, hi, lo, led.index(), 0]
}
pub(crate) fn set_now(c: Color, gamma: bool) -> Report {
let (r, g, b) = chan(c, gamma);
[REPORT_ID, b'n', r, g, b, 0, 0, 0, 0]
}
pub(crate) fn read_rgb_req(led: Led) -> Report {
[REPORT_ID, b'r', 0, 0, 0, 0, 0, led.index(), 0]
}
pub(crate) fn parse_read_rgb(buf: &Report) -> (Color, Duration) {
(Color::rgb(buf[2], buf[3], buf[4]), millis(buf[5], buf[6]))
}
pub(crate) fn version_req() -> Report {
[REPORT_ID, b'v', 0, 0, 0, 0, 0, 0, 0]
}
pub(crate) fn parse_version(buf: &Report) -> Result<u16> {
let digit = |b: u8| (b as char).to_digit(10).ok_or(Error::BadResponse);
Ok((digit(buf[3])? * 100 + digit(buf[4])?) as u16)
}
pub(crate) fn serverdown(on: bool, dur: Duration, stay: bool, start: u8, end: u8) -> Report {
let [hi, lo] = ticks(dur).to_be_bytes();
[REPORT_ID, b'D', on as u8, hi, lo, stay as u8, start, end, 0]
}
pub(crate) fn play(playing: bool, start: u8, end: u8, count: u8) -> Report {
[REPORT_ID, b'p', playing as u8, start, end, count, 0, 0, 0]
}
pub(crate) fn play_state_req() -> Report {
[REPORT_ID, b'S', 0, 0, 0, 0, 0, 0, 0]
}
pub(crate) fn parse_play_state(buf: &Report) -> PlayState {
PlayState {
playing: buf[2] != 0,
start: buf[3],
end: buf[4],
count: buf[5],
pos: buf[6],
}
}
pub(crate) fn write_pattern_line(c: Color, dur: Duration, pos: u8, gamma: bool) -> Report {
let (r, g, b) = chan(c, gamma);
let [hi, lo] = ticks(dur).to_be_bytes();
[REPORT_ID, b'P', r, g, b, hi, lo, pos, 0]
}
pub(crate) fn read_pattern_line_req(pos: u8) -> Report {
[REPORT_ID, b'R', 0, 0, 0, 0, 0, pos, 0]
}
pub(crate) fn parse_pattern_line(buf: &Report) -> PatternLine {
PatternLine {
color: Color::rgb(buf[2], buf[3], buf[4]),
duration: millis(buf[5], buf[6]),
led: match buf[7] {
0 => Led::All,
n => Led::N(n),
},
}
}
pub(crate) fn save_patterns() -> Report {
[REPORT_ID, b'W', 0xBE, 0xEF, 0xCA, 0xFE, 0x00, 0x00, 0]
}
pub(crate) fn set_led_n(led: Led) -> Report {
[REPORT_ID, b'l', led.index(), 0, 0, 0, 0, 0, 0]
}
pub(crate) fn startup_params_req() -> Report {
[REPORT_ID, b'b', 0, 0, 0, 0, 0, 0, 0]
}
pub(crate) fn parse_startup_params(buf: &Report) -> StartupParams {
StartupParams {
mode: BootMode::from_byte(buf[2]),
start: buf[3],
end: buf[4],
count: buf[5],
}
}
pub(crate) fn set_startup_params(p: StartupParams) -> Report {
[
REPORT_ID,
b'B',
p.mode.to_byte(),
p.start,
p.end,
p.count,
0,
0,
0,
]
}
#[cfg(test)]
mod tests {
use super::*;
const MS1000: Duration = Duration::from_millis(1000);
#[test]
fn fade_matches_c_lib() {
assert_eq!(
fade(Color::rgb(0xff, 0x00, 0x80), MS1000, Led::All, false),
[0x01, b'c', 0xff, 0x00, 0x80, 0x00, 0x64, 0x00, 0x00]
);
assert_eq!(
fade(Color::rgb(1, 2, 3), MS1000, Led::N(2), false),
[0x01, b'c', 1, 2, 3, 0x00, 0x64, 0x02, 0x00]
);
}
#[test]
fn set_now_matches_c_lib() {
assert_eq!(
set_now(Color::rgb(0x11, 0x22, 0x33), false),
[0x01, b'n', 0x11, 0x22, 0x33, 0, 0, 0, 0]
);
}
#[test]
fn gamma_changes_the_bytes_on_the_wire() {
let c = Color::rgb(128, 64, 32);
assert_eq!(&set_now(c, false)[2..5], &[128, 64, 32]);
assert_eq!(&set_now(c, true)[2..5], &[55, 11, 2]);
assert_eq!(&fade(c, MS1000, Led::All, true)[2..5], &[55, 11, 2]);
assert_eq!(
&write_pattern_line(c, MS1000, 0, true)[2..5],
&[55, 11, 2],
"'P' is degamma'd too (blink1-lib.c:400)"
);
}
#[test]
fn read_rgb_request_carries_the_led_in_byte_7() {
assert_eq!(read_rgb_req(Led::N(2)), [0x01, b'r', 0, 0, 0, 0, 0, 2, 0]);
assert_eq!(read_rgb_req(Led::All), [0x01, b'r', 0, 0, 0, 0, 0, 0, 0]);
}
#[test]
fn read_rgb_response_scales_millis_by_ten() {
let buf = [0x01, b'r', 0xde, 0xad, 0xbe, 0x00, 0x64, 0x00, 0x00];
let (c, d) = parse_read_rgb(&buf);
assert_eq!(c, Color::rgb(0xde, 0xad, 0xbe));
assert_eq!(d, Duration::from_millis(1000));
}
#[test]
fn version_digits_live_at_indices_three_and_four() {
let buf = [0x01, b'v', 0x5a, b'1', b'1', 0, 0, 0, 0];
assert_eq!(parse_version(&buf).unwrap(), 101);
let mk3 = [0x01, b'v', 0x00, b'3', b'0', 0, 0, 0, 0];
assert_eq!(parse_version(&mk3).unwrap(), 300);
let junk = [0x01, b'v', 0, 0xff, 0xff, 0, 0, 0, 0];
assert!(matches!(parse_version(&junk), Err(Error::BadResponse)));
}
#[test]
fn serverdown_matches_c_lib() {
assert_eq!(
serverdown(true, Duration::from_secs(30), false, 0, 0),
[0x01, b'D', 1, 0x0b, 0xb8, 0, 0, 0, 0]
);
assert_eq!(
serverdown(false, Duration::ZERO, false, 0, 0),
[0x01, b'D', 0, 0, 0, 0, 0, 0, 0]
);
assert_eq!(
serverdown(true, Duration::from_secs(1), true, 3, 7),
[0x01, b'D', 1, 0x00, 0x64, 1, 3, 7, 0]
);
}
#[test]
fn play_matches_c_lib() {
assert_eq!(play(true, 0, 0, 0), [0x01, b'p', 1, 0, 0, 0, 0, 0, 0]);
assert_eq!(play(false, 0, 0, 0), [0x01, b'p', 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(play(true, 2, 5, 3), [0x01, b'p', 1, 2, 5, 3, 0, 0, 0]);
}
#[test]
fn play_state_parses_all_five_fields() {
let buf = [0x01, b'S', 1, 2, 5, 3, 4, 0, 0];
assert_eq!(
parse_play_state(&buf),
PlayState {
playing: true,
start: 2,
end: 5,
count: 3,
pos: 4
}
);
}
#[test]
fn pattern_line_round_trips_through_the_wire_format() {
assert_eq!(
write_pattern_line(Color::rgb(9, 8, 7), MS1000, 5, false),
[0x01, b'P', 9, 8, 7, 0x00, 0x64, 5, 0]
);
assert_eq!(read_pattern_line_req(5), [0x01, b'R', 0, 0, 0, 0, 0, 5, 0]);
let resp = [0x01, b'R', 9, 8, 7, 0x00, 0x64, 2, 0];
assert_eq!(
parse_pattern_line(&resp),
PatternLine {
color: Color::rgb(9, 8, 7),
duration: MS1000,
led: Led::N(2),
}
);
}
#[test]
fn save_patterns_sends_the_magic() {
assert_eq!(
save_patterns(),
[0x01, b'W', 0xBE, 0xEF, 0xCA, 0xFE, 0x00, 0x00, 0]
);
}
#[test]
fn set_led_n_is_zero_filled() {
assert_eq!(set_led_n(Led::N(2)), [0x01, b'l', 2, 0, 0, 0, 0, 0, 0]);
assert_eq!(set_led_n(Led::All), [0x01, b'l', 0, 0, 0, 0, 0, 0, 0]);
}
#[test]
fn startup_params_match_c_lib() {
assert_eq!(startup_params_req(), [0x01, b'b', 0, 0, 0, 0, 0, 0, 0]);
let p = StartupParams {
mode: BootMode::Play,
start: 1,
end: 4,
count: 0,
};
assert_eq!(set_startup_params(p), [0x01, b'B', 1, 1, 4, 0, 0, 0, 0]);
let resp = [0x01, b'b', 2, 1, 4, 7, 0, 0, 0];
assert_eq!(
parse_startup_params(&resp),
StartupParams {
mode: BootMode::Off,
start: 1,
end: 4,
count: 7
}
);
}
#[test]
fn every_report_carries_report_id_one() {
for r in [
fade(Color::RED, MS1000, Led::All, false),
set_now(Color::RED, false),
read_rgb_req(Led::All),
version_req(),
serverdown(true, MS1000, false, 0, 0),
play(true, 0, 0, 0),
play_state_req(),
write_pattern_line(Color::RED, MS1000, 0, false),
read_pattern_line_req(0),
save_patterns(),
set_led_n(Led::All),
startup_params_req(),
set_startup_params(StartupParams {
mode: BootMode::Normal,
start: 0,
end: 0,
count: 0,
}),
] {
assert_eq!(r[0], REPORT_ID, "report id missing from {r:?}");
assert_eq!(r.len(), BUF_SIZE);
}
}
#[test]
fn durations_below_one_tick_become_instant() {
assert_eq!(ticks(Duration::ZERO), 0);
assert_eq!(ticks(Duration::from_millis(9)), 0);
assert_eq!(ticks(Duration::from_millis(10)), 1);
assert_eq!(ticks(Duration::from_millis(19)), 1);
}
#[test]
fn durations_past_the_field_width_saturate() {
assert_eq!(ticks(Duration::from_millis(655_350)), u16::MAX);
assert_eq!(ticks(Duration::from_secs(3600)), u16::MAX);
assert_eq!(
fade(Color::RED, Duration::from_secs(3600), Led::All, false)[5..7],
[0xff, 0xff]
);
}
}