How DPI Presets and Cycle/Set-Preset Actions Control Hardware in OpenLogi
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, 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.
// 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, 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.
// 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. 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.
// 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, which forwards the request to openlogi_hid::set_dpi_on.
The low-level implementation in 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.
// 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 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.
// 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:
// 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:
// 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:
// 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
AppStateper-device configuration, accessible viadpi_presets()and mutable throughcommit_dpi_presets()incrates/openlogi-desktop/src/state/dpi.rs. - The thumbwheel preset
ThumbwheelPreset::CycleDpiinthumbwheel.rsbinds physical inputs toAction::CycleDpiPresetsorAction::SetDpiPreset(idx). - The agent runtime in
crates/openlogi-agent-core/src/runtime.rsresolves 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.00x2201command viacrates/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 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. 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 to bypass the preset system and set arbitrary DPI values.
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 →