# How OpenLogi Handles Device Enumeration and Probing for Receivers and Stand-Alone Devices

> OpenLogi discovers Logitech HID++ hardware by first detecting receiver types then probing slots to build a complete device inventory with cached feature tables. Learn how OpenLogi handles device enumeration and probing.

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

---

**OpenLogi discovers Logitech HID++ hardware through a two-phase inventory subsystem that first detects receiver types (Bolt, Unifying, or direct) and then probes each slot to build a complete device inventory with cached feature tables.**

The `openlogi-device` crate in the [AprilNEA/OpenLogi](https://github.com/AprilNEA/OpenLogi) repository implements a robust, platform-agnostic enumeration pipeline. It abstracts platform-specific HID transport details to provide a unified view of both receiver-based and stand-alone Logitech peripherals across Linux, macOS, and Windows.

## Overview of the Inventory Subsystem

Device enumeration begins in [`crates/openlogi-device/src/inventory/mod.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/mod.rs) (or [`inventory.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/inventory.rs)), where the public `enumerate()` function serves as the entry point. This asynchronous orchestrator iterates over available HID backends, identifies potential Logitech receivers, and delegates to specialized probing logic based on the detected hardware type.

The subsystem distinguishes between three physical configurations:
- **Bolt receivers** – Logi Bolt USB dongles using HID++ 2.0+
- **Unifying receivers** – Legacy Unifying receivers
- **Stand-alone devices** – Bluetooth-direct or wired devices without a receiver

## Receiver Detection Phase

Detection starts when `probe_one` in [`crates/openlogi-device/src/inventory/probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/probe.rs) opens a raw HID++ channel. The code calls `receiver::detect` from the `hidpp` crate to inspect the product ID and classify the node.

If the channel identifies a known receiver, the system branches to `probe_bolt_receiver` or `probe_unifying_receiver`. When no receiver signature matches, execution falls back to `probe_direct`, treating the node as a stand-alone device.

## Device Probing Mechanisms

Each receiver type requires distinct probing strategies to map paired slots and extract device identities.

### Bolt Receiver Probing

For Bolt hardware, `probe_bolt_receiver` follows this sequence:
1. Reads the *pairing-count* register to determine active slots
2. Drains pending *device-arrival* events from the queue
3. Sequentially reads each occupied slot's identity via registers **0x41** and **0x42**
4. Concurrently walks the per-slot feature table

The operation respects the `BOLT_SLOT_PROBE` time budget to prevent blocking the inventory loop.

### Unifying Receiver Probing

The `probe_unifying_receiver` function handles legacy receivers differently:
1. Reads the *pairing-count* register
2. Triggers a broadcast-drain to collect one event per paired slot
3. Concurrently walks each slot's feature table using the `UNIFYING_SLOT_PROBE` timeout

### Stand-Alone Device Probing

When `probe_direct` is invoked (defined in [`crates/openlogi-device/src/inventory/standalone.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/standalone.rs)), it queries the device at slot **0xff** (represented by the constant `DIRECT_DEVICE_INDEX`). This slot address corresponds to the device's own HID++ interface rather than a receiver proxy. The function walks the feature set directly and returns a single `PairedDevice` entry representing the physical hardware.

## Result Assembly and Caching

After probing completes, `assemble_bolt_probe` or `assemble_unifying_probe` constructs a `DeviceInventory` struct containing:
- A `ReceiverInfo` metadata object
- A vector of `PairedDevice` entries for each populated slot

The system generates a `ProbeVerdict` enum variant:
- `Healthy { complete }` – Indicates the pairing-count register responded and every expected slot was processed
- `Failed` – Triggers fallback to the last known good snapshot

Feature-walk results are cached in `inventory::cache`. When `is_stale` returns false for a slot, the probe skips HID++ traffic and reuses cached data, significantly reducing latency for stable devices.

## Hot-Plug Handling

The inventory subsystem maintains responsiveness to physical changes through `inventory::hotplug::watch_hotplug`. This parallel watcher monitors udev (Linux) or IOKit (macOS) for arrival and removal events, pushing notifications onto an async channel.

When the inventory loop receives an event, it re-runs `probe_one` for the affected node, ensuring the GUI reflects devices added or removed during runtime without requiring a full rescan.

## Code Example: Enumerating Devices

The following example demonstrates the public API for consuming device inventories:

```rust
use openlogi_device::inventory::{enumerate, InventoryError};

#[tokio::main]
async fn main() -> Result<(), InventoryError> {
    // Initialize the platform-specific HID backend
    let backend = openlogi_hid::native::NativeBackend::new();

    // Fetch complete inventory including all receivers and stand-alone devices
    let inventories = enumerate(&backend).await?;
    
    for inv in inventories {
        println!("Receiver: {}", inv.receiver.name);
        for dev in inv.paired {
            println!("  • Device slot {} – kind: {:?}", dev.slot, dev.kind);
        }
    }
    Ok(())
}

```

This same implementation works across operating systems because the `openlogi_hid::Backend` trait abstracts platform-specific transport details.

## Summary

- **Two-phase detection** – `probe_one` classifies hardware as Bolt, Unifying, or direct, then delegates to specialized probe functions in [`probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/probe.rs).
- **Slot-based enumeration** – Receivers are probed slot-by-slot with time-bounded operations (`BOLT_SLOT_PROBE`, `UNIFYING_SLOT_PROBE`), while stand-alone devices use the fixed index `0xff`.
- **Resilient caching** – The `inventory::cache` module stores feature tables, avoiding redundant HID++ register reads when data remains fresh.
- **Runtime hot-plug** – The [`hotplug.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/hotplug.rs) watcher triggers selective re-probing when devices connect or disconnect during operation.
- **Unified abstraction** – The `enumerate()` function in [`inventory.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/inventory.rs) provides a cross-platform interface hiding OS-specific HID implementations.

## Frequently Asked Questions

### How does OpenLogi differentiate between Bolt and Unifying receivers?

OpenLogi uses the `receiver::detect` function from the internal `hidpp` crate to inspect the product ID of the opened HID++ channel. Based on this identifier, `probe_one` in [`probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/probe.rs) dispatches to either `probe_bolt_receiver` or `probe_unifying_receiver`, each implementing protocol-specific probing sequences optimized for the respective hardware generation.

### What happens when a device is connected without a receiver?

When `probe_one` cannot identify a known receiver signature, it falls back to `probe_direct` in [`standalone.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/standalone.rs). This function treats the HID node as a stand-alone device and queries slot `0xff` (`DIRECT_DEVICE_INDEX`), which represents the device's native HID++ interface. The result is a single `PairedDevice` entry returned in a `DeviceInventory` with no receiver metadata.

### How does the inventory system handle devices being plugged in while the application is running?

The `inventory::hotplug::watch_hotplug` module runs a platform-specific watcher (using udev on Linux or IOKit on macOS) that monitors kernel events for device arrival and removal. When the watcher detects a change, it pushes an event to the inventory loop, which triggers a targeted re-probe of only the affected node, updating the published `DeviceInventory` snapshot without requiring a full system rescan.

### Why does probing use time budgets for each slot?

The constants `BOLT_SLOT_PROBE` and `UNIFYING_SLOT_PROBE` define per-slot timeouts to prevent a single unresponsive device from blocking the entire enumeration process. Since HID++ operations involve synchronous register reads, these budgets ensure the inventory system remains responsive even when individual paired devices are sleeping or disconnected but still occupying logical slots in the receiver.