Litra Light Integration in OpenLogi: Camera-Linked Auto Power Control Explained

OpenLogi integrates Logitech Litra lights through a three-layer raw-HID stack that supports camera-automated power control via the auto_camera flag in LightSettings, which automatically powers the light on when any camera becomes active.

OpenLogi treats Logitech Litra lights as stand-alone-light devices accessible through a raw-HID interface. This integration enables precise hardware control alongside an optional automation feature that links light power state to camera activity. The implementation spans device detection, low-level protocol encoding, async IPC handling, and desktop UI configuration.

How OpenLogi Detects Litra Devices

The device registry identifies Litra hardware through USB vendor/product IDs and HID usage descriptors. In crates/openlogi-device-registry/src/litra.rs, the matches_litra function validates connected devices against known Litra identifiers:

// openlogi_device_registry/src/litra.rs
pub fn matches_litra(vendor_id: u16, product_id: u16, usage_page: u16, usage_id: u16) -> bool {
    find_litra(vendor_id, product_id, usage_page, usage_id).is_some()
}

Supported models include Litra Glow (PID 0xc900) and Litra Beam (PID 0xc901). Upon detection, the registry constructs a LitraDescriptor recording the model variant and capability ranges for brightness and color temperature.

Raw-HID Driver: Encoding and Sending Commands

The Litra driver in crates/openlogi-device/src/write/litra.rs handles protocol translation. The encode_command function converts semantic LightCommand values into the 20-byte report format required by Logitech's firmware:

// openlogi_device/src/write/litra.rs
pub fn encode_command(model: LitraModel, command: LightCommand) -> Result<[u8; REPORT_LEN], WriteError> {
    // … builds the report and validates ranges …
}

Report structure uses report ID 0x11, prefix bytes 0xff 0x04, and command identifiers 0x1c (power), 0x4c (brightness), and 0x9c (temperature). The apply function executes the write with per-device locking and a 2-second timeout:

// openlogi_device/src/write/litra.rs
pub async fn apply(
    backend: &dyn HidBackend,
    route: &DeviceRoute,
    model: LitraModel,
    command: LightCommand,
) -> Result<(), WriteError> {
    // … resolve model, encode, lock, write …
}

Agent-Core IPC and Async API

The agent core provides a higher-level interface in crates/openlogi-agent-core/src/hardware/light.rs. apply_light_unlocked resolves the Litra model and forwards commands through openlogi_hid::apply_litra:

// openlogi-agent-core/src/hardware/light.rs
async fn apply_light_unlocked(route: &DeviceRoute, command: LightCommand) -> Result<(), WriteError> {
    let Some(model) = litra_model_for_route(route) else {
        return Err(WriteError::LightUnsupported { … });
    };
    super::timed(HidppOperation::Light, openlogi_hid::apply_litra(route, model, command)).await
}

A per-device async mutex (light_write_lock) serializes all writes via apply_light, preventing concurrent HID access conflicts.

Persisting Settings and Background Re-Application

LightSettings in openlogi_core::config stores user preferences including the critical auto_camera boolean for camera-linked automation. When settings change or devices reconnect, set_light_in_background spawns a coalescing worker from LIGHT_WORKERS to re-apply commands:

// openlogi-agent-core/src/hardware/light.rs
pub fn set_light_in_background(..., light: &LightSettings, ...) {
    // send a LightApplyRequest to the per‑device worker
}

The worker calls apply_light_settings, which iterates through commands_for_light_settings outputs—brightness, temperature, and power commands—and dispatches each via apply_light_unlocked.

Desktop UI: Camera-Automation Toggle

The stand-alone-light panel in crates/openlogi-desktop/src/features/lighting/standalone.rs exposes the camera-automation toggle ("Auto-on with camera"). Toggling updates LightSettings.auto_camera and commits to persistent state:

// openlogi-desktop/src/features/lighting/standalone.rs
Toggle::new("standalone-light-camera-automation")
    .selected(current.auto_camera)
    .on_change(|auto_camera, _window, cx| {
        update_light(cx, |state| {
            let mut light = state.light();
            light.auto_camera = *auto_camera;
            state.commit_light(light);
        });
    });

