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

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. Here, the known_features! macro constructs a static HashMap<u16, KnownFeature> that associates each Logitech-defined ID with its corresponding Rust type.

// 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) 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:

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:

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:

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:

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:

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:

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

Summary

  • Centralized Registry: The known_features! macro in 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 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, 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, macos.rs, and 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →