RGB Lighting Control on Logitech Keyboards via HID++ Features in OpenLogi
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_valuerequests 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. This async function delegates to set_keyboard_color_with, which accepts a LightingMethod enum to control protocol selection:
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:
- Try ColorLedEffects (0x8070).
- If the device returns
FeatureUnsupported, fall back to PerKeyLighting2 (0x8081). - 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:
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_effectcall usesPersistence::Volatile, ensuring colors live only in RAM and are reapplied automatically by the agent upon reconnection. - Effect type –
EFFECT_FIXED(effect index0x01) 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:
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:
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:
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:
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:
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, with public APIs exposed throughwrite.rsand integrated into the CLI atcrates/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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →