# RGB Lighting Control on Logitech Keyboards via HID++ Features in OpenLogi

> Discover how OpenLogi enables RGB lighting control on Logitech keyboards using HID++ features. Learn about ColorLedEffects, PerKeyLighting2, and PerKeyLighting protocols for dynamic illumination.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: deep-dive
- Published: 2026-08-23

---

**OpenLogi drives RGB backlighting on Logitech HID++ keyboards by automatically selecting between three protocol features—ColorLedEffects (0x8070), PerKeyLighting2 (0x8081), and PerKeyLighting (0x8080)—applying colors with volatile persistence to avoid flash memory wear.**

The OpenLogi project implements open-source drivers for Logitech devices, providing robust RGB lighting control without proprietary software. This article examines how the `openlogi-device` crate manages illumination on supported keyboards, detailing the feature detection, protocol negotiation, and low-level report construction that powers the backlighting system.

## Understanding HID++ RGB Lighting Features

OpenLogi supports three distinct HID++ feature families for keyboard illumination, each representing a different generation of Logitech hardware capabilities:

- **ColorLedEffects (0x8070)** – Controls a built-in effect engine that writes solid colors to zones. Setting a fixed effect (index `0x01`) applies colors volatility (RAM only), overriding onboard profiles without touching flash memory. This is the preferred and fastest path.
- **PerKeyLighting2 (0x8081)** – Works with abstract zones rather than raw key IDs, allowing the driver to send `set_rgb_zones_single_value` requests for each present zone before committing the frame. This supersedes the older per-key method on newer keyboards.
- **PerKeyLighting (0x8080)** – The legacy fallback that streams individual key colors via raw HID++ reports, supporting up to 14 key-color entries per 64-byte packet. This works on older hardware lacking the effect engine or zone-based lighting.

According to the OpenLogi source code, the system probes for these features in the order listed above, prioritizing the effect engine for performance and reliability.

## Selecting the Lighting Method

The public entry point for all RGB operations is `set_keyboard_color`, defined in [`crates/openlogi-device/src/write/lighting.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/write/lighting.rs). This async function delegates to `set_keyboard_color_with`, which accepts a `LightingMethod` enum to control protocol selection:

```rust
pub async fn set_keyboard_color(
    backend: &dyn HidBackend,
    route: &DeviceRoute,
    r: u8,
    g: u8,
    b: u8,
) -> Result<(), WriteError> {
    set_keyboard_color_with(backend, route, LightingMethod::Auto, r, g, b).await
}

```

When `LightingMethod::Auto` is specified, the selection logic inside `set_keyboard_color_with_on_channel` (lines 121‑146) attempts the following sequence:

1. Try **ColorLedEffects** (0x8070).
2. If the device returns `FeatureUnsupported`, fall back to **PerKeyLighting2** (0x8081).
3. Finally, attempt **PerKeyLighting** (0x8080) if both higher-level features are unavailable.

This cascading approach ensures maximum hardware compatibility while preferring the most efficient protocol available.

## The ColorLedEffects Implementation (0x8070)

The `set_color_effects` function (lines 180‑220) handles the preferred lighting path. It opens the `ColorLedEffectsFeature` and queries the device’s zone count via `get_info`:

```rust
async fn set_color_effects(
    channel: &Arc<HidppChannel>,
    index: u8,
    r: u8,
    g: u8,
    b: u8,
) -> Result<(), WriteError> {
    let mut device = Device::new(Arc::clone(channel), index).await?;
    let feature = open_feature::<ColorLedEffectsFeature>(&mut device).await?;
    let zone_count = feature.get_info().await?.zone_count;
    // ...
    for zone in 0..zones_to_write {
        feature.set_zone_effect(
            zone,
            EFFECT_FIXED,
            &[r, g, b, 0, 0, 0, 0, 0],
            Persistence::Volatile,
        ).await?;
        tokio::time::sleep(FRAME_GAP).await;
    }
}

```

Key implementation details include:

- **Zone capping** – The code limits writes to the first four zones regardless of the device's reported zone count (see lines 199‑207), accommodating legacy hardware constraints.
- **Volatile persistence** – Each `set_zone_effect` call uses `Persistence::Volatile`, ensuring colors live only in RAM and are reapplied automatically by the agent upon reconnection.
- **Effect type** – `EFFECT_FIXED` (effect index `0x01`) creates a solid color across the specified zones.

## The PerKeyLighting2 Path (0x8081)

When the effect engine is unavailable, OpenLogi attempts the zone-based `PerKeyLighting2` protocol through `set_color_per_key_v2`:

```rust
async fn set_color_per_key_v2(
    channel: &Arc<HidppChannel>,
    index: u8,
    r: u8,
    g: u8,
    b: u8,
) -> Result<(), WriteError> {
    let mut device = Device::new(Arc::clone(channel), index).await?;
    let feature = open_feature::<PerKeyLightingFeature>(&mut device).await?;
    let zones = present_zones(&feature).await?;
    // ...
    for chunk in zones.chunks(MAX_SINGLE_VALUE_ZONES) {
        feature.set_rgb_zones_single_value(Rgb { red: r, green: g, blue: b }, chunk).await?;
    }
    feature.frame_end(FramePersistence::Volatile, 0, 0).await?;
}

```

The `present_zones` helper (lines 300‑318) reads three zone-presence pages from the device, constructing a list of active zones while skipping sentinel IDs `0x00` and `0xff`. The driver transmits colors in chunks limited by `MAX_SINGLE_VALUE_ZONES`, then commits the frame using `frame_end` with volatile persistence.

## The PerKeyLighting Fallback (0x8080)

For older keyboards lacking both previous features, OpenLogi falls back to raw HID++ report streaming via `per_key_reports` (lines 891‑927). This low-level approach constructs 64-byte packets carrying up to 14 `(keyID, R, G, B)` entries per report:

```rust
pub super fn per_key_reports(
    device_index: u8,
    feature_index: u8,
    r: u8,
    g: u8,
    b: u8,
) -> Vec<Vec<u8>> {
    // Build REPORT_SET_KEYS (0x12) packets, then a final 20-byte commit.
}

```

The `resolve_feature_index` function (lines 149‑168) obtains the runtime feature index for `0x8080`, after which `channel.write_raw_report` transmits each constructed payload. This method works on any HID++ keyboard exposing the `0x8080` feature, regardless of zone support.

## Practical Code Examples

Use the default automatic method to set a solid red color:

```rust
let backend = /* an impl of HidBackend */;
let route   = /* DeviceRoute for the target keyboard */;
openlogi_device::write::lighting::set_keyboard_color(
    &backend, &route, 0xFF, 0x00, 0x00
).await?;

```

Force the raw per-key stream for debugging older models:

```rust
openlogi_device::write::lighting::set_keyboard_color_with(
    &backend,
    &route,
    openlogi_device::write::lighting::LightingMethod::PerKey,
    0x00, 0xFF, 0x00,
).await?; // Sets green via 0x8080

```

Operate on an existing shared channel managed by the GUI agent:

```rust
let shared = /* SharedChannel from agent */;
openlogi_device::write::lighting::set_keyboard_color_on(
    &shared, 0x00, 0x00, 0xFF
).await?; // Sets blue

```

## Summary

- OpenLogi implements **three HID++ protocols** (0x8070, 0x8081, 0x8080) for RGB control, automatically selecting the best available option.
- The **ColorLedEffects** path (0x8070) is preferred for its efficiency and zone-based operation, capped at four zones with volatile writes.
- **PerKeyLighting2** (0x8081) uses zone presence detection and chunked writes, while **PerKeyLighting** (0x8080) streams raw reports for maximum compatibility.
- All lighting changes use **volatile persistence** to prevent flash wear, requiring the agent to reapply colors on reconnect.
- Core logic resides in [`crates/openlogi-device/src/write/lighting.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/write/lighting.rs), with public APIs exposed through [`write.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/write.rs) and integrated into the CLI at [`crates/openlogi-cli/src/cmd/light.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-cli/src/cmd/light.rs).

## Frequently Asked Questions

### What does "volatile persistence" mean in OpenLogi's RGB control?

Volatile persistence means color data is written only to the keyboard's RAM, not its onboard flash memory. According to the implementation in `set_zone_effect` and `frame_end`, this prevents flash degradation from repeated writes. However, because colors aren't stored permanently, the OpenLogi agent must reapply the lighting profile when the keyboard reconnects or the system reboots.

### Why does OpenLogi limit ColorLedEffects to four zones?

The source code explicitly caps zone writes at four (lines 199‑207) despite the device potentially reporting more zones via `get_info`. This limitation accommodates legacy hardware constraints within the Logitech ecosystem, ensuring consistent behavior across different keyboard generations that share the `0x8070` feature ID.

### How does OpenLogi handle keyboards that don't support the ColorLedEffects feature?

When `0x8070` returns `FeatureUnsupported`, the selection logic in `set_keyboard_color_with_on_channel` automatically cascades to `PerKeyLighting2` (0x8081) if available, then finally to the raw streaming `PerKeyLighting` (0x8080) method. This graceful degradation ensures RGB control works on both modern zone-based keyboards and older per-key-only models.

### Can I force a specific lighting method instead of using Auto?

Yes. The `set_keyboard_color_with` function accepts a `LightingMethod` enum variant including `Effects`, `PerKey`, and `PerKeyV2`. This allows developers and diagnostic tools to bypass automatic selection and test specific protocol paths, as demonstrated in the CLI's diagnostic lighting command at [`crates/openlogi-cli/src/cmd/diag/lighting.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-cli/src/cmd/diag/lighting.rs).