How OpenLogi Implements DPI Cycle and SmartShift Control Using HID++ Features

OpenLogi implements DPI cycle and SmartShift control by communicating directly with Logitech hardware through the HID++ 2.0 protocol, using feature IDs 0x2201 and 0x2202 for DPI management and 0x2110 and 0x2111 for SmartShift wheel modes, with automatic fallback logic and an IPC facade that abstracts these protocol details from the desktop UI.

OpenLogi is an open-source Rust project that provides advanced control over Logitech peripherals by leveraging the proprietary HID++ 2.0 protocol. The implementation of DPI cycle and SmartShift control demonstrates how the codebase handles hardware abstraction, feature detection, and error recovery while maintaining a clean API for desktop clients.

Understanding HID++ Feature IDs for DPI and SmartShift

OpenLogi interacts with Logitech devices through specific HID++ feature identifiers that determine available capabilities:

Feature HID++ ID Description
Adjustable DPI 0x2201 Classic single-value DPI list
Extended DPI 0x2202 Independent X/Y axes with lift-off distance control
SmartShift Legacy 0x2110 Basic ratchet/free-spin wheel mode
SmartShift Enhanced 0x2111 Advanced mode with auto-disengage threshold and tunable torque

The architecture automatically detects which features a device supports and selects the appropriate implementation path.

Implementing DPI Cycle Control

Feature Detection and Fallback Logic

In openlogi-device/src/write/dpi.rs, the DpiFeature enum attempts to open the Adjustable DPI feature (0x2201) first, falling back to Extended DPI (0x2202) if unavailable:

// openlogi-device/src/write/dpi.rs:46-60
async fn open(device: &mut Device) -> Result<Self, WriteError> {
    if let Some(index) = feature_index(device, AdjustableDpiFeature::ID).await? {
        return Ok(Self::Adjustable(device.add_feature(index)));
    }
    if let Some(index) = feature_index(device, ExtendedDpiFeature::ID).await? {
        return Ok(Self::Extended(device.add_feature(index)));
    }
    Err(WriteError::FeatureUnsupported { … })
}

This ensures compatibility across both older and newer Logitech hardware without requiring device-specific configuration.

Reading Current DPI and Supported Values

The abstraction layer provides uniform methods for querying device state regardless of the underlying feature version:

  • current_dpi() retrieves the active DPI value (X-axis for Extended features)
  • supported_dpi() returns a flattened Vec<u16> of available values, expanding range descriptions for Extended DPI devices via expand_dpi_ranges
// openlogi-device/src/write/dpi.rs:84-110
async fn current_dpi(&self) -> Result<Dpi, …> { … }
async fn supported_dpi(&self) -> Result<Vec<u16>, …> { … }

Writing New DPI Values with Preservation

When cycling DPI, set_dpi() handles the protocol-specific requirements of each feature type. For Extended DPI devices, the implementation first reads the full parameter set to preserve lift-off distance and Y-axis values:

// openlogi-device/src/write/dpi.rs:112-140
async fn set_dpi(&self, dpi: Dpi) -> Result<(), …> {
    match self {
        Self::Adjustable(f) => f.set_sensor_dpi(SENSOR, dpi.into()).await,
        Self::Extended(f) => {
            let cur = f.get_sensor_dpi_parameters(SENSOR).await?;
            f.set_sensor_dpi_parameters(
                SENSOR,
                SetDpiParameters {
                    dpi_x: dpi.into(),
                    dpi_y: if cur.dpi_y == 0 { 0 } else { dpi.into() },
                    lod: cur.lod,
                })
            .await
        }
    }
}

IPC Facade for UI Integration

The openlogi-ipc crate exposes async helpers that the GUI and CLI use to interact with device capabilities:

// openlogi-ipc/src/ipc.rs:456-459
async fn set_dpi(route: DeviceRoute, dpi: Dpi) { … }
async fn get_dpi(route: DeviceRoute) -> Result<Dpi, WriteError> { … }

The desktop UI calls get_dpi to display the current value, constructs the selection list from supported_dpi, and advances to the next entry to achieve DPI cycling.

SmartShift Control Implementation

Enhanced vs Legacy Feature Detection

In openlogi-device/src/write/smartshift.rs, the SmartShift enum prioritizes the Enhanced feature (0x2111) while implementing retry logic for race conditions before falling back to Legacy (0x2110):

// openlogi-device/src/write/smartshift.rs:19-33
async fn open(device: &mut Device) -> Result<Self, WriteError> {
    match open_feature::<SmartShiftEnhancedFeature>(device).await {
        Ok(f) => Ok(Self::Enhanced(f)),
        Err(err) if is_missing_enhanced(&err) => {
            // retry, then fallback
        }
        Err(err) => Err(err),
    }
}

