How OpenLogi Handles DPI Cycling and Preset Management: A Technical Deep Dive
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, 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:
- Device-side discovery – Reading sensor capabilities and current values via the HID++ protocol
- State-side persistence – Managing user-defined presets and caching values in the UI layer
- 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, 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.
// 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, 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.
// 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 and updates the shared hook map (self.pointer.reads) so that the next Cycle DPI Presets press sees the updated list.
// 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. 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.
// 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, thepreset_chiphandler callsstate.commit_dpi(value), updating both the local state and sendingSetDpito the hardware. -
Adding New Presets – Clicking the "+" chip in the UI (defined in
crates/openlogi-desktop/src/features/mouse/thumbwheel.rs) snapshots the current DPI viastate.dpi()and appends it to the in-memory list before callingstate.commit_dpi_presets(presets). -
Configuration Persistence – The
commit_dpi_presetsfunction persists the new list inconfig.tomlviaconfig.set_dpi_presets(&key, presets), then triggerspersist_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
SetDpiHID++ command through the IPC layer. -
Immediate Feedback – The device caches the new value locally, and the UI receives
StateEvent::DpiChangedto update the displayed preset highlight without polling the hardware.
Summary
- OpenLogi separates DPI concerns into hardware discovery (
openlogi-core/src/hid/dpi.rs), UI state management (openlogi-desktop/src/state/dpi.rs), and runtime cycling (openlogi-agent-core/src/runtime.rs). - Read caching via
pointer.readsandDpiStatus::Ready(info)eliminates redundant hardware queries while keeping the UI synchronized. - Preset persistence stores user-defined lists in
config.tomland 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 whenAction::CycleDpiPresetstriggers.
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. 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 file. The commit_dpi_presets function in 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. 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 renders preset chips that reflect the current value stored in the state layer, ensuring immediate visual feedback without polling the hardware directly.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →