# How OpenLogi Handles DPI Cycling and Preset Management: A Technical Deep Dive

> Discover how OpenLogi handles DPI cycling and preset management via its three-layer architecture. Learn about HID++ discovery, UI-state persistence, and agent core actions for seamless control.

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

---

**OpenLogi implements DPI cycling and preset management through a three-layer architecture that separates device-side HID++ discovery, UI-state persistence in [`state/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/state/dpi.rs), and runtime cycling via the agent core's `Action::CycleDpiPresets` handler.**

The AprilNEA/OpenLogi project provides aRust-based driver stack for Logitech devices, with sophisticated handling of DPI (dots-per-inch) settings that separates hardware communication from UI state and runtime actions. Understanding how OpenLogi manages DPI cycling and preset management requires examining the interaction between the HID++ protocol implementation, the desktop state management layer, and the agent core runtime.

## Architecture Overview

OpenLogi divides DPI handling into three distinct concerns:

1. **Device-side discovery** – Reading sensor capabilities and current values via the HID++ protocol
2. **State-side persistence** – Managing user-defined presets and caching values in the UI layer
3. **Runtime cycling** – Executing preset rotation through IPC commands when users trigger cycle actions

This separation ensures that hardware communication remains isolated from UI logic while maintaining responsive preset switching.

## Device-Side DPI Discovery

The lowest layer of the stack handles raw hardware communication through the HID++ protocol. In [`crates/openlogi-core/src/hid/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/hid/dpi.rs), OpenLogi implements the primitives for reading and writing DPI values directly to the sensor.

### HID++ Protocol and Read Caching

When the system queries a device for its current DPI, the result is stored in a **read cache** (`pointer.reads`) that exposes the status via `DpiStatus::Ready(info)`. This caching mechanism prevents redundant hardware queries and allows the UI to reflect DPI changes immediately without waiting for a fresh HID++ transaction.

```rust
// From crates/openlogi-core/src/hid/dpi.rs
// DpiStatus exposes the current state including cached values
pub enum DpiStatus {
    Ready(DpiInfo),
    // ... other variants
}

```

## State Management and Persistence

The desktop application layer handles user interactions and persistent storage. The primary logic resides in [`crates/openlogi-desktop/src/state/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/state/dpi.rs), which coordinates between the device cache, configuration files, and the UI.

### Loading Current DPI Values

The `load_current_dpi` method triggers a read of the active device's DPI and stores it in `self.pointer.dpi`. This bridges the gap between the hardware cache and the UI state, ensuring that the displayed value matches the sensor's actual configuration.

```rust
// From crates/openlogi-desktop/src/state/dpi.rs
impl DpiState {
    pub fn load_current_dpi(&mut self, device: &Device) {
        // Reads from pointer.reads cache and updates UI state
        self.pointer.dpi = device.read_dpi_from_cache();
    }
}

```

### Committing DPI Presets

When users modify their preset list through the Actions Ring interface, `commit_dpi_presets` handles persistence. This method writes the user-defined preset list to [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) and updates the shared **hook map** (`self.pointer.reads`) so that the next *Cycle DPI Presets* press sees the updated list.

```rust
// From crates/openlogi-desktop/src/state/dpi.rs
pub fn commit_dpi_presets(&self, device_key: &str, presets: Vec<u16>) {
    // Persist to disk
    config.set_dpi_presets(device_key, &presets);
    // Update runtime hook map
    self.persist_and_reload("DPI presets");
}

```

### Applying DPI Changes

The `commit_dpi` method applies a chosen DPI value to the device via an IPC command (`SetDpi`). For transient devices, the value may remain in memory only, while persistent devices receive both the hardware command and configuration storage.

## Runtime DPI Cycling

The agent core maintains the actual cycling logic in [`crates/openlogi-agent-core/src/runtime.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/runtime.rs). This runtime component listens for input events and manages the **cycle index** (`self.dpi_cycle`) that tracks the current position in the preset list.

### The CycleDpiPresets Action Handler

When a user presses a button bound to *Cycle DPI Presets*, the runtime matches on `Action::CycleDpiPresets` and consults the cycle index to determine the next value. The implementation acquires a write lock on `self.dpi_cycle`, selects the next preset (wrapping around to the beginning when reaching the end), and dispatches a `SetDpi` IPC command to the device.

