1#[cfg(test)]
11mod tests;
12
13use num_enum::{IntoPrimitive, TryFromPrimitive};
14use openlogi_hidpp_derive::Feature;
15
16use crate::{
17 feature::FeatureEndpoint,
18 protocol::v20::{ErrorType, Hidpp20Error},
19};
20
21pub const ZONE_PRESENCE_PAGE_LEN: usize = 14;
23pub const DELTA_PACKED_LEN: usize = 15;
25const TYPE_RGB_ZONE_PRESENCE: u8 = 0x00;
27const MAX_INDIVIDUAL_ZONES: usize = 4;
29const CONSECUTIVE_ZONES: usize = 5;
31const MAX_RANGES: usize = 3;
33pub const MAX_SINGLE_VALUE_ZONES: usize = 13;
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize))]
43pub struct Rgb {
44 pub red: u8,
46 pub green: u8,
48 pub blue: u8,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize))]
55pub struct RgbZone {
56 pub zone_id: u8,
58 pub color: Rgb,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize))]
65pub struct RgbZoneRange {
66 pub first_zone_id: u8,
68 pub last_zone_id: u8,
70 pub color: Rgb,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77#[non_exhaustive]
78#[repr(u8)]
79pub enum ZonePresencePage {
80 Zones0To111 = 0,
82 Zones112To223 = 1,
84 Zones224To255 = 2,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize))]
91#[non_exhaustive]
92#[repr(u8)]
93pub enum FramePersistence {
94 Volatile = 0,
96 VolatileAndNonVolatile = 1,
98}
99
100#[derive(Clone, Feature)]
102#[creatable(id = 0x8081, version = 0)]
103pub struct PerKeyLightingFeature {
104 endpoint: FeatureEndpoint,
106}
107
108impl PerKeyLightingFeature {
109 pub async fn get_rgb_zone_presence(
114 &self,
115 page: ZonePresencePage,
116 ) -> Result<[u8; ZONE_PRESENCE_PAGE_LEN], Hidpp20Error> {
117 let payload = self
118 .endpoint
119 .call(0, [TYPE_RGB_ZONE_PRESENCE, page.into(), 0])
120 .await?
121 .extend_payload();
122 let mut bitfield = [0; ZONE_PRESENCE_PAGE_LEN];
123 bitfield.copy_from_slice(&payload[2..2 + ZONE_PRESENCE_PAGE_LEN]);
124 Ok(bitfield)
125 }
126
127 pub async fn set_individual_rgb_zones(&self, zones: &[RgbZone]) -> Result<(), Hidpp20Error> {
131 validate_individual_zones(zones)?;
132 self.endpoint
133 .call_long(1, individual_zones_args(zones))
134 .await?;
135 Ok(())
136 }
137
138 pub async fn set_consecutive_rgb_zones(
140 &self,
141 first_zone_id: u8,
142 colors: [Rgb; CONSECUTIVE_ZONES],
143 ) -> Result<(), Hidpp20Error> {
144 validate_zone_id(first_zone_id)?;
145 self.endpoint
146 .call_long(2, consecutive_zones_args(first_zone_id, colors))
147 .await?;
148 Ok(())
149 }
150
151 pub async fn set_consecutive_rgb_zones_delta_5bit(
158 &self,
159 first_zone_id: u8,
160 packed: [u8; DELTA_PACKED_LEN],
161 ) -> Result<(), Hidpp20Error> {
162 self.send_delta(3, first_zone_id, packed).await
163 }
164
165 pub async fn set_consecutive_rgb_zones_delta_4bit(
171 &self,
172 first_zone_id: u8,
173 packed: [u8; DELTA_PACKED_LEN],
174 ) -> Result<(), Hidpp20Error> {
175 self.send_delta(4, first_zone_id, packed).await
176 }
177
178 pub async fn set_range_rgb_zones(&self, ranges: &[RgbZoneRange]) -> Result<(), Hidpp20Error> {
182 validate_ranges(ranges)?;
183 self.endpoint.call_long(5, range_zones_args(ranges)).await?;
184 Ok(())
185 }
186
187 pub async fn set_rgb_zones_single_value(
191 &self,
192 color: Rgb,
193 zone_ids: &[u8],
194 ) -> Result<(), Hidpp20Error> {
195 validate_single_value_zones(zone_ids)?;
196 self.endpoint
197 .call_long(6, single_value_args(color, zone_ids))
198 .await?;
199 Ok(())
200 }
201
202 pub async fn frame_end(
207 &self,
208 persistence: FramePersistence,
209 current_frame: u16,
210 frames_till_next_change: u16,
211 ) -> Result<(), Hidpp20Error> {
212 let args = frame_end_args(persistence, current_frame, frames_till_next_change);
213 self.endpoint.call_long(7, args).await?;
214 Ok(())
215 }
216
217 async fn send_delta(
219 &self,
220 function: u8,
221 first_zone_id: u8,
222 packed: [u8; DELTA_PACKED_LEN],
223 ) -> Result<(), Hidpp20Error> {
224 validate_zone_id(first_zone_id)?;
225 self.endpoint
226 .call_long(function, delta_args(first_zone_id, packed))
227 .await?;
228 Ok(())
229 }
230}
231
232fn validate_zone_id(zone_id: u8) -> Result<(), Hidpp20Error> {
233 if matches!(zone_id, 0 | 0xff) {
234 return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
235 }
236 Ok(())
237}
238
239fn validate_individual_zones(zones: &[RgbZone]) -> Result<(), Hidpp20Error> {
240 for zone in zones.iter().take(MAX_INDIVIDUAL_ZONES) {
241 validate_zone_id(zone.zone_id)?;
242 }
243 Ok(())
244}
245
246fn validate_ranges(ranges: &[RgbZoneRange]) -> Result<(), Hidpp20Error> {
247 for range in ranges.iter().take(MAX_RANGES) {
248 validate_zone_id(range.first_zone_id)?;
249 validate_zone_id(range.last_zone_id)?;
250 }
251 Ok(())
252}
253
254fn validate_single_value_zones(zone_ids: &[u8]) -> Result<(), Hidpp20Error> {
255 for &zone_id in zone_ids.iter().take(MAX_SINGLE_VALUE_ZONES) {
256 validate_zone_id(zone_id)?;
257 }
258 Ok(())
259}
260
261fn individual_zones_args(zones: &[RgbZone]) -> [u8; 16] {
263 let mut args = [0; 16];
264 for (slot, zone) in zones.iter().take(MAX_INDIVIDUAL_ZONES).enumerate() {
265 let base = slot * 4;
266 args[base] = zone.zone_id;
267 args[base + 1] = zone.color.red;
268 args[base + 2] = zone.color.green;
269 args[base + 3] = zone.color.blue;
270 }
271 args
272}
273
274fn consecutive_zones_args(first_zone_id: u8, colors: [Rgb; CONSECUTIVE_ZONES]) -> [u8; 16] {
276 let mut args = [0; 16];
277 args[0] = first_zone_id;
278 for (i, color) in colors.iter().enumerate() {
279 let base = 1 + i * 3;
280 args[base] = color.red;
281 args[base + 1] = color.green;
282 args[base + 2] = color.blue;
283 }
284 args
285}
286
287fn range_zones_args(ranges: &[RgbZoneRange]) -> [u8; 16] {
289 let mut args = [0; 16];
290 for (slot, range) in ranges.iter().take(MAX_RANGES).enumerate() {
291 let base = slot * 5;
292 args[base] = range.first_zone_id;
293 args[base + 1] = range.last_zone_id;
294 args[base + 2] = range.color.red;
295 args[base + 3] = range.color.green;
296 args[base + 4] = range.color.blue;
297 }
298 args
299}
300
301fn single_value_args(color: Rgb, zone_ids: &[u8]) -> [u8; 16] {
303 let mut args = [0; 16];
304 args[0] = color.red;
305 args[1] = color.green;
306 args[2] = color.blue;
307 for (i, &zone_id) in zone_ids.iter().take(MAX_SINGLE_VALUE_ZONES).enumerate() {
308 args[3 + i] = zone_id;
309 }
310 args
311}
312
313fn frame_end_args(
315 persistence: FramePersistence,
316 current_frame: u16,
317 frames_till_next_change: u16,
318) -> [u8; 16] {
319 let [frame_hi, frame_lo] = current_frame.to_be_bytes();
320 let [next_hi, next_lo] = frames_till_next_change.to_be_bytes();
321 let mut args = [0; 16];
322 args[..5].copy_from_slice(&[persistence.into(), frame_hi, frame_lo, next_hi, next_lo]);
323 args
324}
325
326fn delta_args(first_zone_id: u8, packed: [u8; DELTA_PACKED_LEN]) -> [u8; 16] {
328 let mut args = [0; 16];
329 args[0] = first_zone_id;
330 args[1..=DELTA_PACKED_LEN].copy_from_slice(&packed);
331 args
332}