How OpenLogi Separates HID++ Device I/O from OS Input Capture: A Modular Rust Architecture

OpenLogi handles HID++ device I/O and OS input capture through strictly separated crates—openlogi-device, openlogi-hid, and openlogi-hidpp for direct Logitech hardware communication, and openlogi-hook for low-level OS event interception—allowing independent testing, mocking, and platform-specific optimization of each subsystem.

This architectural decision in the AprilNEA/OpenLogi repository enables clean abstraction boundaries between talking to physical Logitech devices and observing or modifying what the operating system reports. The separation matters because HID++ device communication requires raw USB/Bluetooth HID access, while OS input capture demands platform-specific event tap APIs that differ radically across macOS, Linux, and Windows.

Architecture Overview: Two Distinct Subsystems

Responsibility Crates Core Purpose Key Entry Points
HID++ device I/O openlogi-device → openlogi-hid → openlogi-hidpp Enumerate Logitech devices, open raw HID transports, send/receive HID++ commands DeviceSession::new in crates/openlogi-device/src/session.rsTransport::open in crates/openlogi-hid/src/transport.rsDevice::send_report in crates/openlogi-hidpp/src/device.rs
OS input capture openlogi-hook Install low-level hooks, deliver raw OS input events, provide cursor/app utilities Hook::start / Hook::stop in crates/openlogi-hook/src/lib.rsfrontmost_application and cursor_position in same file

The crate-level separation enforces compile-time boundaries. No code in openlogi-hook can import from openlogi-device, and vice versa. The openlogi-agent-core crate alone bridges them at runtime.

HID++ Device I/O: Direct Hardware Communication

The device I/O stack implements three layers of abstraction, each with a dedicated crate.

Layer 1: Device Session (openlogi-device)

DeviceSession provides the high-level API that most OpenLogi components consume. In crates/openlogi-device/src/session.rs, DeviceSession::new() enumerates connected Logitech devices and negotiates a HID++ protocol version.

use openlogi_device::session::DeviceSession;
use openlogi_core::hid::dpi::DpiProfile;

fn set_dpi(profile: DpiProfile) -> Result<(), Box<dyn std::error::Error>> {
    // Create a session with the first matched Logitech device
    let mut session = DeviceSession::new()?;
    // Send a DPI-change command over HID++
    session.write().dpi_set(profile)?;
    Ok(())
}

The session holds a Backend trait object selected at compile time. This backend-agnostic design allows headless operation—critical for the mock agent used in CI testing.

Layer 2: Transport Abstraction (openlogi-hid)

Transport in crates/openlogi-hid/src/transport.rs abstracts platform-specific raw HID access:

  • macOS: async-hid crate for asynchronous I/O
  • Linux: Direct /dev/hidraw reads or hidraw/evdev hybrid
  • Windows: WinUSB and native HID APIs

Transport::open() returns a channel pair for sending reports and receiving device responses. The transport layer knows nothing about HID++ semantics—it merely moves bytes.

Layer 3: Protocol Implementation (openlogi-hidpp)

Device::send_report in crates/openlogi-hidpp/src/device.rs encodes HID++ 1.0 and 2.0 command structures. This crate contains:

  • Feature set discovery (IFeatureSet)
  • Device-specific commands (DPI profiles, SmartShift, gesture recognition)
  • Report checksum validation and sequence numbering

Because protocol logic sits below the session layer, new HID++ features can be added without touching transport or session code.

OS Input Capture: Platform-Native Event Hooks

The openlogi-hook crate in crates/openlogi-hook/src/lib.rs solves an entirely different problem: observing and optionally modifying what the OS delivers to applications.

Platform-Specific Hook Implementations

Platform API Mechanism
macOS CGEventTap Core Graphics event tap with kCGHIDEventTap placement
Linux evdev + uinput Grab exclusive access with EVIOCGRAB, re-inject modified events via uinput
Windows WH_MOUSE_LL / WH_KEYBOARD_LL Low-level Win32 hooks in a dedicated thread with message loop

Hook API Usage

Hook::start installs the platform hook and returns a handle. The callback receives HookEvent values and returns EventDisposition to determine propagation:

use openlogi_hook::{Hook, HookEvent, EventDisposition};

fn run_hook() -> Result<(), openlogi_hook::HookError> {
    let hook = Hook::start(|event| {
        println!("OS event: {:?}", event);
        // Pass the event through unchanged
        EventDisposition::PassThrough
    })?;

    // ...run the rest of the app...

    // Clean up when done
    hook.stop();
    Ok(())
}

The hook crate also exports frontmost_application() and cursor_position() for context-aware remapping—querying which app has focus without any HID++ device interaction.

How the Agent Unifies Both Subsystems

openlogi-agent-core contains the runtime that connects device I/O and OS hooks. It runs two concurrent tasks:

  1. Device event reader: Polls DeviceSession for HID++ reports (e.g., wheel rotations, button presses on the device itself) and translates them to HookEvent::Mouse::Scroll or HookEvent::Mouse::Button for the hook callback chain.

  2. Hook event processor: Receives HookEvent from Hook::start and optionally triggers device commands—such as remapping a physical button via session.write().remap_button().

This directional flow matters: device I/O and OS capture run on independent threads with distinct error handling. If the device disconnects, the hook continues capturing OS input. If the hook loses permissions, the device session remains functional for direct configuration.

Why This Separation Benefits Developers

  • Independent testing: Mock Backend and Hook implementations allow unit testing without physical hardware or OS permissions.
  • Platform specificity where needed: Linux evdev complexity stays in openlogi-hook; macOS IOHIDManager details stay in openlogi-hid.
  • Clean feature flags: Compile out openlogi-hook entirely for headless configuration daemons.
  • Protocol evolution: HID++ 2.0 extensions in openlogi-hidpp don't risk regressing input capture behavior.

Key Source Files

Summary

  • OpenLogi separates HID++ device I/O (openlogi-device, openlogi-hid, openlogi-hidpp) from OS input capture (openlogi-hook) at the crate level with no compile-time dependencies between them.
  • The device stack uses DeviceSession → Transport → HID++ protocol layers, abstracting macOS async-hid, Linux /dev/hidraw, and Windows WinUSB.
  • The hook stack uses platform-native APIs (CGEventTap, evdev/uinput, WH_MOUSE_LL) to observe and optionally modify OS-delivered input events.
  • openlogi-agent-core bridges both subsystems at runtime, enabling bidirectional flows like device-triggered scroll events and hook-triggered device remapping.
  • This modularity enables independent testing, mocking, and platform-specific optimization without cross-contamination.

Frequently Asked Questions

Can I use OpenLogi's HID++ device I/O without the OS input hook?

Yes. The openlogi-device crate has no dependency on openlogi-hook. You can create a DeviceSession in a headless daemon or CLI tool to configure Logitech devices without capturing OS input. The mock agent in OpenLogi's test suite demonstrates this pattern.

Why does OpenLogi need to capture OS input if it talks directly to HID++ devices?

HID++ devices report physical state changes (button presses, wheel movements), but the OS may apply acceleration, scaling, or gesture recognition before delivering events to applications. The hook captures the final OS-delivered input for accurate remapping and prevents feedback loops where OpenLogi's own injected events re-trigger processing.

How does the transport layer handle Bluetooth versus USB Logitech devices?

Transport::open in crates/openlogi-hid/src/transport.rs enumerates both USB and Bluetooth HID interfaces through the same platform abstraction. On macOS and Linux, Bluetooth devices appear as standard HID devices with no protocol differences. The HID++ protocol implementation remains identical regardless of physical transport.

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 →