How OpenLogi Configures Native Scroll Inversion Per-Device (HID++ 0x2121)

OpenLogi stores the native scroll-inversion flag in persistent device configuration and applies it via the HID++ 0x2121 HiResWheel feature, supporting both device-wide defaults and per-link overrides.

The OpenLogi daemon manages Logitech HID++ peripherals through a layered configuration system. Native scroll inversion allows users to flip scroll direction at the hardware level rather than relying on OS settings. This article examines how the invert_scroll flag flows from configuration files through to the HID++ command sent to the device.

Configuration Architecture

OpenLogi defines scroll inversion settings in crates/openlogi-core/src/config/device.rs through two complementary mechanisms.

Device-Wide Flag

The primary setting resides in the DeviceConfig struct as a boolean field:

/// Invert this device's scroll-wheel direction relative to the OS setting (issue #126)
#[serde(default, skip_serializing_if = "is_false")]
pub invert_scroll: bool,

This field defaults to false (native direction) and is omitted from config.toml when unchanged to keep configuration files tidy.

For advanced routing scenarios, the LinkOverrides struct provides route-specific control:

pub invert_scroll: Option<bool>,

Located in the same file, this allows different values for specific connection paths—such as distinguishing between a USB receiver and a direct USB cable.

Effective Value Resolution

The runtime resolves the final boolean through the effective_invert_scroll method (lines 85-92):

pub fn effective_invert_scroll(&self, route_key: &str) -> bool {
    self.link_overrides(route_key)
        .and_then(|o| o.invert_scroll)
        .unwrap_or(self.invert_scroll)
}

Per-link values shadow the device-wide default when present.

Runtime Application Flow

Changing the setting triggers a coordinated update across the desktop UI and background agent.

Desktop UI Toggle

The interface implements inversion control in crates/openlogi-desktop/src/state/scroll.rs. When a user activates the toggle, the system executes:

  1. Validation: Checks current_scroll_inversion_supported to verify the device exposes the HID++ feature.
  2. Persistence: Calls Config::set_invert_scroll to update the device entry.
  3. Reload: Invokes persist_and_reload("invert scroll") to signal the agent.

The relevant code (lines 30-49) handles the UI callback and commits the change only after confirming hardware compatibility.

Agent HID++ Command

Upon reload, the agent invokes openlogi_hid::set_scroll_inversion(route, invert).await as defined in crates/openlogi-hid/src/host.rs (lines 8-11). This facade method routes the command to the appropriate HID++ feature implementation.

HID++ 0x2121 Implementation

The actual hardware toggle uses the HiResWheel feature (feature index 0x2121) implemented in crates/openlogi-hidpp/src/feature/hires_wheel.rs.

Capability Detection

The feature parser examines capability flags from the device payload:

has_invert: payload[1] & (1 << 3) != 0,

This bit indicates whether the hardware supports native inversion.

Inversion Bit Manipulation

When applying settings, the code toggles the inversion state via bit 2 of payload byte 0:

inverted: payload[0] & (1 << 2) != 0,

The agent sets or clears this bit to enable or disable native scroll inversion without OS-level manipulation.

Configuration Persistence

Settings persist in config.toml under device-specific tables. The system uses the device route key as the identifier:

[devices."direct:046d:c08d:unit:6be9d300"]
invert_scroll = true

As implemented in the test fixtures at crates/openlogi-core/src/config/tests.rs (lines 398-405), false values are omitted during serialization to maintain clean configuration files.

Practical Configuration Examples

Programmatic Device Setup

Configure inversion programmatically using the core configuration API:

use openlogi_core::config::Config;

let mut cfg = Config::default();
let device_key = "direct:046d:c08d:unit:6be9d300";

// Enable hardware scroll inversion
cfg.set_invert_scroll(device_key, true);

Command Line Interface

Toggle inversion via the CLI utility:

openlogi-cli set invert-scroll <device-key> true

This invokes Config::set_invert_scroll internally and triggers the same reload sequence as the desktop UI.

Override settings for specific connection routes:

use openlogi_core::config::Config;

let mut cfg = Config::default();
let device_key = "receiver:82839805:slot:1";

cfg.devices
   .entry(device_key.to_string())
   .or_default()
   .links
   .entry("receiver:82839805:slot:1".to_string())
   .or_default()
   .overrides
   .invert_scroll = Some(true);

This configuration inverts scroll only when connected through the specified receiver slot, leaving other routes unaffected.

Summary

  • Configuration Layer: The invert_scroll boolean in DeviceConfig provides device-wide defaults, while LinkOverrides enables route-specific exceptions.
  • Resolution Logic: The effective_invert_scroll method prioritizes per-link settings over global device configuration.
  • UI Integration: The desktop application validates hardware support through current_scroll_inversion_supported before persisting changes.
  • Hardware Interface: The agent communicates via HID++ feature 0x2121 (HiResWheel), manipulating bit 2 of the feature payload to toggle inversion.
  • Storage Format: Settings persist in TOML format under device tables, with false values elided to minimize configuration file size.

Frequently Asked Questions

What is HID++ feature 0x2121?

HID++ feature 0x2121 corresponds to the HiResWheel feature on modern Logitech devices. According to the OpenLogi source code in crates/openlogi-hidpp/src/feature/hires_wheel.rs, this feature exposes native scroll-direction control through specific bits in the feature payload, allowing the driver to invert scroll direction at the hardware level before the OS processes input events.

Can different connection methods have different scroll directions?

Yes. The LinkOverrides structure in crates/openlogi-core/src/config/device.rs supports per-route configuration. By setting invert_scroll within a specific link entry, you can enable inversion for a USB receiver while maintaining standard direction for direct USB connections, or vice versa. The effective_invert_scroll function resolves these overrides at runtime.

Where is the scroll inversion setting stored?

The configuration persists in the OpenLogi config.toml file under device-specific table headers using the device route key (e.g., [devices."direct:046d:c08d:unit:6be9d300"]). The invert_scroll field appears only when set to true, as the serialization logic skips default false values to keep the configuration file concise.

Does this work on all Logitech devices?

No. The setting requires hardware support for the HiResWheel feature. The desktop UI checks current_scroll_inversion_supported before presenting the toggle, which verifies the capability bit (bit 3 of payload byte 1) reported by the device. Devices lacking this feature bit cannot perform native scroll inversion and rely on OS-level scroll direction settings instead.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →