use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, PoisonError, RwLock};
use hidpp::channel::{HidppChannel, HidppMessage, MessageListenerGuard};
use hidpp::feature::adc_measurement::AdcMeasurementFeature;
use hidpp::feature::backlight::BacklightFeature;
use hidpp::feature::battery_status::BatteryStatusFeature;
use hidpp::feature::battery_voltage::BatteryVoltageFeature;
use hidpp::feature::extended_dpi::ExtendedDpiFeature;
use hidpp::feature::extended_report_rate::ExtendedReportRateFeature;
use hidpp::feature::fn_inversion::{
FnInversionMultiHostFeature, FnInversionWithDefaultStateFeature,
};
use hidpp::feature::multi_platform::PlatformChange;
use hidpp::feature::unified_battery::BatteryEvent;
use hidpp::feature::wireless_device_status::WirelessDeviceStatusEvent;
use hidpp::protocol::{v10, v20};
use hidpp::receiver::{bolt, unifying};
use serde::{Deserialize, Serialize};
#[cfg(test)]
use tokio::sync::Notify;
use tokio::sync::mpsc;
use crate::ReceiverProtocol;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HidppEventSource {
ReceiverConnection,
WirelessDeviceStatus,
UnifiedBattery,
BatteryStatus,
BatteryVoltage,
AdcMeasurement,
FnInversion,
ExtendedDpi,
MultiPlatform,
ExtendedReportRate,
Backlight,
}
impl HidppEventSource {
const fn changes_settings(self) -> bool {
matches!(
self,
Self::FnInversion
| Self::ExtendedDpi
| Self::MultiPlatform
| Self::ExtendedReportRate
| Self::Backlight
)
}
}
#[derive(Clone)]
pub struct EventNotifier {
sender: mpsc::Sender<HidppEventSource>,
#[cfg(test)]
observation: Option<Arc<EventObservation>>,
}
impl EventNotifier {
fn notify(&self, source: HidppEventSource) {
let _ = self.sender.try_send(source);
#[cfg(test)]
if let Some(observation) = &self.observation {
observation.count.fetch_add(1, Ordering::Release);
observation.changed.notify_waiters();
}
}
}
pub type EventReceiver = mpsc::Receiver<HidppEventSource>;
#[must_use]
pub fn event_channel() -> (EventNotifier, EventReceiver) {
let (sender, receiver) = mpsc::channel(1);
(
EventNotifier {
sender,
#[cfg(test)]
observation: None,
},
receiver,
)
}
#[cfg(test)]
struct EventObservation {
count: AtomicUsize,
changed: Notify,
}
#[cfg(test)]
pub(crate) struct EventObserver {
observation: Arc<EventObservation>,
}
#[cfg(test)]
impl EventObserver {
pub(crate) async fn wait_for(&self, expected: usize) {
loop {
let changed = self.observation.changed.notified();
if self.observation.count.load(Ordering::Acquire) >= expected {
return;
}
changed.await;
}
}
}
#[cfg(test)]
pub(crate) fn observed_event_channel() -> (EventNotifier, EventReceiver, EventObserver) {
let (sender, receiver) = mpsc::channel(1);
let observation = Arc::new(EventObservation {
count: AtomicUsize::new(0),
changed: Notify::new(),
});
(
EventNotifier {
sender,
observation: Some(Arc::clone(&observation)),
},
receiver,
EventObserver { observation },
)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(super) struct EventFeatureIndices {
pub(super) wireless_status: Option<u8>,
pub(super) unified_battery: Option<u8>,
pub(super) battery_status: Option<u8>,
pub(super) battery_voltage: Option<u8>,
pub(super) adc_measurement: Option<u8>,
pub(super) fn_inversion: Option<u8>,
pub(super) fn_inversion_multi_host: Option<u8>,
pub(super) extended_dpi: Option<u8>,
pub(super) multi_platform: Option<u8>,
pub(super) extended_report_rate: Option<u8>,
pub(super) backlight: Option<u8>,
}
impl EventFeatureIndices {
pub(super) fn from_feature_ids(ids: &[u16]) -> Self {
let mut indices = Self::default();
for (position, id) in ids.iter().copied().enumerate() {
let Ok(index) = u8::try_from(position + 1) else {
break;
};
match id {
0x1d4b => indices.wireless_status = Some(index),
0x1004 => indices.unified_battery = Some(index),
0x1000 => indices.battery_status = Some(index),
0x1001 => indices.battery_voltage = Some(index),
0x1f20 => indices.adc_measurement = Some(index),
0x40a2 => indices.fn_inversion = Some(index),
0x40a3 => indices.fn_inversion_multi_host = Some(index),
0x2202 => indices.extended_dpi = Some(index),
0x4531 => indices.multi_platform = Some(index),
0x8061 => indices.extended_report_rate = Some(index),
0x1982 => indices.backlight = Some(index),
_ => {}
}
}
indices
}
fn recognizes(self, message: &v20::Message) -> Option<HidppEventSource> {
let header = message.header();
if header.software_id.to_lo() != 0 {
return None;
}
let function_id = header.function_id.to_lo();
let payload = message.extend_payload();
if self.wireless_status == Some(header.feature_index)
&& WirelessDeviceStatusEvent::decode(function_id, &payload).is_some()
{
return Some(HidppEventSource::WirelessDeviceStatus);
}
if self.unified_battery == Some(header.feature_index)
&& BatteryEvent::decode(function_id, &payload).is_some()
{
return Some(HidppEventSource::UnifiedBattery);
}
if self.battery_status == Some(header.feature_index)
&& BatteryStatusFeature::is_status_broadcast(function_id)
{
return Some(HidppEventSource::BatteryStatus);
}
if self.battery_voltage == Some(header.feature_index)
&& BatteryVoltageFeature::is_status_broadcast(function_id)
{
return Some(HidppEventSource::BatteryVoltage);
}
if self.adc_measurement == Some(header.feature_index)
&& AdcMeasurementFeature::is_status_broadcast(function_id)
{
return Some(HidppEventSource::AdcMeasurement);
}
if self.fn_inversion == Some(header.feature_index)
&& FnInversionWithDefaultStateFeature::is_state_broadcast(function_id)
{
return Some(HidppEventSource::FnInversion);
}
if self.fn_inversion_multi_host == Some(header.feature_index)
&& FnInversionMultiHostFeature::is_state_broadcast(message)
{
return Some(HidppEventSource::FnInversion);
}
if self.extended_dpi == Some(header.feature_index)
&& ExtendedDpiFeature::is_parameters_broadcast(function_id)
{
return Some(HidppEventSource::ExtendedDpi);
}
if self.multi_platform == Some(header.feature_index)
&& PlatformChange::decode(&payload).is_some()
{
return Some(HidppEventSource::MultiPlatform);
}
if self.extended_report_rate == Some(header.feature_index)
&& ExtendedReportRateFeature::is_rate_broadcast(function_id)
{
return Some(HidppEventSource::ExtendedReportRate);
}
if self.backlight == Some(header.feature_index)
&& BacklightFeature::is_info_broadcast(function_id)
{
return Some(HidppEventSource::Backlight);
}
None
}
}
#[derive(Clone, Copy)]
struct DeviceEvents {
device_index: u8,
features: EventFeatureIndices,
settings_revision: u64,
}
struct SubscriptionState {
protocol: Option<ReceiverProtocol>,
devices: RwLock<Vec<DeviceEvents>>,
receiver_snapshot_depth: AtomicUsize,
notifier: EventNotifier,
}
impl SubscriptionState {
fn decode(&self, raw: HidppMessage, matched: bool) -> Option<HidppEventSource> {
if matched {
return None;
}
let receiver_connection = match self.protocol {
Some(ReceiverProtocol::Bolt) => matches!(
bolt::decode_notification(&v10::Message::from(raw)),
Some(bolt::Event::DeviceConnection(_))
),
Some(ReceiverProtocol::Unifying) => matches!(
unifying::decode_notification(&v10::Message::from(raw)),
Some(unifying::Event::DeviceConnection(_))
),
None => false,
};
if receiver_connection {
return (self.receiver_snapshot_depth.load(Ordering::Acquire) == 0)
.then_some(HidppEventSource::ReceiverConnection);
}
let message = v20::Message::from(raw);
let device_index = message.header().device_index;
let source = self
.devices
.read()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.find(|device| device.device_index == device_index)
.and_then(|device| device.features.recognizes(&message))?;
if source.changes_settings() {
if let Some(device) = self
.devices
.write()
.unwrap_or_else(PoisonError::into_inner)
.iter_mut()
.find(|device| device.device_index == device_index)
{
device.settings_revision = device.settings_revision.wrapping_add(1);
}
}
Some(source)
}
}
#[derive(Clone)]
pub(super) struct EventSubscriptionHandle {
state: Arc<SubscriptionState>,
}
impl EventSubscriptionHandle {
pub(super) fn register_device(&self, device_index: u8, features: EventFeatureIndices) {
let mut devices = self
.state
.devices
.write()
.unwrap_or_else(PoisonError::into_inner);
if let Some(device) = devices
.iter_mut()
.find(|device| device.device_index == device_index)
{
device.features = features;
} else {
devices.push(DeviceEvents {
device_index,
features,
settings_revision: 0,
});
}
}
pub(super) fn settings_revision(&self, device_index: u8) -> u64 {
self.state
.devices
.read()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.find(|device| device.device_index == device_index)
.map_or(0, |device| device.settings_revision)
}
pub(super) fn begin_receiver_snapshot(&self) -> ReceiverSnapshotGuard {
self.state
.receiver_snapshot_depth
.fetch_add(1, Ordering::AcqRel);
ReceiverSnapshotGuard {
state: Arc::clone(&self.state),
}
}
}
pub(super) struct ChannelEventSubscriptions {
handle: EventSubscriptionHandle,
_listener: MessageListenerGuard,
}
impl ChannelEventSubscriptions {
pub(super) fn attach(
channel: &Arc<HidppChannel>,
protocol: Option<ReceiverProtocol>,
notifier: EventNotifier,
) -> Self {
let state = Arc::new(SubscriptionState {
protocol,
devices: RwLock::new(Vec::new()),
receiver_snapshot_depth: AtomicUsize::new(0),
notifier,
});
let listener = channel.add_msg_listener_guarded({
let state = Arc::clone(&state);
move |raw, matched| {
if let Some(source) = state.decode(raw, matched) {
state.notifier.notify(source);
}
}
});
Self {
handle: EventSubscriptionHandle { state },
_listener: listener,
}
}
pub(super) fn handle(&self) -> EventSubscriptionHandle {
self.handle.clone()
}
}
pub(super) struct ReceiverSnapshotGuard {
state: Arc<SubscriptionState>,
}
impl Drop for ReceiverSnapshotGuard {
fn drop(&mut self) {
self.state
.receiver_snapshot_depth
.fetch_sub(1, Ordering::AcqRel);
}
}
#[cfg(test)]
mod tests {
use hidpp::nibble::U4;
use hidpp::protocol::{v10, v20};
use super::*;
fn state(protocol: Option<ReceiverProtocol>) -> Arc<SubscriptionState> {
let (notifier, _receiver) = event_channel();
Arc::new(SubscriptionState {
protocol,
devices: RwLock::new(Vec::new()),
receiver_snapshot_depth: AtomicUsize::new(0),
notifier,
})
}
#[test]
fn feature_indices_are_runtime_table_positions() {
assert_eq!(
EventFeatureIndices::from_feature_ids(&[0x0001, 0x1d4b, 0x1004]),
EventFeatureIndices {
wireless_status: Some(2),
unified_battery: Some(3),
battery_status: None,
battery_voltage: None,
adc_measurement: None,
fn_inversion: None,
fn_inversion_multi_host: None,
extended_dpi: None,
multi_platform: None,
extended_report_rate: None,
backlight: None,
}
);
}
#[test]
fn receiver_snapshot_suppresses_only_triggered_connection_wakeups() {
let state = state(Some(ReceiverProtocol::Unifying));
let raw = v10::Message::Short(
v10::MessageHeader {
device_index: 2,
sub_id: 0x41,
},
[0, 0x02, 0x34, 0x12],
)
.into();
assert_eq!(
state.decode(raw, false),
Some(HidppEventSource::ReceiverConnection)
);
state.receiver_snapshot_depth.store(1, Ordering::Release);
assert_eq!(state.decode(raw, false), None);
}
#[test]
fn receiver_snapshot_does_not_suppress_device_feature_events() {
let state = state(Some(ReceiverProtocol::Bolt));
state.devices.write().unwrap().push(DeviceEvents {
device_index: 2,
settings_revision: 0,
features: EventFeatureIndices {
wireless_status: Some(5),
unified_battery: None,
battery_status: None,
battery_voltage: None,
adc_measurement: None,
fn_inversion: None,
fn_inversion_multi_host: None,
extended_dpi: None,
multi_platform: None,
extended_report_rate: None,
backlight: None,
},
});
state.receiver_snapshot_depth.store(1, Ordering::Release);
let wireless = v20::Message::Long(
v20::MessageHeader {
device_index: 2,
feature_index: 5,
function_id: U4::from_lo(0),
software_id: U4::from_lo(0),
},
[0; 16],
)
.into();
assert_eq!(
state.decode(wireless, false),
Some(HidppEventSource::WirelessDeviceStatus)
);
}
#[test]
fn registered_device_events_decode_to_typed_sources() {
let state = state(None);
state.devices.write().unwrap().push(DeviceEvents {
device_index: 3,
settings_revision: 0,
features: EventFeatureIndices {
wireless_status: Some(5),
unified_battery: Some(7),
battery_status: None,
battery_voltage: None,
adc_measurement: None,
fn_inversion: None,
fn_inversion_multi_host: None,
extended_dpi: None,
multi_platform: None,
extended_report_rate: None,
backlight: None,
},
});
let wireless = v20::Message::Long(
v20::MessageHeader {
device_index: 3,
feature_index: 5,
function_id: U4::from_lo(0),
software_id: U4::from_lo(0),
},
[0; 16],
)
.into();
assert_eq!(
state.decode(wireless, false),
Some(HidppEventSource::WirelessDeviceStatus)
);
let mut payload = [0; 16];
payload[0] = 80;
payload[1] = 4;
let battery = v20::Message::Long(
v20::MessageHeader {
device_index: 3,
feature_index: 7,
function_id: U4::from_lo(0),
software_id: U4::from_lo(0),
},
payload,
)
.into();
assert_eq!(
state.decode(battery, false),
Some(HidppEventSource::UnifiedBattery)
);
}
#[test]
fn adc_measurement_index_is_its_table_position() {
let table = [
0x0001, 0x0003, 0x0005, 0x8070, 0x8010, 0x8310, 0x8300, 0x1f20,
];
assert_eq!(
EventFeatureIndices::from_feature_ids(&table).adc_measurement,
Some(8)
);
}
#[test]
fn adc_measurement_broadcasts_request_reconciliation() {
let state = state(None);
state.devices.write().unwrap().push(DeviceEvents {
device_index: 0xff,
settings_revision: 0,
features: EventFeatureIndices {
wireless_status: None,
unified_battery: None,
battery_status: None,
battery_voltage: None,
adc_measurement: Some(8),
fn_inversion: None,
fn_inversion_multi_host: None,
extended_dpi: None,
multi_platform: None,
extended_report_rate: None,
backlight: None,
},
});
let event = |function_id: u8, software_id: u8, bytes: [u8; 3]| {
let mut payload = [0; 16];
payload[..3].copy_from_slice(&bytes);
v20::Message::Long(
v20::MessageHeader {
device_index: 0xff,
feature_index: 8,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
payload,
)
.into()
};
for bytes in [[0x00, 0x00, 0x00], [0x10, 0x4a, 0x03]] {
assert_eq!(
state.decode(event(0, 0, bytes), false),
Some(HidppEventSource::AdcMeasurement),
"{bytes:02x?}"
);
}
assert_eq!(
state.decode(event(0, 0, [0x10, 0x4a, 0x0f]), false),
Some(HidppEventSource::AdcMeasurement)
);
assert_eq!(state.decode(event(0, 0xd, [0x10, 0x4a, 0x01]), false), None);
assert_eq!(state.decode(event(1, 0, [0x10, 0x4a, 0x01]), false), None);
}
#[test]
fn legacy_and_voltage_battery_indexes_are_table_positions() {
let legacy =
EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x0005, 0x1000, 0x1d4b]);
assert_eq!(legacy.battery_status, Some(4));
assert_eq!(legacy.battery_voltage, None);
assert_eq!(legacy.unified_battery, None);
let voltage = EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x1001, 0x8070]);
assert_eq!(voltage.battery_voltage, Some(3));
assert_eq!(voltage.battery_status, None);
}
#[test]
fn legacy_and_voltage_battery_broadcasts_request_reconciliation() {
let state = state(None);
state.devices.write().unwrap().push(DeviceEvents {
device_index: 1,
settings_revision: 0,
features: EventFeatureIndices {
battery_status: Some(4),
battery_voltage: Some(6),
..EventFeatureIndices::default()
},
});
let event = |feature_index: u8, function_id: u8, software_id: u8, bytes: [u8; 3]| {
let mut payload = [0; 16];
payload[..3].copy_from_slice(&bytes);
v20::Message::Long(
v20::MessageHeader {
device_index: 1,
feature_index,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
payload,
)
.into()
};
for bytes in [[50, 20, 0x00], [0, 0, 0x09]] {
assert_eq!(
state.decode(event(4, 0, 0, bytes), false),
Some(HidppEventSource::BatteryStatus),
"{bytes:02x?}"
);
}
assert_eq!(
state.decode(event(6, 0, 0, [0x0f, 0x3c, 0x80]), false),
Some(HidppEventSource::BatteryVoltage)
);
for (feature_index, source) in [(4, "0x1000"), (6, "0x1001")] {
assert_eq!(
state.decode(event(feature_index, 0, 0, [50, 20, 0]), true),
None,
"{source}"
);
assert_eq!(
state.decode(event(feature_index, 0, 0xd, [50, 20, 0]), false),
None,
"{source}"
);
assert_eq!(
state.decode(event(feature_index, 1, 0, [50, 20, 0]), false),
None,
"{source}"
);
}
assert_eq!(state.decode(event(5, 0, 0, [50, 20, 0]), false), None);
}
#[test]
fn setting_feature_indexes_are_table_positions() {
let keyboard = EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x1d4b, 0x40a3]);
assert_eq!(keyboard.fn_inversion_multi_host, Some(4));
assert_eq!(keyboard.fn_inversion, None);
let single_host = EventFeatureIndices::from_feature_ids(&[0x0001, 0x40a2]);
assert_eq!(single_host.fn_inversion, Some(2));
let mouse = EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x2202, 0x1001]);
assert_eq!(mouse.extended_dpi, Some(3));
assert_eq!(mouse.battery_voltage, Some(4));
}
#[test]
fn setting_broadcasts_request_reconciliation_and_bump_the_revision() {
let state = state(Some(ReceiverProtocol::Bolt));
let handle = EventSubscriptionHandle {
state: Arc::clone(&state),
};
let features = EventFeatureIndices {
fn_inversion: Some(4),
fn_inversion_multi_host: Some(5),
extended_dpi: Some(6),
battery_status: Some(7),
..EventFeatureIndices::default()
};
handle.register_device(1, features);
handle.register_device(2, features);
let long = |device_index: u8, feature_index: u8, function_id: u8, software_id: u8| {
v20::Message::Long(
v20::MessageHeader {
device_index,
feature_index,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
[0; 16],
)
.into()
};
assert_eq!(handle.settings_revision(1), 0);
assert_eq!(
state.decode(long(1, 4, 1, 0), false),
Some(HidppEventSource::FnInversion)
);
assert_eq!(handle.settings_revision(1), 1);
assert_eq!(state.decode(long(1, 4, 0, 0), false), None);
assert_eq!(
state.decode(long(1, 5, 0, 0), false),
Some(HidppEventSource::FnInversion)
);
let short = v20::Message::Short(
v20::MessageHeader {
device_index: 1,
feature_index: 5,
function_id: U4::from_lo(0),
software_id: U4::from_lo(0),
},
[1, 1, 0],
)
.into();
assert_eq!(state.decode(short, false), None);
assert_eq!(handle.settings_revision(1), 2);
assert_eq!(
state.decode(long(1, 6, 0, 0), false),
Some(HidppEventSource::ExtendedDpi)
);
assert_eq!(state.decode(long(1, 6, 1, 0), false), None);
assert_eq!(handle.settings_revision(1), 3);
assert_eq!(state.decode(long(1, 5, 1, 0), true), None);
assert_eq!(state.decode(long(1, 5, 1, 0xd), false), None);
assert_eq!(
state.decode(long(1, 7, 0, 0), false),
Some(HidppEventSource::BatteryStatus)
);
assert_eq!(handle.settings_revision(1), 3);
handle.register_device(1, features);
assert_eq!(handle.settings_revision(1), 3);
assert_eq!(handle.settings_revision(2), 0);
assert_eq!(state.decode(long(3, 5, 0, 0), false), None);
assert_eq!(handle.settings_revision(3), 0);
}
#[test]
fn platform_change_reports_bump_the_revision() {
let keyboard = EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x40a3, 0x4531]);
assert_eq!(keyboard.multi_platform, Some(4));
let state = state(Some(ReceiverProtocol::Bolt));
let handle = EventSubscriptionHandle {
state: Arc::clone(&state),
};
handle.register_device(1, keyboard);
let report = |function_id: u8, software_id: u8| {
v20::Message::Short(
v20::MessageHeader {
device_index: 1,
feature_index: 4,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
[1, 2, 2],
)
.into()
};
for (function_id, revision) in [(0, 1), (1, 2)] {
assert_eq!(
state.decode(report(function_id, 0), false),
Some(HidppEventSource::MultiPlatform)
);
assert_eq!(handle.settings_revision(1), revision);
}
assert_eq!(state.decode(report(2, 0), true), None);
assert_eq!(state.decode(report(2, 0xd), false), None);
assert_eq!(handle.settings_revision(1), 2);
}
#[test]
fn backlight_info_broadcasts_bump_the_revision() {
let keyboard =
EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x40a3, 0x1982, 0x4531]);
assert_eq!(keyboard.backlight, Some(4));
let state = state(Some(ReceiverProtocol::Bolt));
let handle = EventSubscriptionHandle {
state: Arc::clone(&state),
};
handle.register_device(1, keyboard);
let report = |function_id: u8, software_id: u8| {
v20::Message::Long(
v20::MessageHeader {
device_index: 1,
feature_index: 4,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
[8, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
)
.into()
};
assert_eq!(
state.decode(report(0, 0), false),
Some(HidppEventSource::Backlight)
);
assert_eq!(handle.settings_revision(1), 1);
assert_eq!(state.decode(report(1, 0), false), None);
assert_eq!(state.decode(report(0, 0), true), None);
assert_eq!(state.decode(report(0, 0xd), false), None);
assert_eq!(handle.settings_revision(1), 1);
}
#[test]
fn report_rate_broadcasts_bump_the_revision() {
let mouse =
EventFeatureIndices::from_feature_ids(&[0x0001, 0x0003, 0x2202, 0x8060, 0x8061]);
assert_eq!(mouse.extended_report_rate, Some(5));
let state = state(Some(ReceiverProtocol::Bolt));
let handle = EventSubscriptionHandle {
state: Arc::clone(&state),
};
handle.register_device(1, mouse);
let report = |feature_index: u8, function_id: u8, software_id: u8| {
v20::Message::Short(
v20::MessageHeader {
device_index: 1,
feature_index,
function_id: U4::from_lo(function_id),
software_id: U4::from_lo(software_id),
},
[3, 0, 0],
)
.into()
};
assert_eq!(
state.decode(report(5, 0, 0), false),
Some(HidppEventSource::ExtendedReportRate)
);
assert_eq!(handle.settings_revision(1), 1);
assert_eq!(state.decode(report(5, 2, 0), false), None);
assert_eq!(state.decode(report(5, 0, 0), true), None);
assert_eq!(state.decode(report(5, 0, 0xd), false), None);
assert_eq!(state.decode(report(4, 0, 0), false), None);
assert_eq!(handle.settings_revision(1), 1);
}
#[test]
fn event_requests_are_bounded_and_coalesced() {
let (notifier, mut receiver) = event_channel();
notifier.notify(HidppEventSource::ReceiverConnection);
notifier.notify(HidppEventSource::UnifiedBattery);
assert_eq!(
receiver.try_recv(),
Ok(HidppEventSource::ReceiverConnection)
);
assert_eq!(receiver.try_recv(), Err(mpsc::error::TryRecvError::Empty));
}
}