hidpp/feature/per_key_lighting/
mod.rs1#[cfg(test)]
11mod tests;
12
13use std::sync::Arc;
14
15use num_enum::{IntoPrimitive, TryFromPrimitive};
16
17use crate::{
18 channel::HidppChannel,
19 feature::{CreatableFeature, Feature, FeatureEndpoint},
20 protocol::v20::{ErrorType, Hidpp20Error},
21};
22
23pub const ZONE_PRESENCE_PAGE_LEN: usize = 14;
25pub const DELTA_PACKED_LEN: usize = 15;
27const TYPE_RGB_ZONE_PRESENCE: u8 = 0x00;
29const MAX_INDIVIDUAL_ZONES: usize = 4;
31const CONSECUTIVE_ZONES: usize = 5;
33const MAX_RANGES: usize = 3;
35const MAX_SINGLE_VALUE_ZONES: usize = 13;
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize))]
41pub struct Rgb {
42 pub red: u8,
44 pub green: u8,
46 pub blue: u8,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize))]
53pub struct RgbZone {
54 pub zone_id: u8,
56 pub color: Rgb,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize))]
63pub struct RgbZoneRange {
64 pub first_zone_id: u8,
66 pub last_zone_id: u8,
68 pub color: Rgb,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize))]
75#[non_exhaustive]
76#[repr(u8)]
77pub enum ZonePresencePage {
78 Zones0To111 = 0,
80 Zones112To223 = 1,
82 Zones224To255 = 2,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89#[non_exhaustive]
90#[repr(u8)]
91pub enum FramePersistence {
92 Volatile = 0,
94 VolatileAndNonVolatile = 1,
96}
97
98#[derive(Clone)]
100pub struct PerKeyLightingFeature {
101 endpoint: FeatureEndpoint,
103}
104
105impl CreatableFeature for PerKeyLightingFeature {
106 const ID: u16 = 0x8081;
107 const STARTING_VERSION: u8 = 0;
108
109 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
110 Self {
111 endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
112 }
113 }
114}
115
116impl Feature for PerKeyLightingFeature {}
117
118impl PerKeyLightingFeature {
119 pub async fn get_rgb_zone_presence(
124 &self,
125 page: ZonePresencePage,
126 ) -> Result<[u8; ZONE_PRESENCE_PAGE_LEN], Hidpp20Error> {
127 let payload = self
128 .endpoint
129 .call(0, [TYPE_RGB_ZONE_PRESENCE, page.into(), 0])
130 .await?
131 .extend_payload();
132 let mut bitfield = [0; ZONE_PRESENCE_PAGE_LEN];
133 bitfield.copy_from_slice(&payload[2..2 + ZONE_PRESENCE_PAGE_LEN]);
134 Ok(bitfield)
135 }
136
137 pub async fn set_individual_rgb_zones(&self, zones: &[RgbZone]) -> Result<(), Hidpp20Error> {
141 validate_individual_zones(zones)?;
142 self.endpoint
143 .call_long(1, individual_zones_args(zones))
144 .await?;
145 Ok(())
146 }
147
148 pub async fn set_consecutive_rgb_zones(
150 &self,
151 first_zone_id: u8,
152 colors: [Rgb; CONSECUTIVE_ZONES],
153 ) -> Result<(), Hidpp20Error> {
154 validate_zone_id(first_zone_id)?;
155 self.endpoint
156 .call_long(2, consecutive_zones_args(first_zone_id, colors))
157 .await?;
158 Ok(())
159 }
160
161 pub async fn set_consecutive_rgb_zones_delta_5bit(
168 &self,
169 first_zone_id: u8,
170 packed: [u8; DELTA_PACKED_LEN],
171 ) -> Result<(), Hidpp20Error> {
172 self.send_delta(3, first_zone_id, packed).await
173 }
174
175 pub async fn set_consecutive_rgb_zones_delta_4bit(
181 &self,
182 first_zone_id: u8,
183 packed: [u8; DELTA_PACKED_LEN],
184 ) -> Result<(), Hidpp20Error> {
185 self.send_delta(4, first_zone_id, packed).await
186 }
187
188 pub async fn set_range_rgb_zones(&self, ranges: &[RgbZoneRange]) -> Result<(), Hidpp20Error> {
192 validate_ranges(ranges)?;
193 self.endpoint.call_long(5, range_zones_args(ranges)).await?;
194 Ok(())
195 }
196
197 pub async fn set_rgb_zones_single_value(
201 &self,
202 color: Rgb,
203 zone_ids: &[u8],
204 ) -> Result<(), Hidpp20Error> {
205 validate_single_value_zones(zone_ids)?;
206 self.endpoint
207 .call_long(6, single_value_args(color, zone_ids))
208 .await?;
209 Ok(())
210 }
211
212 pub async fn frame_end(
217 &self,
218 persistence: FramePersistence,
219 current_frame: u16,
220 frames_till_next_change: u16,
221 ) -> Result<(), Hidpp20Error> {
222 let args = frame_end_args(persistence, current_frame, frames_till_next_change);
223 self.endpoint.call_long(7, args).await?;
224 Ok(())
225 }
226
227 async fn send_delta(
229 &self,
230 function: u8,
231 first_zone_id: u8,
232 packed: [u8; DELTA_PACKED_LEN],
233 ) -> Result<(), Hidpp20Error> {
234 validate_zone_id(first_zone_id)?;
235 self.endpoint
236 .call_long(function, delta_args(first_zone_id, packed))
237 .await?;
238 Ok(())
239 }
240}
241
242fn validate_zone_id(zone_id: u8) -> Result<(), Hidpp20Error> {
243 if matches!(zone_id, 0 | 0xff) {
244 return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
245 }
246 Ok(())
247}
248
249fn validate_individual_zones(zones: &[RgbZone]) -> Result<(), Hidpp20Error> {
250 for zone in zones.iter().take(MAX_INDIVIDUAL_ZONES) {
251 validate_zone_id(zone.zone_id)?;
252 }
253 Ok(())
254}
255
256fn validate_ranges(ranges: &[RgbZoneRange]) -> Result<(), Hidpp20Error> {
257 for range in ranges.iter().take(MAX_RANGES) {
258 validate_zone_id(range.first_zone_id)?;
259 validate_zone_id(range.last_zone_id)?;
260 }
261 Ok(())
262}
263
264fn validate_single_value_zones(zone_ids: &[u8]) -> Result<(), Hidpp20Error> {
265 for &zone_id in zone_ids.iter().take(MAX_SINGLE_VALUE_ZONES) {
266 validate_zone_id(zone_id)?;
267 }
268 Ok(())
269}
270
271fn individual_zones_args(zones: &[RgbZone]) -> [u8; 16] {
273 let mut args = [0; 16];
274 for (slot, zone) in zones.iter().take(MAX_INDIVIDUAL_ZONES).enumerate() {
275 let base = slot * 4;
276 args[base] = zone.zone_id;
277 args[base + 1] = zone.color.red;
278 args[base + 2] = zone.color.green;
279 args[base + 3] = zone.color.blue;
280 }
281 args
282}
283
284fn consecutive_zones_args(first_zone_id: u8, colors: [Rgb; CONSECUTIVE_ZONES]) -> [u8; 16] {
286 let mut args = [0; 16];
287 args[0] = first_zone_id;
288 for (i, color) in colors.iter().enumerate() {
289 let base = 1 + i * 3;
290 args[base] = color.red;
291 args[base + 1] = color.green;
292 args[base + 2] = color.blue;
293 }
294 args
295}
296
297fn range_zones_args(ranges: &[RgbZoneRange]) -> [u8; 16] {
299 let mut args = [0; 16];
300 for (slot, range) in ranges.iter().take(MAX_RANGES).enumerate() {
301 let base = slot * 5;
302 args[base] = range.first_zone_id;
303 args[base + 1] = range.last_zone_id;
304 args[base + 2] = range.color.red;
305 args[base + 3] = range.color.green;
306 args[base + 4] = range.color.blue;
307 }
308 args
309}
310
311fn single_value_args(color: Rgb, zone_ids: &[u8]) -> [u8; 16] {
313 let mut args = [0; 16];
314 args[0] = color.red;
315 args[1] = color.green;
316 args[2] = color.blue;
317 for (i, &zone_id) in zone_ids.iter().take(MAX_SINGLE_VALUE_ZONES).enumerate() {
318 args[3 + i] = zone_id;
319 }
320 args
321}
322
323fn frame_end_args(
325 persistence: FramePersistence,
326 current_frame: u16,
327 frames_till_next_change: u16,
328) -> [u8; 16] {
329 let [frame_hi, frame_lo] = current_frame.to_be_bytes();
330 let [next_hi, next_lo] = frames_till_next_change.to_be_bytes();
331 let mut args = [0; 16];
332 args[..5].copy_from_slice(&[persistence.into(), frame_hi, frame_lo, next_hi, next_lo]);
333 args
334}
335
336fn delta_args(first_zone_id: u8, packed: [u8; DELTA_PACKED_LEN]) -> [u8; 16] {
338 let mut args = [0; 16];
339 args[0] = first_zone_id;
340 args[1..1 + DELTA_PACKED_LEN].copy_from_slice(&packed);
341 args
342}