```rust
// From crates/openlogi-agent-core/src/runtime.rs
match action {
    Action::CycleDpiPresets => match self.dpi_cycle.write() {
        Ok(mut cycle) => {
            let presets = cycle.presets(); // Loaded from hook map
            let next_dpi = cycle.next_preset(); // Wraps around
            self.ipc.send(Command::SetDpi(next_dpi));
            // Emit UI update
            self.state_tx.send(StateEvent::DpiChanged(next_dpi));
        }
        // ... error handling
    },
    // ... other actions
}

```

The `dpi_cycle` object populates its preset list from the state's `dpi_presets()` method, ensuring it always reflects the most recently committed configuration.

## Preset Management Flow

The complete flow for managing and cycling DPI presets involves coordination between multiple components:

- **UI Interaction (Actions Ring)** – When clicking a DPI preset chip in [`crates/openlogi-desktop/src/features/pointer/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/pointer/dpi.rs), the `preset_chip` handler calls `state.commit_dpi(value)`, updating both the local state and sending `SetDpi` to the hardware.

- **Adding New Presets** – Clicking the "+" chip in the UI (defined in [`crates/openlogi-desktop/src/features/mouse/thumbwheel.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/mouse/thumbwheel.rs)) snapshots the current DPI via `state.dpi()` and appends it to the in-memory list before calling `state.commit_dpi_presets(presets)`.

- **Configuration Persistence** – The `commit_dpi_presets` function persists the new list in [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) via `config.set_dpi_presets(&key, presets)`, then triggers `persist_and_reload("DPI presets")` to refresh the hook map.

- **Hardware Application** – When cycling, the agent core reads the current preset list from the hook map, increments the cycle index, and issues a `SetDpi` HID++ command through the IPC layer.

- **Immediate Feedback** – The device caches the new value locally, and the UI receives `StateEvent::DpiChanged` to update the displayed preset highlight without polling the hardware.

## Summary

- OpenLogi separates DPI concerns into hardware discovery ([`openlogi-core/src/hid/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-core/src/hid/dpi.rs)), UI state management ([`openlogi-desktop/src/state/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-desktop/src/state/dpi.rs)), and runtime cycling ([`openlogi-agent-core/src/runtime.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/openlogi-agent-core/src/runtime.rs)).
- **Read caching** via `pointer.reads` and `DpiStatus::Ready(info)` eliminates redundant hardware queries while keeping the UI synchronized.
- **Preset persistence** stores user-defined lists in [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) and updates the **hook map** to make them available to the runtime cycle handler.
- **IPC commands** (`SetDpi`) bridge the desktop state and hardware, with the agent core maintaining a **cycle index** that wraps through preset lists when `Action::CycleDpiPresets` triggers.

## Frequently Asked Questions

### How does OpenLogi read the current DPI from the device?

OpenLogi queries the sensor through the HID++ protocol implemented in [`crates/openlogi-core/src/hid/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/hid/dpi.rs). The result is wrapped in `DpiStatus::Ready(info)` and stored in the `pointer.reads` cache, which the UI layer accesses through `load_current_dpi` without issuing additional hardware commands.

### Where are DPI presets stored in OpenLogi?

DPI presets persist in the user's [`config.toml`](https://github.com/AprilNEA/OpenLogi/blob/main/config.toml) file. 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) handles serialization via `config.set_dpi_presets(&key, presets)`, followed by `persist_and_reload` to update the runtime's hook map with the new list.

### What happens when I press the Cycle DPI Presets button?

The agent core captures the input and matches on `Action::CycleDpiPresets` in [`crates/openlogi-agent-core/src/runtime.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/runtime.rs). It acquires a write lock on `self.dpi_cycle`, retrieves the current preset list from the hook map, selects the next entry (wrapping to the start if necessary), and sends a `SetDpi` IPC command to the device while emitting `StateEvent::DpiChanged` to update the UI.

### How does the UI know when the DPI has changed?

The UI subscribes to `StateEvent::DpiChanged` events emitted by the agent core after successful DPI applications. Additionally, [`crates/openlogi-desktop/src/features/pointer/dpi.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-desktop/src/features/pointer/dpi.rs) renders preset chips that reflect the current value stored in the state layer, ensuring immediate visual feedback without polling the hardware directly.