Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions crates/openlogi-hidpp/src/feature/crown/event.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! The event emitted by the `Crown` feature (`0x4600`).

use num_enum::{IntoPrimitive, TryFromPrimitive};
use num_enum::{FromPrimitive, IntoPrimitive};

/// Rotation phase reported in a [`CrownUpdate`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
Expand All @@ -16,10 +16,13 @@ pub enum RotationState {
Active = 2,
/// Rotation stopped.
Stop = 3,
/// A state this crate does not model; carries the raw byte.
#[num_enum(catch_all)]
Other(u8),
}

/// Proximity or touch activity phase reported in a [`CrownUpdate`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
Expand All @@ -32,10 +35,13 @@ pub enum ActivityState {
Active = 2,
/// Stopped.
Stop = 3,
/// A state this crate does not model; carries the raw byte.
#[num_enum(catch_all)]
Other(u8),
}

/// Touch gesture reported in a [`CrownUpdate`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
Expand All @@ -46,10 +52,13 @@ pub enum CrownGesture {
Tap = 1,
/// Double tap.
DoubleTap = 2,
/// A gesture this crate does not model; carries the raw byte.
#[num_enum(catch_all)]
Other(u8),
}

/// Crown button state reported in a [`CrownUpdate`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
Expand All @@ -66,6 +75,9 @@ pub enum ButtonState {
LongPressActive = 4,
/// Released.
Release = 5,
/// A state this crate does not model; carries the raw byte.
#[num_enum(catch_all)]
Other(u8),
}

/// An event emitted by [`CrownFeature`](super::CrownFeature).
Expand Down Expand Up @@ -107,13 +119,13 @@ pub struct CrownUpdate {
pub(super) fn decode_event(sub_id: u8, payload: &[u8; 16]) -> Option<CrownEvent> {
match sub_id {
0 => Some(CrownEvent::Update(CrownUpdate {
rotation_state: RotationState::try_from(payload[0]).ok()?,
rotation_state: RotationState::from(payload[0]),
relative_slot_rotation: payload[1] as i8,
relative_ratchet_rotation: payload[2] as i8,
proximity: ActivityState::try_from(payload[3]).ok()?,
touch: ActivityState::try_from(payload[4]).ok()?,
gesture: CrownGesture::try_from(payload[5]).ok()?,
button: ButtonState::try_from(payload[6]).ok()?,
proximity: ActivityState::from(payload[3]),
touch: ActivityState::from(payload[4]),
gesture: CrownGesture::from(payload[5]),
button: ButtonState::from(payload[6]),
speed: i16::from_be_bytes([payload[14], payload[15]]),
})),
_ => None,
Expand Down
26 changes: 24 additions & 2 deletions crates/openlogi-hidpp/src/feature/crown/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,32 @@ fn ignores_unknown_event_sub_id() {
}

#[test]
fn ignores_event_with_unknown_enum() {
fn keeps_event_with_unknown_enum_field() {
let mut payload = [0; 16];
payload[1] = 0xfb; // -5 slots — a valid sibling field that must survive
payload[6] = 0x09; // out-of-range button state
assert!(decode_event(0, &payload).is_none());
let CrownEvent::Update(update) = decode_event(0, &payload).expect("event kept");
assert_eq!(update.button, ButtonState::Other(0x09));
assert_eq!(update.relative_slot_rotation, -5);
}

/// Totality: for a known sub-id, no single field byte value may drop the whole
/// event. Sweeps every value of each enum-typed field position.
#[test]
fn known_sub_id_survives_any_field_byte() {
for byte in 0..=u8::MAX {
for pos in [0usize, 3, 4, 5, 6] {
let mut payload = [0; 16];
payload[1] = 0xfb; // sibling that must always survive
payload[pos] = byte;
let CrownEvent::Update(update) = decode_event(0, &payload)
.unwrap_or_else(|| panic!("dropped event for payload[{pos}]={byte:#04x}"));
assert_eq!(
update.relative_slot_rotation, -5,
"sibling lost for payload[{pos}]={byte:#04x}"
);
}
}
}

#[test]
Expand Down
14 changes: 8 additions & 6 deletions crates/openlogi-hidpp/src/feature/extended_dpi/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ pub struct DpiParametersChanged {
pub dpi_x: u16,
/// New Y-axis DPI, or `0` when the sensor has no independent Y axis.
pub dpi_y: u16,
/// New lift-off distance.
pub lod: Lod,
/// New lift-off distance, or `None` when the device reported a value this
/// crate does not model (the rest of the event is still delivered).
pub lod: Option<Lod>,
}

/// Payload of [`ExtendedDpiEvent::CalibrationCompleted`].
Expand All @@ -36,8 +37,9 @@ pub struct DpiParametersChanged {
pub struct DpiCalibrationCompleted {
/// Index of the sensor.
pub sensor_index: u8,
/// Axis that was calibrated.
pub direction: DpiDirection,
/// Axis that was calibrated, or `None` when the device reported a value
/// this crate does not model (the rest of the event is still delivered).
pub direction: Option<DpiDirection>,
/// Calibration correction value; [`i16::MIN`] (`0x8000`) signals a
/// sensor-level calibration failure (see [`Self::failed`]).
pub correction: i16,
Expand Down Expand Up @@ -65,12 +67,12 @@ pub(super) fn decode_event(sub_id: u8, payload: &[u8; 16]) -> Option<ExtendedDpi
sensor_index: payload[0],
dpi_x: u16::from_be_bytes([payload[1], payload[2]]),
dpi_y: u16::from_be_bytes([payload[3], payload[4]]),
lod: Lod::try_from(payload[5]).ok()?,
lod: Lod::try_from(payload[5]).ok(),
})),
1 => Some(ExtendedDpiEvent::CalibrationCompleted(
DpiCalibrationCompleted {
sensor_index: payload[0],
direction: DpiDirection::try_from(payload[1]).ok()?,
direction: DpiDirection::try_from(payload[1]).ok(),
correction: i16::from_be_bytes([payload[2], payload[3]]),
delta: i16::from_be_bytes([payload[4], payload[5]]),
},
Expand Down
34 changes: 30 additions & 4 deletions crates/openlogi-hidpp/src/feature/extended_dpi/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ fn decodes_parameters_changed_event() {
assert_eq!(event.sensor_index, 1);
assert_eq!(event.dpi_x, 800);
assert_eq!(event.dpi_y, 1600);
assert_eq!(event.lod, Lod::Medium);
assert_eq!(event.lod, Some(Lod::Medium));
}

#[test]
Expand All @@ -242,7 +242,7 @@ fn decodes_calibration_completed_event() {
let ExtendedDpiEvent::CalibrationCompleted(event) = decode_event(1, &payload).unwrap() else {
panic!("expected a calibration-completed event");
};
assert_eq!(event.direction, DpiDirection::Y);
assert_eq!(event.direction, Some(DpiDirection::Y));
assert_eq!(event.correction, 100);
assert_eq!(event.delta, -1);
assert!(!event.failed());
Expand All @@ -265,11 +265,37 @@ fn ignores_unknown_event_sub_id() {
}

#[test]
fn ignores_event_with_unknown_lod() {
fn keeps_event_with_unknown_lod() {
let mut payload = [0; 16];
payload[0] = 3; // sensor index — a valid sibling that must survive
payload[5] = 9;

assert!(decode_event(0, &payload).is_none());
let ExtendedDpiEvent::ParametersChanged(changed) =
decode_event(0, &payload).expect("event kept")
else {
panic!("expected ParametersChanged");
};
// Lod stays a closed, write-safe enum; an unknown lift-off value surfaces
// as None on the event without dropping the DPI change.
assert_eq!(changed.lod, None);
assert_eq!(changed.sensor_index, 3);
}

/// Totality: a known event sub-id must decode for any enum-field byte value.
#[test]
fn known_sub_id_survives_any_field_byte() {
for byte in 0..=u8::MAX {
// sub 0: Lod at payload[5]; sub 1: DpiDirection at payload[1].
for (sub_id, pos) in [(0u8, 5usize), (1, 1)] {
let mut payload = [0; 16];
payload[0] = 3; // sensor index sibling
payload[pos] = byte;
assert!(
decode_event(sub_id, &payload).is_some(),
"dropped sub {sub_id} for payload[{pos}]={byte:#04x}"
);
}
}
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/openlogi-hidpp/src/feature/illumination/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(super) fn decode_event(sub_id: u8, payload: &[u8; 16]) -> Option<Illuminatio
payload, 0,
))),
4 => Some(IlluminationEvent::BrightnessClamped {
source: BrightnessClampedSource::try_from(payload[0]).ok()?,
source: BrightnessClampedSource::from(payload[0]),
brightness: be16(payload, 1),
}),
_ => None,
Expand Down
20 changes: 20 additions & 0 deletions crates/openlogi-hidpp/src/feature/illumination/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,23 @@ fn decodes_brightness_clamped_event() {
fn ignores_unknown_event_sub_id() {
assert!(decode_event(9, &[0; 16]).is_none());
}

/// Totality: the clamp-source event must decode for any source byte; an
/// unknown source folds to `Other(_)` and the brightness sibling survives.
#[test]
fn keeps_clamp_event_with_unknown_source() {
for byte in 0..=u8::MAX {
let mut payload = [0; 16];
payload[0] = byte; // BrightnessClampedSource
payload[1..3].copy_from_slice(&300u16.to_be_bytes()); // brightness sibling
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,
}
);
}
}
7 changes: 5 additions & 2 deletions crates/openlogi-hidpp/src/feature/illumination/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Domain types for the `Illumination` feature (`0x1990`).

use num_enum::{IntoPrimitive, TryFromPrimitive};
use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};

use crate::protocol::v20::{ErrorType, Hidpp20Error};

Expand Down Expand Up @@ -189,7 +189,7 @@ impl From<bool> for IlluminationState {
}

/// What caused a [`brightness clamp`](super::event::IlluminationEvent::BrightnessClamped).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
Expand All @@ -200,6 +200,9 @@ pub enum BrightnessClampedSource {
HidPlusPlus = 1,
/// A hardware button triggered the clamp.
Button = 2,
/// A source this crate does not model; carries the raw byte.
#[num_enum(catch_all)]
Other(u8),
}

/// Decodes the on/off state bit shared by `getIllumination` and its event.
Expand Down
13 changes: 7 additions & 6 deletions crates/openlogi-hidpp/src/feature/rgb_effects/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ pub enum RgbEffectsEvent {
params: [u8; CLUSTER_EFFECT_PARAM_COUNT],
/// Persistence the effect was applied with.
persistence: RgbPersistence,
/// Power-mode target the effect applies to.
power_mode: PowerModeTarget,
/// Power-mode target the effect applies to, or `None` when the device
/// reported a value this crate does not model.
power_mode: Option<PowerModeTarget>,
},
}

Expand All @@ -45,9 +46,9 @@ pub(super) fn decode_event(sub_id: u8, payload: &[u8; 16]) -> Option<RgbEffectsE
cluster_index: payload[0],
effect_counter: be16(payload, 1),
}),
1 => Some(RgbEffectsEvent::UserActivity(
ActivityEventType::try_from(payload[0]).ok()?,
)),
1 => Some(RgbEffectsEvent::UserActivity(ActivityEventType::from(
payload[0],
))),
2 => {
let mut params = [0; CLUSTER_EFFECT_PARAM_COUNT];
params.copy_from_slice(&payload[2..2 + CLUSTER_EFFECT_PARAM_COUNT]);
Expand All @@ -62,7 +63,7 @@ pub(super) fn decode_event(sub_id: u8, payload: &[u8; 16]) -> Option<RgbEffectsE
power_mode: PowerModeTarget::try_from(
(flags >> POWER_TARGET_SHIFT) & FLAGS_FIELD_MASK,
)
.ok()?,
.ok(),
})
}
_ => None,
Expand Down
34 changes: 33 additions & 1 deletion crates/openlogi-hidpp/src/feature/rgb_effects/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn decodes_cluster_changed_event() {
cluster_effect_index: 2,
params: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
persistence: RgbPersistence::VOLATILE,
power_mode: PowerModeTarget::PowerSave,
power_mode: Some(PowerModeTarget::PowerSave),
})
);
}
Expand All @@ -156,3 +156,35 @@ fn maps_stable_enum_wire_values() {
);
assert!(SlotInfoType::try_from(7u8).is_err());
}