The UI consults state.camera_automation_active() to determine whether manual slider adjustments should immediately power the light. With automation disabled, slider movement sets light.enabled = true directly. With automation enabled, the light waits for camera activation.

How Camera Activity Triggers Auto Power-On

The camera subsystem emits StateEvent::CameraChanged when any Logitech camera activates. The LightPanel listens for this event and updates accordingly. The automation flow works as follows:

  1. Camera activation fires CameraChanged
  2. UI logic (or background listener) sets light.enabled = true
  3. Background worker's next re-apply cycle issues Power(true) via apply_light_settings

The auto_camera flag itself does not directly control hardware; it gates whether the UI/background logic sets light.enabled based on camera state. The worker faithfully re-applies whatever enabled value is present in LightSettings.

Command-Line and Programmatic Usage

CLI Examples


# Discover Litra device route

openlogi list --filter litra

# Power on and configure

openlogi light on
openlogi light brightness 70

# Enable camera automation

openlogi light auto-camera on

Rust API

use openlogi_hid::{LightCommand, apply_litra};
use openlogi_device_registry::litra::find_litra;
use openlogi_core::config::LightSettings;

let route = /* DeviceRoute::RawHid … */;
let model = find_litra(0x046d, 0xc900, 0xff43, 0x0202).unwrap().model;

// Direct hardware control
apply_litra(&route, model, LightCommand::Power(true)).await?;
apply_litra(&route, model, LightCommand::BrightnessPercent(70)).await?;

// Persist with automation enabled
let mut settings = LightSettings::default();
settings.enabled = true;
settings.brightness_percent = 70;
settings.auto_camera = true;
openlogi_agent_core::hardware::light::set_light_in_background(
    &device_io_gate,
    Some(route.clone()),
    &settings,
    litra_capabilities(model),
);

Key Source Files

File Responsibility
crates/openlogi-device-registry/src/litra.rs USB VID/PID matching, capability description
crates/openlogi-device/src/write/litra.rs 20-byte report encoding, raw-HID writes
crates/openlogi-hid/src/lib.rs apply_litra IPC entry point
crates/openlogi-agent-core/src/hardware/light.rs Async API, background workers, setting re-application
crates/openlogi-desktop/src/features/lighting/standalone.rs UI panel, camera-automation toggle
crates/openlogi-desktop/src/state/app_state.rs camera_automation_active(), LightSettings state

Summary

  • Three-layer architecture: Device registry for detection, raw-HID driver for protocol encoding, agent/UI for high-level control and automation
  • Camera-linked automation: Controlled by LightSettings.auto_camera boolean; when enabled, camera activity sets light.enabled = true, triggering background Power(true) commands
  • Concurrent safety: Per-device async mutexes and coalescing background workers prevent write collisions
  • Protocol precision: Fixed 20-byte reports with validated model-specific ranges for Glow and Beam variants

Frequently Asked Questions

What Litra models does OpenLogi support?

OpenLogi supports Litra Glow (USB PID 0xc900) and Litra Beam (USB PID 0xc901). The find_litra function in the device registry matches these identifiers along with the HID usage page 0xff43 and usage ID 0x0202 that Logitech firmware exposes.

How does camera auto-power actually work?

The auto_camera flag gates whether the UI automatically sets light.enabled = true when StateEvent::CameraChanged fires. The background worker then re-applies settings and issues LightCommand::Power(true) to the Litra. The flag does not bypass the normal LightSettings → worker → HID driver pathway.

Can I control Litra brightness and temperature without the UI?

Yes. The openlogi CLI provides light brightness and light temperature subcommands. Programmatically, use apply_litra with LightCommand::BrightnessPercent(n) or LightCommand::TemperatureKelvin(k) after resolving the DeviceRoute and LitraModel.

What prevents conflicting commands when multiple operations target the Litra?

Two mechanisms ensure serialization: (1) apply_light acquires a per-device async mutex (light_write_lock) before calling apply_light_unlocked, and (2) the raw-HID driver's apply function obtains a device_lock before writing. Background workers further coalesce rapid setting changes through LIGHT_WORKERS to reduce HID traffic.

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 →