# How DPI Presets and Cycle/Set-Preset Actions Control Hardware in OpenLogi

> Learn how OpenLogi's DPI presets and cycle actions control hardware. Discover how UI interactions become HID++ 2.0 commands to write sensitivity values directly to mouse firmware via an in-memory cycle map.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-08-26

---

**OpenLogi translates UI interactions with DPI presets into HID++ 2.0 protocol commands that write sensitivity values directly to mouse firmware, using an in-memory cycle map to manage rotation through preset lists.**

OpenLogi is an open-source configuration platform for Logitech mice that implements **DPI presets and cycle/set-preset actions** to manage hardware sensitivity settings. When users select a preset or cycle through options in the desktop interface, the application coordinates between UI components, an agent runtime, and low-level device drivers to modify firmware settings. This article traces the complete data flow from the configuration state in `AppState` to the hardware-level command dispatch.

## Preset Storage and State Management

OpenLogi maintains DPI presets in per-device application state using the `AppState` struct. The system retrieves the current preset list via `AppState::dpi_presets()` and updates it through `AppState::commit_dpi_presets()`, which modifies the in-memory configuration without persisting to TOML files.

In [`crates/openlogi-desktop/src/state/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/dpi.rs), the `commit_dpi_presets` function accepts a vector of DPI values and replaces the stored list. This operation updates the runtime state immediately but does not write the configuration to disk, meaning presets survive for the session duration only.

```rust
// crates/openlogi-desktop/src/state/dpi.rs
pub fn commit_dpi_presets(&self, presets: Vec<u16>) {
    self.config.dpi_presets.replace(presets);
    // Triggers UI reload but no disk persistence
}

```

## Action Dispatch from UI Components

The desktop UI renders DPI selection chips that emit specific actions when clicked. Selecting a specific preset generates `Action::SetDpiPreset(idx)`, while the cycle button emits `Action::CycleDpiPresets`. These actions are bound to hardware inputs through the `ThumbwheelPreset::CycleDpi` variant defined in the thumbwheel configuration.

In [`crates/openlogi-desktop/src/features/mouse/thumbwheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/mouse/thumbwheel.rs), the preset mapping connects physical thumbwheel gestures to the cycle action. When the user activates this binding, the system dispatches the action to the agent core for hardware execution.

```rust
// crates/openlogi-desktop/src/features/mouse/thumbwheel.rs
pub enum ThumbwheelPreset {
    CycleDpi, // Maps to Action::CycleDpiPresets
    // ... other presets
}

```

## Agent Runtime Processing

The **agent core** receives actions through `Runtime::handle_action` in [`crates/openlogi-agent-core/src/runtime.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/runtime.rs). For preset-specific actions, the runtime resolves the target DPI value either by indexing into the stored preset list or by consulting the **cycle map**.

When handling `Action::SetDpiPreset(idx)`, the runtime validates the index against `self.state.dpi_presets()`, retrieves the corresponding DPI value, and calls `set_dpi()`. For `Action::CycleDpiPresets`, the runtime uses `self.dpi_cycle.write().next()` to advance a circular iterator through the preset list before writing the value.

```rust
// crates/openlogi-agent-core/src/runtime.rs
match action {
    Action::CycleDpiPresets => {
        let next = self.dpi_cycle.write().next();
        self.set_dpi(next).await?;
    }
    Action::SetDpiPreset(idx) => {
        let dpi = self.state.dpi_presets()
            .get(idx)
            .cloned()
            .ok_or(InvalidPreset)?;
        self.set_dpi(dpi).await?;
    }
}

```

## Hardware Communication via HID++

Once the runtime determines the target DPI, it delegates hardware communication to the device layer. The agent server exposes `set_dpi` as an IPC endpoint in [`crates/openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/server.rs), which forwards the request to `openlogi_hid::set_dpi_on`.

