How OpenLogi Uniquely Identifies and Keys Devices in Configuration

OpenLogi uses a stable identifier called DeviceStableId that combines transport route information with per-unit identity (serial number or unit ID) to create unique, persistent configuration keys for each physical device.

In AprilNEA/OpenLogi, every mouse, keyboard, or raw-HID device receives a deterministic identifier that survives disconnections, transport changes, and system restarts. This article explains the four route variants, three key-generation methods, and how they map to the TOML configuration structure.


DeviceStableId: The Core Identifier

The DeviceStableId enum in crates/openlogi-core/src/device_order.rs is OpenLogi's answer to reliable device identification. Rather than relying on volatile OS node names, it derives a stable ID from two inputs: the DeviceRoute (transport path) and DeviceIdentity (per-unit serial or unit ID)【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-core/src/device_order.rs#L24-L33】.

The from_parts constructor assembles this identifier:

let stable_id = DeviceStableId::from_parts(
    Some(&device_route),  // transport route
    slot,                 // receiver slot if applicable
    serial,               // optional serial string
    unit_id,              // 4-byte fallback identifier
);

Four Route Variants for Complete Device Coverage

DeviceStableId supports every possible connection method through four enum variants:

Variant Connection Type Key Format
Bolt Logitech receiver (Bolt/Unifying) receiver:<uid>:slot:<slot>
Direct USB or Bluetooth direct attachment direct:<vendor_id>:<product_id> + identity
RawHid Stand-alone raw-HID device raw:<vendor_id>:<product_id>:<usage_page>:<usage_id>:<identity>
Unknown Cached device without live route info unknown:slot:<slot> + identity

Each variant ensures the resulting key contains enough information to locate and identify the device if it reconnects.


DeviceIdentity: Per-Unit Serial vs. Unit ID

Inside DeviceStableId, the DeviceIdentity enum captures the actual hardware identifier. The from_parts helper implements a clear precedence rule: serial number preferred, unit ID as fallback【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-core/src/device_order.rs#L68-L85】.

// Case-folded serial string (preferred)
DeviceIdentity::Serial("ABC12345".to_lowercase())

// 4-byte unit ID when serial unavailable
DeviceIdentity::UnitId([0x12, 0x34, 0x56, 0x78])

This design accommodates both premium devices with unique serials and mass-market hardware that only exposes a unit ID.


Three Key Methods for Different Use Cases

DeviceStableId exposes three distinct string representations, each serving a specific purpose in the codebase:

runtime_key(): Transient Session Key

Returns the full key including route-derived identity. Used for short-lived UI bookkeeping and internal device tracking. Not persisted to configuration files【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-core/src/device_order.rs#L35-L45】.

Returns the path portion without device-specific identity. This string is the map key for the links table inside DeviceConfig.links【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-core/src/device_order.rs#L67-L77】.

physical_key(): Persistent Device Identifier

Returns Option<PhysicalDeviceKey> — a stable, cross-session key only when the device has genuine physical identity (non-empty serial or non-zero unit ID). This becomes the top-level map key in per-device TOML configuration【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-openlogi-core/src/device_order.rs#L88-L103】.

PhysicalDeviceKey can be round-tripped via PhysicalDeviceKey::parse.


Configuration Layout: How Keys Map to TOML

In crates/openlogi-core/src/config/device.rs, the DeviceConfig struct defines the persisted configuration:

pub struct DeviceConfig {
    pub links: BTreeMap<String, LinkConfig>,  // keyed by route_key()
    // ... other fields
}

The links field uses route_key() as its map key【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-core/src/config/device.rs#L71-L78】. Meanwhile, the entire DeviceConfig entry is keyed by PhysicalDeviceKey when the configuration file is serialized or deserialized.

This two-level key structure separates transport-specific link configuration from device identity, allowing a single physical device to maintain consistent settings across multiple connection methods.


Practical Examples

Bolt-Receiver Mouse

use openlogi_core::device_order::{DeviceStableId, DeviceIdentity};
use openlogi_core::hid::DeviceRoute;

let bolt_route = DeviceRoute::Bolt {
    receiver_uid: "AABBCCDD".into(),
    slot: 1,
};
let stable_id = DeviceStableId::from_parts(Some(&bolt_route), 1, None, [0; 4]);

assert_eq!(stable_id.runtime_key(), "receiver:aabbccdd:slot:1");
assert_eq!(stable_id.route_key(), "receiver:aabbccdd:slot:1");
assert!(stable_id.physical_key().is_some());

Direct-USB Keyboard with Serial

let direct_route = DeviceRoute::Direct {
    vendor_id: 0x046d,   // Logitech
    product_id: 0xc08d,  // G915 TKL
};
let stable_id = DeviceStableId::from_parts(
    Some(&direct_route),
    0xff,  // unused slot for direct
    Some("ABC12345"),
    [0; 4],
);

assert_eq!(stable_id.runtime_key(), "direct:046d:c08d:serial:abc12345");
assert_eq!(stable_id.route_key(), "direct:046d:c08d");
assert_eq!(
    stable_id.physical_key().map(|k| k.into_string()),
    Some("direct:046d:c08d:serial:abc12345".to_string())
);

Using route_key in Configuration

let config_key = stable_id.route_key(); // "direct:046d:c08d"
// Insert into DeviceConfig.links map
device_config.links.insert(config_key, link_config);

Summary

  • DeviceStableId combines transport route and hardware identity to create deterministic device identifiers in OpenLogi.
  • Four route variants (Bolt, Direct, RawHid, Unknown) cover all connection scenarios.
  • Three key methods serve distinct purposes: runtime_key() for session tracking, route_key() for link configuration maps, and physical_key() for persistent device identity.
  • Configuration uses a two-level key structure: PhysicalDeviceKey for top-level device entries and route_key() for per-link settings inside DeviceConfig.links【/cache/repos/github.com/AprilNEA/OpenLogi/master/crates/openlogi-core/src/config/device.rs#L71-L78】.
  • The implementation guarantees configuration persistence across sleep/wake, transport changes, and system restarts.

Frequently Asked Questions

What happens if a device has no serial number?

OpenLogi falls back to the 4-byte unit ID. If both are absent, the device can still be managed at runtime but cannot produce a PhysicalDeviceKey, meaning its configuration won't persist across restarts. This is handled by DeviceIdentity::from_parts in device_order.rs.

Can the same physical device have multiple configuration entries?

No. The PhysicalDeviceKey is designed to be unique per physical device. However, a single device can have multiple link configurations keyed by different route_key() values if it supports multiple connection methods (e.g., both Bluetooth and USB receiver).

How does OpenLogi handle a device switching from USB to receiver?

The physical_key() remains stable because it incorporates the underlying device identity (serial or unit ID). Only the route_key() changes to reflect the new transport path, so link-specific settings may differ per connection method while core device settings persist.

Where is the configuration actually stored on disk?

Per-device TOML files use the PhysicalDeviceKey string as their identifier. The exact storage path is determined by the configuration backend, but the key format follows the direct:046d:c08d:serial:abc12345 pattern shown in the examples above.

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 →