# How OpenLogi Manages HID++ Feature IDs for Buttons and DPI

> Discover how OpenLogi manages HID++ feature IDs like DPI and buttons using a type-safe Rust registry for dynamic device capability discovery.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-29

---

**OpenLogi maps raw 16-bit Logitech HID++ feature IDs to type-safe Rust implementations via a centralized registry, enabling dynamic discovery of device capabilities like DPI control and button handling.**

OpenLogi is an open-source Rust application for managing Logitech HID++ 2.0 devices. Instead of hardcoding hex values throughout the codebase, it treats every capability—from sensor configuration to button remapping—as a **feature** identified by a 16-bit ID, abstracting protocol details behind version-aware Rust structs.

## The Feature Registry Architecture

The mapping between numeric HID++ feature IDs and their implementations lives in [`crates/openlogi-hidpp/src/feature/registry.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/registry.rs). Here, the `known_features!` macro constructs a static `HashMap<u16, KnownFeature>` that associates each Logitech-defined ID with its corresponding Rust type.

```rust
// Excerpt from the registry showing DPI and smart-shift mappings
0x2201 "AdjustableDpi" => AdjustableDpiFeature,
0x2202 "ExtendedAdjustableDpi" => ExtendedDpiFeature,
0x2110 "SmartShiftWheel" => SmartShiftFeature,

```

The registry exposes two primary query methods: `lookup(feature_id)` returns the base feature metadata, while `lookup_version(feature_id, version)` accounts for protocol revisions. Each entry stores a `FeatureImplProducer` and a `STARTING_VERSION` constant, allowing the system to select the appropriate implementation when a device reports its supported feature set.

## Dynamic Feature Instantiation

When the OpenLogi agent discovers a device, it iterates over the `(feature_id, version)` pairs returned by the HID++ *FeatureInfo* request. For each ID, the agent queries the registry and invokes `new_dyn` to construct a concrete feature object wrapped in `Arc<dyn Feature>`.

This architecture provides the UI layer with type-safe handles to device capabilities without compile-time knowledge of which specific features a given mouse or keyboard supports. The numeric HID++ feature IDs remain encapsulated within the transport layer, while higher-level code interacts with ergonomic methods like `get_sensor_dpi()` or `set_dpi()`.

## DPI Feature Implementation

OpenLogi manages sensor resolution through two distinct HID++ feature IDs, each with dedicated Rust structs in the `crates/openlogi-hidpp/src/feature/` directory:

| Feature ID | Rust Struct | Capabilities |
|------------|-------------|--------------|
| `0x2201` | `AdjustableDpiFeature` | Query sensor count, read/write single DPI values, retrieve supported DPI lists |
| `0x2202` | `ExtendedDpiFeature` | Full `AdjustableDpi` functionality plus sensor-wide range queries, calibration data, and multi-sensor orchestration |

Both structs derive the `Feature` trait via procedural macros and implement `FeatureEndpoint`, which handles the low-level encoding of HID++ commands. For example, `AdjustableDpiFeature::get_sensor_dpi` (defined around lines 41–50 of [`adjustable_dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/adjustable_dpi.rs)) constructs a command with function ID `0x02` (GET_DPI), appends the sensor index, and decodes the two-byte big-endian response into the actual DPI value.

The `parse_dpi_list_payload` helper (lines 63–108) expands the firmware's compact range encoding—often expressed as stepped intervals—into a complete list of supported resolutions for UI display.

## Button ID Abstraction and Platform Hooks

While DPI features use direct HID++ protocol access, button handling requires translating between three domains: HID++ button numbers, OS-specific raw input codes, and OpenLogi's internal `ButtonId` enum.

The canonical button definitions reside in [`crates/openlogi-core/src/binding/button.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding/button.rs):

```rust
pub enum ButtonId {
    LeftClick,   // Primary click - never remapped by OS hook
    RightClick,  // Secondary click - never remapped by OS hook
    MiddleClick,
    Back,
    Forward,
    DpiToggle,
    // ... thumb-wheel, gesture buttons, keyboard F-row keys
}

```

Platform-specific hook implementations perform the translation from raw OS button numbers to these variants:
- **Linux:** `key_to_button` in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs) maps `evdev` key codes to `ButtonId`
- **macOS:** `button_number_to_id` in [`crates/openlogi-hook/src/macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/macos.rs) handles IOKit button indices
- **Windows:** The Windows hook implementation performs equivalent translation for raw input messages

### OS Hook vs. HID++ Gesture Sources

Not all buttons pass through the operating system's input hook. The `ButtonId` enum provides two critical classification methods:

- **`is_os_hook_button()`**: Returns `true` only for `MiddleClick`, `Back`, and `Forward`. These three buttons are intercepted by the platform-specific OS hooks and can be remapped at the system level.
- **`is_hidpp_gesture_source()`**: Returns `true` for buttons like `DpiToggle`, thumb-wheel clicks, and the gesture button. These events are captured directly from the HID++ event stream and processed as native device gestures, bypassing the OS input queue.

## Runtime Device Discovery Flow

The complete lifecycle of HID++ feature ID management follows these steps:

1. **Enumeration**: The agent opens an HID++ channel and issues a *FeatureInfo* request to retrieve the device's capability list.
2. **Registry Lookup**: For each `(feature_id, version)` pair, the system calls `lookup_version` to retrieve the appropriate `KnownFeature` metadata.
3. **Instantiation**: The `FeatureImplProducer` creates the concrete implementation (e.g., `AdjustableDpiFeature`) wrapped in `Arc<dyn Feature>`.
4. **Command Execution**: Higher-level code invokes async methods (e.g., `dpi_feature.get_sensor_dpi(0).await`), which encode HID++ payloads via `FeatureEndpoint`, transmit them through the transport layer, and decode responses.
5. **Button Routing**: Raw button events from HID++ *Event* features or OS hooks are converted to `ButtonId` variants and routed to the action system for remapping, gesture recognition, or DPI cycling.

## Working with OpenLogi HID++ Features in Code

To query the current DPI of the first sensor:

```rust
let dpi_feature = device.get_feature::<AdjustableDpiFeature>()?;
let current_dpi = dpi_feature.get_sensor_dpi(0).await?;
println!("Current DPI: {}", current_dpi);

```

To retrieve all supported DPI values for a sensor:

```rust
let dpi_list = dpi_feature.get_sensor_dpi_list(0).await?;
println!("Supported DPIs: {:?}", dpi_list);

```

On Linux platforms, convert a raw `evdev` key code to a `ButtonId`:

```rust
let raw = KeyCode::BTN_SIDE;  // Example raw code from evdev
if let Some(button) = key_to_button(raw) {
    println!("Button pressed: {}", button);
}

```

To determine if a button should be handled by the OS hook layer:

```rust
if button.is_os_hook_button() {
    // Forward to OS-level event dispatcher for remapping
}

```

## Summary

- **Centralized Registry**: The `known_features!` macro in [`registry.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/registry.rs) provides a static mapping of 16-bit Logitech HID++ feature IDs to Rust implementations.
- **Version Awareness**: The `STARTING_VERSION` constant and `lookup_version` method ensure backward compatibility when devices report older protocol revisions.
- **DPI Abstraction**: Feature IDs `0x2201` and `0x2202` map to `AdjustableDpiFeature` and `ExtendedDpiFeature`, providing type-safe sensor control with methods like `get_sensor_dpi`.
- **Button Translation**: The `ButtonId` enum in [`binding/button.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding/button.rs) abstracts physical buttons, with platform-specific hooks (`key_to_button`, `button_number_to_id`) translating raw OS codes.
- **Dual Handling**: Only `MiddleClick`, `Back`, and `Forward` are OS-hookable; other buttons like `DpiToggle` are processed as native HID++ gesture sources.

## Frequently Asked Questions

### What is a HID++ feature ID in OpenLogi?

A HID++ feature ID is a 16-bit hexadecimal identifier defined by Logitech's HID++ 2.0 protocol that represents a specific device capability, such as adjustable DPI (`0x2201`) or smart shift wheel (`0x2110`). OpenLogi maps these numeric IDs to concrete Rust structs via the feature registry in [`crates/openlogi-hidpp/src/feature/registry.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/registry.rs), allowing the application to work with type-safe abstractions rather than raw hex values.

### How does OpenLogi handle different versions of the same HID++ feature?

According to the AprilNEA/OpenLogi source code, each feature implementation specifies a `STARTING_VERSION` constant within the registry. The `lookup_version(feature_id, version)` method compares the device's reported version against this constant to select the most appropriate implementation. This ensures that older devices receive compatible command sequences while newer devices can access extended capabilities like calibration data in `ExtendedDpiFeature`.

### Can any mouse button be remapped through the OS hook layer?

No. Only three buttons return `true` from `ButtonId::is_os_hook_button()`: **MiddleClick**, **Back**, and **Forward**. These are the only buttons intercepted by the platform-specific OS hooks found in [`crates/openlogi-hook/src/linux.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/linux.rs), [`macos.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/macos.rs), and [`windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/windows.rs). All other buttons—including `DpiToggle`, thumb-wheel clicks, and gesture buttons—are classified as HID++ gesture sources via `is_hidpp_gesture_source()` and must be handled through the device's native HID++ event stream rather than the OS input queue.

### Where does the actual DPI value decoding occur in the source code?

The decoding logic resides in `AdjustableDpiFeature::get_sensor_dpi` within [`crates/openlogi-hidpp/src/feature/adjustable_dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/adjustable_dpi.rs) (specifically around lines 41–50). This method constructs an HID++ command with function ID `0x02` (GET_DPI), transmits it through the `FeatureEndpoint`, and parses the two-byte big-endian response payload into the final DPI integer. For lists of supported DPIs, the `parse_dpi_list_payload` helper (lines 63–108) decompresses the firmware's compact range encoding.