use std::assert_matches;
use super::event::{IlluminationEvent, decode_event};
use super::types::{
BrightnessClampedSource, ControlCapabilities, ControlInfo, IlluminationState, LevelConfig,
SetLevels,
};
use crate::protocol::v20::{ErrorType, Hidpp20Error};
#[test]
fn parses_control_info() {
let mut payload = [0; 16];
payload[0] = 0b0000_1011; payload[1..3].copy_from_slice(&100u16.to_be_bytes());
payload[3..5].copy_from_slice(&1000u16.to_be_bytes());
payload[5..7].copy_from_slice(&10u16.to_be_bytes());
payload[7] = 0x05;
let info = ControlInfo::from_payload(&payload);
assert!(info.capabilities.contains(ControlCapabilities::HAS_EVENTS));
assert!(
info.capabilities
.contains(ControlCapabilities::HAS_LINEAR_LEVELS)
);
assert!(
info.capabilities
.contains(ControlCapabilities::HAS_DYNAMIC_MAXIMUM)
);
assert!(
!info
.capabilities
.contains(ControlCapabilities::HAS_NON_LINEAR_LEVELS)
);
assert_eq!(info.min, 100);
assert_eq!(info.max, 1000);
assert_eq!(info.resolution, 10);
assert_eq!(info.max_levels, 5);
}
#[test]
fn parses_linear_levels() {
let mut payload = [0; 16];
payload[0] = 1; payload[2..4].copy_from_slice(&100u16.to_be_bytes());
payload[4..6].copy_from_slice(&500u16.to_be_bytes());
payload[6..8].copy_from_slice(&50u16.to_be_bytes());
assert_eq!(
LevelConfig::from_payload(&payload),
LevelConfig::Linear {
min: 100,
max: 500,
step: 50
}
);
}
#[test]
fn parses_non_linear_levels() {
let mut payload = [0; 16];
payload[0] = 3 << 5;
payload[1] = (1 << 4) | 6;
payload[2..4].copy_from_slice(&200u16.to_be_bytes());
payload[4..6].copy_from_slice(&400u16.to_be_bytes());
payload[6..8].copy_from_slice(&800u16.to_be_bytes());
assert_eq!(
LevelConfig::from_payload(&payload),
LevelConfig::NonLinear {
start_index: 1,
level_count: 6,
values: vec![200, 400, 800],
}
);
}
#[test]
fn encodes_reset_levels() {
let [payload] = SetLevels::Reset.to_payloads().unwrap()[..] else {
panic!("reset is one request");
};
assert_eq!(payload[0], 1 << 1);
assert!(payload[1..].iter().all(|&b| b == 0));
}
#[test]
fn encodes_linear_levels() {
let pages = SetLevels::Linear {
min: 100,
max: 500,
step: 50,
}
.to_payloads()
.unwrap();
let [payload] = pages[..] else {
panic!("linear levels are one request");
};
assert_eq!(payload[0], 1);
assert_eq!(u16::from_be_bytes([payload[2], payload[3]]), 100);
assert_eq!(u16::from_be_bytes([payload[4], payload[5]]), 500);
assert_eq!(u16::from_be_bytes([payload[6], payload[7]]), 50);
}
#[test]
fn encodes_non_linear_levels() {
let pages = SetLevels::NonLinear {
values: vec![100, 200],
}
.to_payloads()
.unwrap();
let [payload] = pages[..] else {
panic!("two levels fit in one request");
};
assert_eq!(payload[0], 2 << 5); assert_eq!(payload[1], 2); assert_eq!(u16::from_be_bytes([payload[2], payload[3]]), 100);
assert_eq!(u16::from_be_bytes([payload[4], payload[5]]), 200);
}
#[test]
fn pages_a_full_non_linear_table_with_one_level_count() {
let values: Vec<u16> = (1..=15).map(|i| i * 100).collect();
let pages = SetLevels::NonLinear {
values: values.clone(),
}
.to_payloads()
.unwrap();
let headers: Vec<[u8; 2]> = pages.iter().map(|page| [page[0], page[1]]).collect();
assert_eq!(
headers,
[
[7 << 5, 15],
[7 << 5, (7 << 4) | 15],
[1 << 5, (14 << 4) | 15]
]
);
assert!(pages[2][4..].iter().all(|&b| b == 0));
let mut read_back = Vec::new();
for (page, expected_start) in pages.iter().zip([0, 7, 14]) {
let LevelConfig::NonLinear {
start_index,
level_count,
values,
} = LevelConfig::from_payload(page)
else {
panic!("non-linear pages decode as non-linear");
};
assert_eq!((start_index, level_count), (expected_start, 15));
read_back.extend(values);
}
assert_eq!(read_back, values);
}
#[test]
fn rejects_non_linear_tables_the_count_field_cannot_hold() {
for values in [vec![], (1..=16).collect::<Vec<u16>>()] {
assert_matches!(
SetLevels::NonLinear { values }.to_payloads(),
Err(Hidpp20Error::Feature(ErrorType::InvalidArgument))
);
}
}
#[test]
fn writes_every_page_of_a_non_linear_table() {
use std::sync::Arc;
use super::{FN_SET_BRIGHTNESS_LEVELS, IlluminationFeature};
use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
use crate::feature::CreatableFeature;
use crate::nibble::U4;
use crate::protocol::v20::{Message, MessageHeader};
futures::executor::block_on(async {
let (raw, handle) = MockRawHidChannel::new();
let channel = Arc::new(channel_with_reader(raw).await);
let feature = IlluminationFeature::new(channel, 3, 8);
let header = MessageHeader {
device_index: 3,
feature_index: 8,
function_id: U4::from_lo(FN_SET_BRIGHTNESS_LEVELS),
software_id: U4::from_lo(1),
};
for _ in 0..2 {
handle.queue_response(Message::Long(header, [0; 16]).into());
}
let values: Vec<u16> = (1..=9).map(|i| i * 10).collect();
feature
.set_brightness_levels(&SetLevels::NonLinear { values })
.await
.unwrap();
let headers: Vec<[u8; 2]> = handle
.written_reports()
.iter()
.map(|report| [report[4], report[5]])
.collect();
assert_eq!(headers, [[7 << 5, 9], [2 << 5, (7 << 4) | 9]]);
});
}
#[test]
fn decodes_state_and_value_events() {
let mut on = [0; 16];
on[0] = 1;
assert_eq!(
decode_event(0, &on),
Some(IlluminationEvent::IlluminationChanged(
IlluminationState::On
))
);
let mut brightness = [0; 16];
brightness[0..2].copy_from_slice(&750u16.to_be_bytes());
assert_eq!(
decode_event(1, &brightness),
Some(IlluminationEvent::BrightnessChanged(750))
);
let mut temp = [0; 16];
temp[0..2].copy_from_slice(&5000u16.to_be_bytes());
assert_eq!(
decode_event(2, &temp),
Some(IlluminationEvent::ColorTemperatureChanged(5000))
);
let mut eff = [0; 16];
eff[0..2].copy_from_slice(&600u16.to_be_bytes());
assert_eq!(
decode_event(3, &eff),
Some(IlluminationEvent::BrightnessEffectiveMaxChanged(600))
);
}
#[test]
fn decodes_brightness_clamped_event() {
let mut payload = [0; 16];
payload[0] = 2; payload[1..3].copy_from_slice(&600u16.to_be_bytes());
assert_eq!(
decode_event(4, &payload),
Some(IlluminationEvent::BrightnessClamped {
source: BrightnessClampedSource::Button,
brightness: 600,
})
);
}
#[test]
fn ignores_unknown_event_sub_id() {
assert!(decode_event(9, &[0; 16]).is_none());
}
#[test]
fn keeps_clamp_event_with_unknown_source() {
for byte in 0..=u8::MAX {
let mut payload = [0; 16];
payload[0] = byte; payload[1..3].copy_from_slice(&300u16.to_be_bytes()); let event = decode_event(4, &payload)
.unwrap_or_else(|| panic!("dropped clamp event for source {byte:#04x}"));
assert_eq!(
event,
IlluminationEvent::BrightnessClamped {
source: BrightnessClampedSource::from(byte),
brightness: 300,
}
);
}
}
fn read_with<T, F>(
function: u8,
read: impl FnOnce(super::IlluminationFeature) -> F,
reply: impl FnOnce(crate::protocol::v20::MessageHeader) -> crate::protocol::v20::Message,
) -> Result<T, Hidpp20Error>
where
F: Future<Output = Result<T, Hidpp20Error>>,
{
use std::sync::Arc;
use crate::channel::mock::{MockRawHidChannel, channel_with_reader};
use crate::feature::CreatableFeature;
use crate::nibble::U4;
use crate::protocol::v20::MessageHeader;
futures::executor::block_on(async {
let (raw, handle) = MockRawHidChannel::new();
let channel = Arc::new(channel_with_reader(raw).await);
let mut pending = Box::pin(read(super::IlluminationFeature::new(channel, 3, 8)));
assert!(futures::poll!(pending.as_mut()).is_pending());
let header = MessageHeader {
device_index: 3,
feature_index: 8,
function_id: U4::from_lo(function),
software_id: U4::from_lo(1),
};
handle.send_incoming(reply(header).into()).await;
pending.await
})
}
#[test]
fn info_and_level_reads_refuse_a_short_reply() {
use super::{
FN_GET_BRIGHTNESS, FN_GET_BRIGHTNESS_INFO, FN_GET_BRIGHTNESS_LEVELS,
FN_GET_COLOR_TEMPERATURE_INFO, FN_GET_COLOR_TEMPERATURE_LEVELS, IlluminationFeature,
LevelConfig,
};
use crate::protocol::v20::Message;
let short = |header| Message::Short(header, [0b0000_0010, 0, 100]);
let mut long_info = [0; 16];
long_info[..8].copy_from_slice(&[0b0000_0010, 0, 100, 0x03, 0xe8, 0, 10, 5]);
for function in [FN_GET_BRIGHTNESS_INFO, FN_GET_COLOR_TEMPERATURE_INFO] {
let info = |f: IlluminationFeature| async move {
if function == FN_GET_BRIGHTNESS_INFO {
f.get_brightness_info().await
} else {
f.get_color_temperature_info().await
}
};
assert_matches!(
read_with(function, info, short),
Err(Hidpp20Error::UnsupportedResponse)
);
let parsed = read_with(function, info, |header| Message::Long(header, long_info)).unwrap();
assert_eq!((parsed.min, parsed.max, parsed.max_levels), (100, 1000, 5));
}
for function in [FN_GET_BRIGHTNESS_LEVELS, FN_GET_COLOR_TEMPERATURE_LEVELS] {
let levels = |f: IlluminationFeature| async move {
if function == FN_GET_BRIGHTNESS_LEVELS {
f.get_brightness_levels(0).await
} else {
f.get_color_temperature_levels(0).await
}
};
let short_linear = |header| Message::Short(header, [1, 0, 0]);
assert_matches!(
read_with(function, levels, short_linear),
Err(Hidpp20Error::UnsupportedResponse)
);
let mut page = [0; 16];
page[0] = 3 << 5;
page[1] = (1 << 4) | 6;
page[2..4].copy_from_slice(&200u16.to_be_bytes());
page[4..6].copy_from_slice(&400u16.to_be_bytes());
page[6..8].copy_from_slice(&800u16.to_be_bytes());
assert_eq!(
read_with(function, levels, |header| Message::Long(header, page)).unwrap(),
LevelConfig::NonLinear {
start_index: 1,
level_count: 6,
values: vec![200, 400, 800],
}
);
}
let brightness = |f: IlluminationFeature| async move { f.get_brightness().await };
assert_eq!(
read_with(FN_GET_BRIGHTNESS, brightness, |header| {
Message::Short(header, [0x01, 0x2c, 0])
})
.unwrap(),
300
);
}