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

> Discover how OpenLogi separates HID++ device I/O from OS input capture using a modular Rust architecture. Learn about its independent crates for hardware and OS event handling.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: architecture
- Published: 2026-09-01

---

**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.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/session.rs)<br>`Transport::open` in [`crates/openlogi-hid/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hid/src/transport.rs)<br>`Device::send_report` in [`crates/openlogi-hidpp/src/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs)<br>`frontmost_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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/session.rs), `DeviceSession::new()` enumerates connected Logitech devices and negotiates a HID++ protocol version.

```rust
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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
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

- [`crates/openlogi-device/src/session.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/session.rs) – `DeviceSession` lifecycle and high-level device API
- [`crates/openlogi-hid/src/transport.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hid/src/transport.rs) – `Transport` trait and platform backends
- [`crates/openlogi-hidpp/src/device.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/device.rs) – HID++ protocol encoding/decoding
- [`crates/openlogi-hook/src/lib.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hook/src/lib.rs) – OS event hook installation and `HookEvent` definitions

## 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.