0x8081 · perKeyLighting
逐键 RGB 灯光控制,通过 7 个 HID++ 函数暂存区域更新,再由帧提交选择是否写入非易失性存储。
0x8081 是面向带可寻址 LED 键盘的 0x8080 区域寻址后继 feature。
它先暂存区域颜色,再由 frame_end 提交完整帧。
**规格:**没有公开的 Logitech 规格。布局由 Logi Options+ 2.8 静态分析逆向得出。**交叉核对:**Solaar。用于:键盘灯光。
函数参考
PerKeyLightingFeature 从 feature 版本 0 开始实现 0x8081。
所有方法都是 async,并返回 Result<…, Hidpp20Error>。
方法
| 函数 | HID++ fn | 签名 | 返回值 |
|---|---|---|---|
get_rgb_zone_presence | 0 | (page: ZonePresencePage) | [u8; 14] |
set_individual_rgb_zones | 1 | (zones: &[RgbZone]) | () |
set_consecutive_rgb_zones | 2 | (first_zone_id: u8, colors: [Rgb; 5]) | () |
set_consecutive_rgb_zones_delta_5bit | 3 | (first_zone_id: u8, packed: [u8; 15]) | () |
set_consecutive_rgb_zones_delta_4bit | 4 | (first_zone_id: u8, packed: [u8; 15]) | () |
set_range_rgb_zones | 5 | (ranges: &[RgbZoneRange]) | () |
set_rgb_zones_single_value | 6 | (color: Rgb, zone_ids: &[u8]) | () |
frame_end | 7 | (persistence: FramePersistence, current_frame: u16, frames_till_next_change: u16) | () |
individual、range 和 single-value setter 最多分别发送 4、3 和 13 个切片条目。 它们会忽略超过这些上限的条目。
类型
Rgb
Rgb 为每个颜色通道保存一个 8-bit 值。
| 字段 | 类型 | 说明 |
|---|---|---|
red | u8 | 红色通道。 |
green | u8 | 绿色通道。 |
blue | u8 | 蓝色通道。 |
RgbZone
RgbZone 将一个 Rgb 值分配给一个区域。
| 字段 | 类型 | 说明 |
|---|---|---|
zone_id | u8 | 区域 ID。0x00 和 0xff 是列表结束哨兵值,调用时会被拒绝。 |
color | Rgb | 要暂存的颜色。 |
RgbZoneRange
RgbZoneRange 将一种颜色分配给一个闭区间范围。
| 字段 | 类型 | 说明 |
|---|---|---|
first_zone_id | u8 | 包含在范围内的第一个区域 ID。哨兵 ID 会被拒绝。 |
last_zone_id | u8 | 包含在范围内的最后一个区域 ID。哨兵 ID 会被拒绝。 |
color | Rgb | 要在范围内暂存的颜色。 |
ZonePresencePage
ZonePresencePage 选择一个 112-bit 的区域存在位图页。
这个非穷尽枚举有下列已知变体。
| 变体 | 值 | 说明 |
|---|---|---|
Zones0To111 | 0 | 区域 ID 0..=111。 |
Zones112To223 | 1 | 区域 ID 112..=223。 |
Zones224To255 | 2 | 区域 ID 224..=255。 |
FramePersistence
FramePersistence 选择 frame_end 的存储目标。
这个非穷尽枚举有下列已知变体。
| 变体 | 值 | 说明 |
|---|---|---|
Volatile | 0 | 只在 RAM 中应用帧。 |
VolatileAndNonVolatile | 1 | 在 RAM 中应用帧,并写入 EEPROM。 |
事件
OpenLogi 没有定义 0x8081 专属的 feature 事件类型。
frame_end 是请求,不是设备事件。
线格式
函数 0 发送 3 字节短载荷。 函数 1 至 7 发送 16 字节长载荷。
setter 回复会重复已接受的请求参数。 OpenLogi 要求函数 1 至 6 返回长回复,并检查区域 ID 回显。
get_rgb_zone_presence(fn 0)
请求载荷为 [0x00, page, 0x00]。
字节 0 选择区域存在信息,字节 1 选择页,字节 2 是零填充。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
| 0–1 | 回复前缀 | 由设备返回 | OpenLogi 不建模这些字节。 |
| 2–15 | 区域存在位图 | 14 字节;每个字节内 LSB-first | 位 i 对应区域 page_base + i。Options+ 2.8 |
set_individual_rgb_zones(fn 1)
请求有 4 个四字节槽位。 OpenLogi 会将未使用的槽位清零。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
slot × 4 | zone_id | 一个 u8 | 槽位为 0..=3;发送的 ID 不能为 0x00 或 0xff。 |
slot × 4 + 1 | red | 一个 u8 | / |
slot × 4 + 2 | green | 一个 u8 | / |
slot × 4 + 3 | blue | 一个 u8 | / |
回复必须为长回复。
OpenLogi 会验证偏移 0、4、8 和 12 处每个已发送的 zone_id。
set_consecutive_rgb_zones(fn 2)
请求以完整 24-bit RGB 设置恰好 5 个连续区域。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
| 0 | first_zone_id | 一个 u8 | 哨兵 ID 会被拒绝。 |
1 + 3 × i | 区域 i 的红色 | 一个 u8 | i 为 0..=4。 |
2 + 3 × i | 区域 i 的绿色 | 一个 u8 | / |
3 + 3 × i | 区域 i 的蓝色 | 一个 u8 | / |
回复必须为长回复。
OpenLogi 会验证字节 0 处回显的 first_zone_id。
set_consecutive_rgb_zones_delta_5bit(fn 3)
请求将 8 个连续区域编码为有符号 5-bit 通道差分值。 差分值按区域、再按通道以 MSB-first 顺序打包。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
| 0 | first_zone_id | 一个 u8 | 哨兵 ID 会被拒绝。 |
| 1–15 | packed | 15 字节 | 8 组,每组包含 3 个有符号 5-bit 差分值。OpenLogi 原样发送这些字节。 |
回复必须为长回复。
OpenLogi 会验证字节 0 处回显的 first_zone_id。
set_consecutive_rgb_zones_delta_4bit(fn 4)
请求将 10 个连续区域编码为有符号 4-bit 通道差分值。 差分值先使用高半字节,再使用低半字节。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
| 0 | first_zone_id | 一个 u8 | 哨兵 ID 会被拒绝。 |
| 1–15 | packed | 15 字节 | 10 组,每组包含 3 个有符号 4-bit 差分值。OpenLogi 原样发送这些字节。 |
回复必须为长回复。
OpenLogi 会验证字节 0 处回显的 first_zone_id。
set_range_rgb_zones(fn 5)
请求有 3 个五字节范围槽位。 OpenLogi 会将未使用的槽位清零。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
slot × 5 | first_zone_id | 一个 u8 | 槽位为 0..=2;哨兵 ID 会被拒绝。 |
slot × 5 + 1 | last_zone_id | 一个 u8 | 闭区间末尾;哨兵 ID 会被拒绝。 |
slot × 5 + 2 | red | 一个 u8 | / |
slot × 5 + 3 | green | 一个 u8 | / |
slot × 5 + 4 | blue | 一个 u8 | / |
| 15 | 填充 | 0x00 | 3 个槽位占用字节 0–14。 |
回复必须为长回复。
OpenLogi 只验证字节 0 处第一个已发送的 first_zone_id。
set_rgb_zones_single_value(fn 6)
请求将一种颜色应用到最多 13 个指定区域。 OpenLogi 会将未使用的区域 ID 字节清零。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
| 0 | red | 一个 u8 | / |
| 1 | green | 一个 u8 | / |
| 2 | blue | 一个 u8 | / |
| 3–15 | zone_ids | 最多 13 × u8 | 发送的 ID 不能为 0x00 或 0xff。 |
回复必须为长回复。
OpenLogi 会验证字节 3 处第一个已发送的 zone_id。
frame_end(fn 7)
frame_end 会提交所有暂存的区域更新。
若要单次更新,请将两个计数器都设为零。
| 字节 | 字段 | 编码 | 备注 |
|---|---|---|---|
| 0 | persistence | 0 或 1 | FramePersistence 值。 |
| 1–2 | current_frame | BE u16 | 帧号。 |
| 3–4 | frames_till_next_change | BE u16 | 到下次变化前的帧数。 |
| 5–15 | 填充 | 0x00 | OpenLogi 写入零。 |
OpenLogi 会等待函数 7 的匹配回复。 它不检查回复载荷。
OpenLogi 的用法
当 0x8070 和 0x8071 无法提供完整的纯色路径时,LightingMethod::Auto 会使用 0x8081。
LightingMethod::PerKeyV2 会直接选择此路径。
OpenLogi 会在写入前查询全部 3 个区域存在页。
它会跳过哨兵 ID 0x00、0xff,以及第 3 页中大于 0xff 的填充。
设备写入器会为每 13 个存在的区域发送一个函数 6 请求。
它会为所有存在区域写入相同颜色,然后以 Volatile、0 和 0 调用函数 7。
任一子写入失败都会阻止 frame_end。
如果 0x8081 不存在或没有可用区域,Auto 会回退到原始 0x8080。
存在 0x8071 时,OpenLogi 会在修改设备前读取软件控制状态。
它会申请 ALL_CLUSTERS、将可用 Static effect 写为底色、发送 0x8081 帧,并在新申请后重放一次。
写入失败会恢复之前的软件控制标志。 agent 写入成功后会保留租约,并在 agent 退出时交还控制权。
仅有 0x8081 而没有 0x8071 的设备会在不申请软件控制权的情况下接收帧。
用户可在键盘灯光中使用此设置。
类型化封装公开了全部 6 个区域 setter。
生产设备路径只使用 set_rgb_zones_single_value,因此它提供的是整键盘纯色,不是用户可见的逐区域映射。
未确认的细节
没有硬件捕获可验证哪些设备需要 0x8071 软件控制和 Static effect,才能显示 0x8081 颜色。
Solaar 将逐键写入视为 Static 子模式,而 OpenLogi 只在存在 0x8071 时采用该顺序。
Options+ 2.8 静态分析支持函数 1 至 6 的区域回显。 完整的多范围回复协议仍未确认,因此 OpenLogi 对函数 5 只检查第一个范围起点。
Options+ 2.8 不检查函数 7 的回复载荷。 在硬件验证前,OpenLogi 也接受任意匹配的函数 7 回复。
Rust 用法
use hidpp::{
device::Device,
feature::per_key_lighting::{
FramePersistence, PerKeyLightingFeature, Rgb, RgbZone, ZonePresencePage,
},
protocol::v20::Hidpp20Error,
};
async fn stage_two_zones(device: &mut Device) -> Result<(), Hidpp20Error> {
device.enumerate_features().await?;
let Some(feature) = device.get_feature::<PerKeyLightingFeature>() else {
return Ok(());
};
let _presence = feature
.get_rgb_zone_presence(ZonePresencePage::Zones0To111)
.await?;
feature
.set_individual_rgb_zones(&[
RgbZone {
zone_id: 1,
color: Rgb {
red: 0xff,
green: 0x00,
blue: 0x00,
},
},
RgbZone {
zone_id: 2,
color: Rgb {
red: 0x00,
green: 0x00,
blue: 0xff,
},
},
])
.await?;
feature.frame_end(FramePersistence::Volatile, 0, 0).await
}