The low-level implementation in [`crates/openlogi-device/src/write/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/write/dpi.rs) opens a HID++ 2.0 channel to the mouse and transmits the **Set DPI command (`0x2201`)**. This command writes the sensitivity value directly to the device firmware, causing immediate hardware behavior changes.

```rust
// crates/openlogi-agent/src/server.rs
async fn set_dpi(self, _: Context, route: DeviceRoute, dpi: Dpi) 
    -> Result<(), WriteError> {
    openlogi_hid::set_dpi_on(&route, dpi).await
}

```

## The DPI Cycle Mechanism

The **cycle map** maintains state across multiple cycle actions, ensuring each press advances to the next preset in the list. This map lives in the agent core and updates atomically when `CycleDpiPresets` is processed.

The [`capture_plan.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/capture_plan.rs) file in the agent core coordinates action routing, ensuring that both `CycleDpiPresets` and `SetDpiPreset` actions trigger the appropriate state updates and hardware writes. After a successful write, the cycle map advances its internal pointer, wrapping to the first preset after reaching the list end.

```rust
// crates/openlogi-agent-core/src/capture_plan.rs
if matches!(action, Action::CycleDpiPresets | Action::SetDpiPreset(_)) {
    // Routes to Runtime::handle_action for DPI resolution
}

```

## Code Implementation Examples

### Reading and Updating Presets

To programmatically modify the preset list from the desktop UI:

```rust
// crates/openlogi-desktop/src/features/pointer/dpi.rs
let mut presets = state.dpi_presets();
presets.push(current_dpi);           // Add current setting as preset
state.commit_dpi_presets(presets);   // Update in-memory state

```

### Implementing Custom Cycle Logic

For agent-side extensions that interact with the preset system:

```rust
// In Runtime implementation (crates/openlogi-agent-core/src/runtime.rs)
fn advance_and_set(&self) -> Result<(), Error> {
    let presets = self.state.dpi_presets();
    if presets.is_empty() {
        return Err(Error::NoPresets);
    }
    let next_idx = self.dpi_cycle.write().next_index(presets.len());
    let dpi = presets[next_idx];
    self.set_dpi(dpi)
}

```

### Direct HID++ Command Construction

For hardware-level debugging or custom device support:

```rust
// Conceptual usage of crates/openlogi-device/src/write/dpi.rs
let command = HidppCommand::new(0x2201, dpi_value.to_le_bytes());
device.send_feature_report(&command)?;

```

## Summary

- **DPI presets** reside in `AppState` per-device configuration, accessible via `dpi_presets()` and mutable through `commit_dpi_presets()` in [`crates/openlogi-desktop/src/state/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/dpi.rs).
- The **thumbwheel preset** `ThumbwheelPreset::CycleDpi` in [`thumbwheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/thumbwheel.rs) binds physical inputs to `Action::CycleDpiPresets` or `Action::SetDpiPreset(idx)`.
- The **agent runtime** in [`crates/openlogi-agent-core/src/runtime.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/runtime.rs) resolves actions by indexing into preset lists or advancing the cycle map (`dpi_cycle`).
- **Hardware writes** occur through `openlogi_hid::set_dpi_on`, which dispatches the HID++ 2.0 `0x2201` command via [`crates/openlogi-device/src/write/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/write/dpi.rs).
- Presets exist only in memory for the session duration; the cycle map maintains state between consecutive cycle actions but does not persist across application restarts.

## Frequently Asked Questions

### Where does OpenLogi store DPI presets between sessions?

OpenLogi stores DPI presets exclusively in memory via the `AppState` configuration. The `commit_dpi_presets()` function in [`crates/openlogi-desktop/src/state/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/dpi.rs) updates the running configuration without writing to TOML files. Users must recreate preset lists each session unless they modify the source to implement persistence.

### What HID++ protocol command does OpenLogi use to change mouse DPI?

OpenLogi uses the HID++ 2.0 **Set DPI command (`0x2201`)**, implemented in [`crates/openlogi-device/src/write/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/write/dpi.rs). This command writes the sensitivity value directly to the mouse firmware through a feature report sent over the HID interface.

### How does the cycle action determine which preset to select next?

The agent maintains an in-memory **cycle map** (`dpi_cycle`) that tracks the current position in the preset list. When processing `Action::CycleDpiPresets`, `Runtime::handle_action` calls `dpi_cycle.write().next()` to advance the iterator, wrapping to the first preset after reaching the end of the list stored in `AppState`.

### Can DPI presets be triggered programmatically without the UI?

Yes, any code with access to the agent runtime can trigger preset changes by sending `Action::SetDpiPreset(idx)` or `Action::CycleDpiPresets` to the runtime's action handler. Third-party integrations can also call the IPC `set_dpi` endpoint directly in [`crates/openlogi-agent/src/server.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/server.rs) to bypass the preset system and set arbitrary DPI values.