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-hidcrate for asynchronous I/O - Linux: Direct
/dev/hidrawreads orhidraw/evdevhybrid - 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:
-
Device event reader: Polls
DeviceSessionfor HID++ reports (e.g., wheel rotations, button presses on the device itself) and translates them toHookEvent::Mouse::ScrollorHookEvent::Mouse::Buttonfor the hook callback chain. -
Hook event processor: Receives
HookEventfromHook::startand optionally triggers device commands—such as remapping a physical button viasession.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
BackendandHookimplementations allow unit testing without physical hardware or OS permissions. - Platform specificity where needed: Linux
evdevcomplexity stays inopenlogi-hook; macOSIOHIDManagerdetails stay inopenlogi-hid. - Clean feature flags: Compile out
openlogi-hookentirely for headless configuration daemons. - Protocol evolution: HID++ 2.0 extensions in
openlogi-hidppdon't risk regressing input capture behavior.
Key Source Files
crates/openlogi-device/src/session.rs–DeviceSessionlifecycle and high-level device APIcrates/openlogi-hid/src/transport.rs–Transporttrait and platform backendscrates/openlogi-hidpp/src/device.rs– HID++ protocol encoding/decodingcrates/openlogi-hook/src/lib.rs– OS event hook installation andHookEventdefinitions
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 macOSasync-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-corebridges 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →