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

> Discover how OpenLogi uses HID++ features like DPI cycle and SmartShift control, communicating directly with Logitech hardware via feature IDs 0x2201 and 0x2110.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
// 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`

```rust
// 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:

```rust
// 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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`):

```rust
// 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

```rust
// 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"):

```rust
// 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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.