/// Totality: the user-activity event must decode for any activity byte.
#[test]
fn keeps_user_activity_event_with_unknown_type() {
for byte in 0..=u8::MAX {
let mut payload = [0; 16];
payload[0] = byte;
assert_eq!(
decode_event(1, &payload),
Some(RgbEffectsEvent::UserActivity(ActivityEventType::from(byte))),
"dropped user-activity event for byte {byte:#04x}"
);
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/// Totality: the cluster-changed event must decode for any flags byte. The
/// 2-bit power-mode nibble can hold values 2 and 3 that the enum does not
/// model; those must surface as `None` without dropping the event.
#[test]
fn keeps_cluster_changed_event_with_unknown_power_mode() {
for flags in 0..=u8::MAX {
let mut payload = [0; 16];
payload[0] = 1; // cluster_index sibling that must survive
payload[12] = flags;
let event = decode_event(2, &payload)
.unwrap_or_else(|| panic!("dropped cluster-changed event for flags {flags:#04x}"));
let RgbEffectsEvent::ClusterChanged { cluster_index, .. } = event else {
panic!("expected ClusterChanged");
};
assert_eq!(cluster_index, 1);
}
}
7 changes: 5 additions & 2 deletions crates/openlogi-hidpp/src/feature/rgb_effects/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Domain types for the `RgbEffects` feature (`0x8071`).

use num_enum::{IntoPrimitive, TryFromPrimitive};
use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};

/// Number of effect parameters carried by `setRgbClusterEffect`.
pub const CLUSTER_EFFECT_PARAM_COUNT: usize = 10;
Expand Down Expand Up @@ -96,7 +96,7 @@ pub enum LedBinIndex {
}

/// The kind of user-activity event.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
Expand All @@ -105,6 +105,9 @@ pub enum ActivityEventType {
NoActivityTimeoutReached = 0,
/// User activity was detected.
UserActivityDetected = 1,
/// A type this crate does not model; carries the raw byte.
#[num_enum(catch_all)]
Other(u8),
}

bitflags::bitflags! {
Expand Down