Reading Wheel Mode and Auto-Disengage Status

The status() method normalizes responses from both feature versions into a unified SmartShiftStatus struct:

  • mode indicates Free-spin or Ratchet state
  • auto_disengage stores the threshold value
  • tunable_torque appears only for Enhanced devices
// openlogi-device/src/write/smartshift.rs:35-71
async fn status(&self) -> Result<SmartShiftStatus, WriteError> {
    match self {
        Self::Enhanced(f) => { … }
        Self::Legacy(f) => { … }
    }
}

Writing Status Changes with Transient Error Handling

The set_status() method constructs protocol-specific change requests, omitting unsupported fields for Legacy devices (which the hardware interprets as "preserve current value"):

// openlogi-device/src/write/smartshift.rs:73-94
async fn set_status(&self, status: SmartShiftStatus) -> Result<(), WriteError> {
    match self {
        Self::Enhanced(f) => f.set_ratchet_control_mode( … ).await,
        Self::Legacy(f) => f.set_ratchet_control_mode( … ).await,
    }
}

A helper function is_transient_smartshift_error identifies recoverable failures, allowing UI components to retry after TRANSIENT_RETRY_DELAY.

IPC Interface for SmartShift Operations

The IPC layer mirrors the DPI API pattern:

// openlogi-ipc/src/ipc.rs:463-471
async fn set_smartshift(route: DeviceRoute, status: SmartShiftStatus) { … }
async fn read_smartshift(route: DeviceRoute) -> Result<SmartShiftStatus, WriteError> { … }

End-to-End DPI Cycling Workflow

When a user initiates a DPI cycle through the OpenLogi desktop client, the system executes the following sequence:

  1. Query current state via ipc::get_dpi to determine the active DPI value
  2. Retrieve capabilities by calling device::write::dpi::supported_dpi to get the sorted list of available values
  3. Calculate next value by finding the current index in the supported list, wrapping to the first entry if at the end
  4. Apply new DPI through ipc::set_dpi, which routes to the appropriate HID++ feature writer

For SmartShift toggles, the UI calls ipc::read_smartshift to populate the interface, then sends modified SmartShiftStatus structs via ipc::set_smartshift when users change modes or adjust the auto-disengage threshold.

Summary

  • OpenLogi uses HID++ feature IDs 0x2201 and 0x2202 to handle both basic and extended DPI capabilities, automatically selecting the appropriate implementation
  • SmartShift control supports both legacy (0x2110) and enhanced (0x2111) features, with retry logic for race conditions during device initialization
  • The write layer in openlogi-device preserves device-specific parameters (like lift-off distance) when updating related values, preventing unintended side effects
  • IPC facades in openlogi-ipc provide protocol-agnostic async functions that enable DPI cycling and SmartShift control from the desktop UI without exposing HID++ complexity

Frequently Asked Questions

What HID++ feature IDs does OpenLogi use for DPI and SmartShift control?

OpenLogi uses 0x2201 for Adjustable DPI (single-value lists) and 0x2202 for Extended DPI (independent X/Y with lift-off distance). For SmartShift, it uses 0x2110 for legacy ratchet/free-spin control and 0x2111 for enhanced mode with auto-disengage thresholds and tunable torque settings.

How does OpenLogi handle devices that only support legacy SmartShift?

The SmartShift::open function in openlogi-device/src/write/smartshift.rs first attempts to connect to the Enhanced feature (0x2111). If that fails with a specific missing-feature error, it retries once to work around race conditions, then automatically falls back to the Legacy feature (0x2110). When writing status changes, the implementation omits enhanced-specific fields for legacy devices, which the hardware interprets as commands to preserve current values.

Why does setting DPI on Extended DPI devices require reading parameters first?

Devices using Extended DPI (0x2202) manage multiple parameters simultaneously: X-axis DPI, Y-axis DPI, and lift-off distance. The set_dpi implementation calls get_sensor_dpi_parameters first to retrieve the current Y-axis value and lift-off setting, then writes a complete SetDpiParameters struct. This prevents the operation from inadvertently resetting the Y-axis or lift-off distance while updating the X-axis DPI.

How do the desktop UI and device agent communicate for DPI cycling?

The architecture uses the openlogi-ipc crate as a communication bridge. The desktop client calls ipc::get_dpi to retrieve the current value and ipc::set_dpi to apply changes. These IPC functions translate to device operations in openlogi-device, which handle the actual HID++ protocol communication through the hidpp crate.

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 →