How OpenLogi Handles Device Capabilities Differently Per Link

OpenLogi stores device capabilities at the link level via LinkConfig rather than treating them as universal device properties, enabling accurate feature detection across different transports like USB, Bluetooth, and wireless receivers.

In the OpenLogi input device management system, a single physical mouse or keyboard can connect through multiple routes—wired USB, a 2.4 GHz wireless dongle, or Bluetooth. Each transport may expose a different subset of HID++ features. The OpenLogi source code implements a layered configuration model that separates device-wide identity from per-link capabilities, ensuring the GUI presents only supported controls for the currently active connection.

The LinkConfig Architecture

OpenLogi defines one DeviceConfig per unique device model, but stores a separate LinkConfig for every route on which that device has been observed. This design is implemented in crates/openlogi-core/src/config/device.rs.

The LinkConfig struct contains an optional capabilities field:

// crates/openlogi-core/src/config/device.rs (lines 84-91)
pub struct LinkConfig {
    /// Capabilities discovered on this specific link.
    /// 
    /// Different links may expose different feature sets due to transport
    /// limitations or firmware variations.
    pub capabilities: Option<Capabilities>,
    /// User-defined overrides that apply only to this link.
    pub overrides: LinkOverrides,
}

This allows the same device model to report Hi-Res Wheel, SmartShift, or RGB lighting features on one transport but not another. For example, a Logitech G502 LIGHTSPEED mouse exposes high-resolution scrolling over its wireless receiver link, yet presents a reduced feature set when connected via USB cable.

Runtime Capability Discovery

When a device appears online, the desktop client probes its HID++ feature table and updates the per-link capabilities. The adopt_routes helper in crates/openlogi-desktop/src/state/inventory.rs merges these discovered capabilities into the canonical configuration:

// crates/openlogi-desktop/src/state/inventory.rs (lines 24-26, 462-466)
pub fn adopt_routes(&mut self, routes: &[DeviceRoute]) {
    for route in routes {
        if let Some(link_cfg) = device.links.get_mut(&route.key()) {
            // Merge freshly discovered capabilities into stored LinkConfig
            link_cfg.capabilities = Some(route.capabilities.clone());
        }
    }
}

This ensures the correct capability set is always available regardless of which link is currently active.

Beyond raw hardware capabilities, each LinkConfig maintains a LinkOverrides struct for settings that should vary by connection type. Common override categories include:

  • DPI sensitivity — different values for desk versus travel mousepad surfaces
  • Scroll direction inversion — enabled for Bluetooth laptop use, disabled for desktop
  • Lighting profiles — battery-saving dim mode on wireless, full brightness on USB
  • SmartShift tension — looser setting for gaming, tighter for productivity

These overrides shadow device-wide defaults only while the device is reachable via that specific route. The GUI queries DeviceConfig.links using the current DeviceStableId::route_key to determine which panels to render.

The following example demonstrates iterating over stored links to inspect capability differences:

use openlogi_core::config::{Config, LinkConfig};

fn print_capabilities(cfg: &Config, device_key: &str) {
    // Resolve the device's config entry
    if let Some(device_cfg) = cfg.device(device_key) {
        // Iterate over each stored link (route key → LinkConfig)
        for (route_key, link_cfg) in &device_cfg.links {
            // `capabilities` is optional — only present if the link has been probed
            if let Some(caps) = &link_cfg.capabilities {
                println!("Link {} reports capabilities: {:#?}", route_key, caps);
            } else {
                println!("Link {} has no discovered capabilities yet.", route_key);
            }
        }
    }
}

This pattern is used by the inventory system to populate device tiles and filter unavailable settings.

To persist a setting that applies only to a specific transport:

use openlogi_core::config::{Config, LinkOverrides};

fn set_link_dpi(cfg: &mut Config, device_key: &str, route_key: &str, new_dpi: Dpi) {
    if let Some(device_cfg) = cfg.device_mut(device_key) {
        // Ensure a LinkConfig exists for the requested route
        let link_cfg = device_cfg.links.entry(route_key.to_string())
                                       .or_default();
        // Record the DPI override for this link only
        link_cfg.overrides.dpi = Some(new_dpi);
        cfg.save(); // persist the change
    }
}

The BTreeMap<String, LinkConfig> keyed by route_key guarantees stable lookup even when connection order varies.

GUI Integration

The desktop client's state layer bridges the core configuration with the user interface. As implemented in crates/openlogi-desktop/src/state/agent.rs, the current AgentLink status determines which LinkConfig to query. The UI then:

  1. Reads LinkConfig.capabilities to decide which feature panels to display
  2. Applies LinkOverrides values as initial control states
  3. Writes changes back to the specific LinkConfig rather than global device settings

This produces accurate device representation—users never see unsupported controls, and preferences automatically follow transport-specific contexts.

Summary

  • Capabilities are link-scoped: The LinkConfig.capabilities: Option<Capabilities> field stores HID++ feature sets independently per transport, defined in crates/openlogi-core/src/config/device.rs
  • Runtime updates merge intelligently: crates/openlogi-desktop/src/state/inventory.rs updates stored capabilities each time a device appears online, with adopt_routes synchronizing route-derived data
  • Overrides enable transport-specific preferences: LinkOverrides within LinkConfig shadows global defaults for DPI, lighting, and gesture settings
  • Single logical device, multiple capability contexts: The DeviceConfig.links: BTreeMap<String, LinkConfig> structure preserves one unified device identity while supporting heterogeneous transport behavior

Frequently Asked Questions

Why doesn't OpenLogi store one capability set per device model?

Device capabilities vary by transport due to protocol limitations and firmware differences. A wireless receiver may support HID++ 2.0 features that the Bluetooth HID profile omits. Storing capabilities per LinkConfig in crates/openlogi-core/src/config/device.rs ensures accurate feature detection for each connection type rather than assuming universal behavior.

How does OpenLogi handle capability changes after firmware updates?

The adopt_routes function in crates/openlogi-desktop/src/state/inventory.rs refreshes LinkConfig.capabilities whenever a device is observed online. New firmware features are automatically detected on the next connection and merged into the stored configuration, superseding cached values without manual intervention.

Can users manually override detected capabilities?

Users cannot directly edit capabilities fields—those reflect hardware-reported HID++ feature tables. However, the LinkOverrides struct in each LinkConfig provides comprehensive control over functional behavior. Settings like disabled buttons or suppressed gestures achieve the same practical effect as capability masking for UI purposes.

What happens if the same device connects via an unrecognized route?

The configuration system creates a new LinkConfig entry via BTreeMap::entry().or_default() when encountering unknown route_key values. Initial capabilities are None until the device is probed, and defaults apply until the user customizes overrides. This ensures graceful handling of new dongles, USB ports, or Bluetooth pairings.

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 →