# How OpenLogi Identifies and Differentiates Devices by Receiver Serial and Unit ID

> Learn how OpenLogi identifies devices using receiver serial numbers or 4-byte unit IDs. Understand device differentiation for your OpenLogi system.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-09-03

---

**OpenLogi uses a `DeviceIdentity` enum that prefers receiver serial numbers when available, falling back to 4-byte unit IDs for devices like lights that lack unique hardware serials.**

OpenLogi is an open-source Rust implementation for managing Logitech HID++ devices. According to the AprilNEA/OpenLogi source code, the project identifies each physical device through a dual-strategy system that handles both traditional peripherals with burned-in serial numbers and accessories that rely on receiver-assigned unit IDs.

## The DeviceIdentity Enum: Serial vs. Unit ID

At the core of the identification system lies the `DeviceIdentity` enum defined in [`crates/openlogi-core/src/device_order.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/device_order.rs). This enum represents the two possible ways a device can be uniquely tracked:

```rust
pub enum DeviceIdentity {
    Serial(String),
    Unit([u8; 4]),
}

```

The `Serial` variant stores a human-readable hexadecimal string extracted directly from the device's hardware, while the `Unit` variant holds a raw 4-byte array representing the pairing slot identifier assigned by the receiver. This design allows OpenLogi to treat both identification schemes interchangeably throughout the application layer.

## Extracting Identifiers from HID++ Receivers

OpenLogi retrieves device identifiers by issuing the HID++ `GetReceiverInfo` command to both Unifying and Bolt receivers.

### Unifying and Bolt Receiver Implementation

In [`crates/openlogi-hidpp/src/receiver/unifying.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/unifying.rs) and the corresponding [`bolt.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bolt.rs) file, the code parses the receiver response to extract two critical fields:

- **Serial number**: A 4-byte value encoded as an uppercase hex string using `hex::encode_upper(&response[1..=4])`
- **Unit ID**: A 4-byte array taken from response bytes 4 through 7 (`[response[4], response[5], response[6], response[7]]`)

The serial provides global uniqueness across all manufactured devices, whereas the unit ID remains stable only as long as the device maintains its pairing slot with the current receiver.

## Constructing Identity with from_parts

The `DeviceIdentity` implementation provides a single constructor method `from_parts` that encapsulates the precedence logic:

```rust
impl DeviceIdentity {
    pub fn from_parts(serial: Option<&str>, unit_id: [u8; 4]) -> Self {
        serial.map(|s| DeviceIdentity::Serial(s.to_string()))
            .unwrap_or(DeviceIdentity::Unit(unit_id))
    }
}

```

**Serial numbers take precedence.** When the hardware exposes a serial string, OpenLogi creates a `DeviceIdentity::Serial` instance. If the serial is `None`—common with lighting accessories and certain haptic devices—the system falls back to `DeviceIdentity::Unit` using the provided 4-byte unit ID.

```rust
use openlogi_core::device_order::DeviceIdentity;

// Device with hardware serial
let id = DeviceIdentity::from_parts(Some("ABCD1234"), [0, 0, 0, 0]);
// Returns: DeviceIdentity::Serial("ABCD1234")

// Device without serial (e.g., RGB light)
let id = DeviceIdentity::from_parts(None, [0x01, 0x02, 0x03, 0x04]);
// Returns: DeviceIdentity::Unit([1, 2, 3, 4])

```

## Persistent Identification in the Inventory System

The device inventory layer converts raw receiver data into persistent `DeviceIdentity` values. In [`crates/openlogi-device/src/inventory/standalone.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/standalone.rs), the code constructs `DeviceModelInfo` structures that store both the optional serial number and the raw unit ID, then canonicalizes them through `DeviceIdentity::from_parts`.

### Cache Stability and Lookup

To prevent cache collisions when multiple receivers share the same serial or when multiple devices connect through a single receiver, [`crates/openlogi-device/src/inventory/probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/probe.rs) uses `CacheKey::Bolt { unit_id }` as the unique lookup key. This guarantees distinct cache entries for each physical receiver slot while maintaining stable identifiers across application restarts.

## Why OpenLogi Uses Both Identifier Types

**Serial numbers** provide global uniqueness and survive device reboots, making them ideal for storing persistent user configurations like DPI settings or button remappings. **Unit IDs** provide a deterministic fallback for devices that lack hardware serials—particularly common in Logitech's lighting and haptic product lines—while remaining stable as long as the pairing relationship persists.

The orchestrator layer in [`crates/openlogi-agent-core/src/orchestrator.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent-core/src/orchestrator.rs) relies on this dual system to consistently route commands to the correct physical device, regardless of whether the underlying hardware exposes a traditional serial number.

## Summary

- OpenLogi identifies devices through the `DeviceIdentity` enum with `Serial(String)` and `Unit([u8; 4])` variants defined in [`crates/openlogi-core/src/device_order.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/device_order.rs).
- Receiver implementations in [`unifying.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/unifying.rs) and [`bolt.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bolt.rs) extract 4-byte serial numbers and unit IDs via the HID++ `GetReceiverInfo` command.
- The `from_parts` constructor prioritizes serial numbers, falling back to unit IDs only when hardware serials are absent.
- The inventory system uses these identities to maintain stable cache keys in [`probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/probe.rs) and persistent device records in [`standalone.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/standalone.rs).
- This dual approach ensures unique identification for both standard peripherals with burned-in serials and accessory devices that rely on receiver-assigned unit IDs.

## Frequently Asked Questions

### What is the difference between serial number and unit ID in OpenLogi?

The **serial number** is a hardware-burned identifier extracted as a 4-byte hex string from the device itself, providing global uniqueness. The **unit ID** is a 4-byte receiver-assigned identifier based on the pairing slot, used when the device lacks a hardware serial. Serials are preferred for persistence, while unit IDs act as a stable fallback.

### How does OpenLogi handle devices without serial numbers?

When `from_parts` receives `None` for the serial parameter, it constructs a `DeviceIdentity::Unit` variant using the 4-byte unit ID. This commonly occurs with Logitech lights and haptic accessories, allowing the inventory system to track these devices reliably via their receiver pairing slot.

### Where is the device identification logic implemented in the OpenLogi codebase?

The `DeviceIdentity` enum and its construction logic reside in [`crates/openlogi-core/src/device_order.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/device_order.rs). The extraction of raw identifiers from receivers happens in [`crates/openlogi-hidpp/src/receiver/unifying.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/receiver/unifying.rs) and [`bolt.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/bolt.rs), while the inventory persistence layer in [`crates/openlogi-device/src/inventory/standalone.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/standalone.rs) bridges these into the application's device model.

### How does the inventory cache distinguish between multiple receivers?

The probe implementation in [`crates/openlogi-device/src/inventory/probe.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/inventory/probe.rs) uses `CacheKey::Bolt { unit_id }` to generate unique cache entries. This ensures that two receivers with identical serial numbers—or multiple devices connected through a single receiver—maintain separate cache entries based on their distinct unit IDs.