Skip to content

fix(hidpp): keep events when a field carries an unknown enum value - #432

Open
bugprone wants to merge 2 commits into
AprilNEA:masterfrom
bugprone:fix/tolerant-event-decode
Open

fix(hidpp): keep events when a field carries an unknown enum value#432
bugprone wants to merge 2 commits into
AprilNEA:masterfrom
bugprone:fix/tolerant-event-decode

Conversation

@bugprone

Copy link
Copy Markdown

Summary

Event listener closures have no error channel, so a single unrecognised field byte made a whole event decode return None — a crown update with an unknown gesture, for example, lost its valid rotation deltas in the same payload. This sweeps the remaining event decoders onto the tolerant pattern #381 established.

The idiom is chosen per how each enum is used:

  • Event-read-only enums (crown RotationState/ActivityState/CrownGesture/ButtonState, DpiDirection, BrightnessClampedSource, ActivityEventType, PowerModeTarget) gain a num_enum(catch_all) Other(u8) variant, so an unknown wire value carries its raw byte instead of dropping the event.
  • Lod is also written back to the device via SetDpiParameters, so making it catch_all would let an invalid lift-off value be written. It stays a closed enum — keeping illegal values unrepresentable — and only its event field becomes Option<Lod>. The write and response paths stay strict: unknown values there still surface UnsupportedResponse.
  • RawWheelResolution and IlluminationState are masked to their full value range at the call site and cannot fail, so they are left unchanged.

Unknown event sub-ids still return None: an unknown event kind has no payload shape to decode, unlike an unknown field within a known event.

Changes

  • crates/openlogi-hidpp: tolerant decode for the crown, extended_dpi, illumination, and rgb_effects event decoders; Lod event field is now Option<Lod>.

Testing

  • cargo test -p openlogi-hidpp — 161 passed. New totality tests sweep 0..=255 across each enum field position to prove a known sub-id never drops, plus the two prior "unknown → dropped" tests are flipped to assert the value is preserved. The strict response-path test (rejects_unknown_lod_value) stays green.
  • cargo clippy -p openlogi-hidpp --all-targets and cargo fmt --check clean.
  • Not runtime-tested on hardware — no physical device to exercise the unknown-value paths.

Fixes #382

Event listener closures have no error channel, so a single unrecognised
field byte made the whole event decode return None — a crown update with
an unknown gesture, for example, lost its valid rotation deltas too.

Sweep the remaining event decoders (crown, extended_dpi, illumination,
rgb_effects) onto the tolerant pattern AprilNEA#381 established, choosing the
idiom per how the enum is used:

- Event-read-only enums gain a num_enum(catch_all) Other(u8) variant so
  an unknown wire value carries its raw byte instead of dropping the event.
- Lod is also written back to the device via SetDpiParameters, so it must
  stay a closed enum to keep invalid lift-off values unrepresentable; its
  event field becomes Option<Lod> instead, leaving the write and response
  paths strict (unknown values there still surface UnsupportedResponse).

Unknown event sub-ids still return None: an unknown event kind has no
payload shape to decode, unlike an unknown field within a known event.

RawWheelResolution and IlluminationState are masked to their full value
range at the call site and cannot fail, so they are left unchanged.
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR applies the tolerant-decode pattern across the remaining HID++ event decoders so that a single unrecognised enum byte no longer silently drops a whole event. The strategy is consistently applied: enums used only in event reading gain a catch_all Other(u8) variant via FromPrimitive, while enums that are also written to the device (Lod, DpiDirection, PowerModeTarget) stay as closed TryFromPrimitive enums and their event fields become Option<_>.

  • Crown (0x4600): RotationState, ActivityState, CrownGesture, and ButtonState all gain Other(u8); the decoder switches from try_from().ok()? to infallible from().
  • ExtendedDpi (0x2202): DpiParametersChanged::lod and DpiCalibrationCompleted::direction become Option<Lod> / Option<DpiDirection>; write-path APIs remain strict.
  • Illumination (0x1990) / RgbEffects (0x8071): BrightnessClampedSource and ActivityEventType get catch_all Other(u8); PowerModeTarget (packed into a 2-bit field) stays closed with the event field as Option<PowerModeTarget>. Each changed path is covered by a new 0–255 totality sweep test.

Confidence Score: 5/5

Safe to merge; the change tightens event-decode robustness without touching any request-encoding or write path.

All write-path APIs remain strict (Lod, DpiDirection, PowerModeTarget keep their closed TryFromPrimitive enums), the tolerant paths are gated behind event-only types, and every changed decode position is covered by a 0..=255 totality sweep. The only finding is a stale doc-comment in extended_dpi/event.rs — the code itself is correct.

The doc-comment on decode_event in crates/openlogi-hidpp/src/feature/extended_dpi/event.rs still describes the old drop-on-unknown-value behaviour and should be updated.

Important Files Changed

Filename Overview
crates/openlogi-hidpp/src/feature/crown/event.rs Converts four crown event enums from TryFromPrimitive to FromPrimitive with catch_all Other(u8); event decode path switches from ok()? to from(), eliminating all silent drops for sub-id 0.
crates/openlogi-hidpp/src/feature/crown/tests.rs Renames the old dropped-unknown-enum test to confirm the event is preserved, and adds a full 0..=255 sweep across all five enum-field positions for sub-id 0.
crates/openlogi-hidpp/src/feature/extended_dpi/event.rs Lod and DpiDirection event fields become Option<_> via try_from().ok(); the decode_event doc-comment still claims None is returned for unsupported enum values, which is now incorrect.
crates/openlogi-hidpp/src/feature/extended_dpi/tests.rs Updates existing assertions to Some(Lod::Medium) / Some(DpiDirection::Y), flips the drop test, and adds a totality sweep over both enum-field positions for sub-ids 0 and 1.
crates/openlogi-hidpp/src/feature/illumination/event.rs BrightnessClampedSource field now uses FromPrimitive::from() instead of try_from().ok()?, preserving the clamp event for any source byte.
crates/openlogi-hidpp/src/feature/illumination/tests.rs Adds a full 0..=255 totality sweep for sub-id 4 (BrightnessClamped), asserting the brightness sibling field survives every source byte.
crates/openlogi-hidpp/src/feature/illumination/types.rs BrightnessClampedSource gains FromPrimitive + catch_all Other(u8) variant; TryFromPrimitive retained for any write-path consumers.
crates/openlogi-hidpp/src/feature/rgb_effects/event.rs UserActivity now uses ActivityEventType::from(); ClusterChanged power_mode becomes Option via try_from().ok(), addressing the previously missing ClusterChanged sweep.
crates/openlogi-hidpp/src/feature/rgb_effects/tests.rs Adds two totality tests: a 0..=255 sweep for UserActivity (sub-id 1) and a full flags-byte sweep for ClusterChanged (sub-id 2), covering the gap noted in the prior review thread.
crates/openlogi-hidpp/src/feature/rgb_effects/types.rs ActivityEventType converted to FromPrimitive with catch_all Other(u8); PowerModeTarget correctly left as a closed TryFromPrimitive enum since it lives in a 2-bit packed field.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Raw HID++ event payload] --> B{Known sub-id?}
    B -- No --> C[return None]
    B -- Yes --> D{Enum field type?}
    D -- catch_all FromPrimitive --> E[Known value gets concrete variant, unknown value gets Other byte]
    E --> F[Some Event always returned]
    D -- Closed TryFromPrimitive --> G{try_from succeeds?}
    G -- Yes --> H[field is Some variant]
    G -- No --> I[field is None]
    H --> J[Some Event with field Some]
    I --> K[Some Event with field None]
    F --> L[Caller receives event with all sibling fields intact]
    J --> L
    K --> L
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Raw HID++ event payload] --> B{Known sub-id?}
    B -- No --> C[return None]
    B -- Yes --> D{Enum field type?}
    D -- catch_all FromPrimitive --> E[Known value gets concrete variant, unknown value gets Other byte]
    E --> F[Some Event always returned]
    D -- Closed TryFromPrimitive --> G{try_from succeeds?}
    G -- Yes --> H[field is Some variant]
    G -- No --> I[field is None]
    H --> J[Some Event with field Some]
    I --> K[Some Event with field None]
    F --> L[Caller receives event with all sibling fields intact]
    J --> L
    K --> L
Loading

Reviews (2): Last reviewed commit: "fix(hidpp): keep DpiDirection and PowerM..." | Re-trigger Greptile

Comment thread crates/openlogi-hidpp/src/feature/extended_dpi/types.rs Outdated
Comment thread crates/openlogi-hidpp/src/feature/rgb_effects/tests.rs
…fety

Both are also written back to the device — DpiDirection via the DPI
request encoders and PowerModeTarget via set_rgb_cluster_effect — so
giving them a catch_all Other(u8) variant would let an unknown value
round-trip onto the wire, the same hazard that kept Lod closed. Treat
them like Lod: the enum stays closed and only the event field becomes
Option. Also add a totality sweep for the ClusterChanged power-mode
nibble, which the 2-bit field can carry values the enum does not model.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sweep the remaining hidpp event decoders onto the tolerant-decode pattern

1 participant