1#![cfg_attr(docsrs, feature(doc_cfg))]
8#![warn(missing_docs)]
9
10use std::collections::HashSet;
11use std::error::Error;
12use std::fmt::{Display, Formatter};
13use std::iter::zip;
14use std::str::Utf8Error;
15use std::sync::RwLock;
16use std::sync::{Arc, Mutex, PoisonError};
17use std::time::Duration;
18
19use crate::images::{convert_image, ImageRect};
20use hidapi::{HidApi, HidDevice, HidError, HidResult};
21use image::{DynamicImage, ImageError};
22
23use crate::info::{is_vendor_familiar, Kind};
24use crate::util::{extract_str, flip_key_index, get_feature_report, read_button_states, read_data, read_encoder_input, read_lcd_input, send_feature_report, write_data};
25
26pub mod info;
28pub mod util;
30pub mod images;
32
33#[cfg(feature = "async")]
35#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
36pub mod asynchronous;
37#[cfg(feature = "async")]
38#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
39pub use asynchronous::AsyncStreamDeck;
40
41pub fn new_hidapi() -> HidResult<HidApi> {
45 HidApi::new()
46}
47
48pub fn refresh_device_list(hidapi: &mut HidApi) -> HidResult<()> {
50 hidapi.refresh_devices()
51}
52
53pub fn list_devices(hidapi: &HidApi) -> Vec<(Kind, String)> {
57 hidapi
58 .device_list()
59 .filter_map(|d| {
60 if !is_vendor_familiar(&d.vendor_id()) {
61 return None;
62 }
63
64 if let Some(serial) = d.serial_number() {
65 Some((Kind::from_vid_pid(d.vendor_id(), d.product_id())?, serial.to_string()))
66 } else {
67 None
68 }
69 })
70 .collect::<HashSet<_>>()
71 .into_iter()
72 .collect()
73}
74
75#[derive(Clone, Debug)]
77pub enum StreamDeckInput {
78 NoData,
80
81 ButtonStateChange(Vec<bool>),
83
84 EncoderStateChange(Vec<bool>),
86
87 EncoderTwist(Vec<i8>),
89
90 TouchScreenPress(u16, u16),
92
93 TouchScreenLongPress(u16, u16),
95
96 TouchScreenSwipe((u16, u16), (u16, u16)),
98}
99
100impl StreamDeckInput {
101 pub fn is_empty(&self) -> bool {
103 matches!(self, StreamDeckInput::NoData)
104 }
105}
106
107pub struct StreamDeck {
109 kind: Kind,
111 device: HidDevice,
113 image_cache: RwLock<Vec<ImageCache>>,
115}
116
117struct ImageCache {
118 key: u8,
119 image_data: Vec<u8>,
120}
121
122impl StreamDeck {
124 pub fn connect(hidapi: &HidApi, kind: Kind, serial: &str) -> Result<StreamDeck, StreamDeckError> {
126 let device = hidapi.open_serial(kind.vendor_id(), kind.product_id(), serial)?;
127
128 Ok(StreamDeck {
129 kind,
130 device,
131 image_cache: RwLock::new(vec![]),
132 })
133 }
134}
135
136impl StreamDeck {
138 pub fn kind(&self) -> Kind {
140 self.kind
141 }
142
143 pub fn manufacturer(&self) -> Result<String, StreamDeckError> {
145 Ok(self.device.get_manufacturer_string()?.unwrap_or_else(|| "Unknown".to_string()))
146 }
147
148 pub fn product(&self) -> Result<String, StreamDeckError> {
150 Ok(self.device.get_product_string()?.unwrap_or_else(|| "Unknown".to_string()))
151 }
152
153 pub fn serial_number(&self) -> Result<String, StreamDeckError> {
155 match self.kind {
156 Kind::Original | Kind::Mini => {
157 let bytes = get_feature_report(&self.device, 0x03, 17)?;
158 Ok(extract_str(&bytes[5..])?)
159 }
160
161 Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => {
162 let bytes = get_feature_report(&self.device, 0x03, 32)?;
163 Ok(extract_str(&bytes[5..])?)
164 }
165
166 _ => {
167 let bytes = get_feature_report(&self.device, 0x06, 32)?;
168 Ok(extract_str(&bytes[2..])?)
169 }
170 }
171 .map(|s| s.replace('\u{0001}', ""))
172 }
173
174 pub fn firmware_version(&self) -> Result<String, StreamDeckError> {
176 match self.kind {
177 Kind::Original | Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord => {
178 let bytes = get_feature_report(&self.device, 0x04, 17)?;
179 Ok(extract_str(&bytes[5..])?)
180 }
181
182 Kind::MiniMk2Module => {
183 let bytes = get_feature_report(&self.device, 0xA1, 17)?;
184 Ok(extract_str(&bytes[5..])?)
185 }
186
187 _ => {
188 let bytes = get_feature_report(&self.device, 0x05, 32)?;
189 Ok(extract_str(&bytes[6..])?)
190 }
191 }
192 }
193
194 pub fn read_input(&self, timeout: Option<Duration>) -> Result<StreamDeckInput, StreamDeckError> {
196 match &self.kind {
197 Kind::Plus => {
198 let data = read_data(&self.device, 14.max(5 + self.kind.encoder_count() as usize), timeout)?;
199
200 if data[0] == 0 {
201 return Ok(StreamDeckInput::NoData);
202 }
203
204 match &data[1] {
205 0x0 => Ok(StreamDeckInput::ButtonStateChange(read_button_states(&self.kind, &data))),
206
207 0x2 => Ok(read_lcd_input(&data)?),
208
209 0x3 => Ok(read_encoder_input(&self.kind, &data)?),
210
211 _ => Err(StreamDeckError::BadData),
212 }
213 }
214
215 _ => {
216 let data = match self.kind {
217 Kind::Original | Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => read_data(&self.device, 1 + self.kind.key_count() as usize, timeout),
218 _ => read_data(&self.device, 4 + self.kind.key_count() as usize + self.kind.touchpoint_count() as usize, timeout),
219 }?;
220
221 if data[0] == 0 {
222 return Ok(StreamDeckInput::NoData);
223 }
224
225 Ok(StreamDeckInput::ButtonStateChange(read_button_states(&self.kind, &data)))
226 }
227 }
228 }
229
230 pub fn reset(&self) -> Result<(), StreamDeckError> {
232 match self.kind {
233 Kind::Original | Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => {
234 let mut buf = vec![0x0B, 0x63];
235
236 buf.extend(vec![0u8; 15]);
237
238 Ok(send_feature_report(&self.device, buf.as_slice())?)
239 }
240
241 _ => {
242 let mut buf = vec![0x03, 0x02];
243
244 buf.extend(vec![0u8; 30]);
245
246 Ok(send_feature_report(&self.device, buf.as_slice())?)
247 }
248 }
249 }
250
251 pub fn set_brightness(&self, percent: u8) -> Result<(), StreamDeckError> {
253 let percent = percent.clamp(0, 100);
254
255 match self.kind {
256 Kind::Original | Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => {
257 let mut buf = vec![0x05, 0x55, 0xaa, 0xd1, 0x01, percent];
258
259 buf.extend(vec![0u8; 11]);
260
261 Ok(send_feature_report(&self.device, buf.as_slice())?)
262 }
263
264 _ => {
265 let mut buf = vec![0x03, 0x08, percent];
266
267 buf.extend(vec![0u8; 29]);
268
269 Ok(send_feature_report(&self.device, buf.as_slice())?)
270 }
271 }
272 }
273
274 fn send_image(&self, key: u8, image_data: &[u8]) -> Result<(), StreamDeckError> {
275 if key >= self.kind.key_count() {
276 return Err(StreamDeckError::InvalidKeyIndex);
277 }
278
279 let key = if let Kind::Original = self.kind { flip_key_index(&self.kind, key) } else { key };
280
281 if !self.kind.is_visual() {
282 return Err(StreamDeckError::NoScreen);
283 }
284
285 self.write_image_data_reports(
286 image_data,
287 WriteImageParameters::for_key(self.kind, image_data.len()),
288 |page_number, this_length, last_package| match self.kind {
289 Kind::Original => vec![0x02, 0x01, (page_number + 1) as u8, 0, if last_package { 1 } else { 0 }, key + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
290
291 Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => vec![0x02, 0x01, page_number as u8, 0, if last_package { 1 } else { 0 }, key + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
292
293 _ => vec![
294 0x02,
295 0x07,
296 key,
297 if last_package { 1 } else { 0 },
298 (this_length & 0xff) as u8,
299 (this_length >> 8) as u8,
300 (page_number & 0xff) as u8,
301 (page_number >> 8) as u8,
302 ],
303 },
304 )?;
305 Ok(())
306 }
307
308 pub fn write_image(&self, key: u8, image_data: &[u8]) -> Result<(), StreamDeckError> {
311 let cache_entry = ImageCache { key, image_data: image_data.to_vec() };
312
313 self.image_cache.write()?.push(cache_entry);
314
315 Ok(())
316 }
317
318 pub fn write_lcd(&self, x: u16, y: u16, rect: &ImageRect) -> Result<(), StreamDeckError> {
321 match self.kind {
322 Kind::Plus => (),
323 _ => return Err(StreamDeckError::UnsupportedOperation),
324 }
325
326 self.write_image_data_reports(
327 rect.data.as_slice(),
328 WriteImageParameters {
329 image_report_length: 1024,
330 image_report_payload_length: 1024 - 16,
331 },
332 |page_number, this_length, last_package| {
333 vec![
334 0x02,
335 0x0c,
336 (x & 0xff) as u8,
337 (x >> 8) as u8,
338 (y & 0xff) as u8,
339 (y >> 8) as u8,
340 (rect.w & 0xff) as u8,
341 (rect.w >> 8) as u8,
342 (rect.h & 0xff) as u8,
343 (rect.h >> 8) as u8,
344 if last_package { 1 } else { 0 },
345 (page_number & 0xff) as u8,
346 (page_number >> 8) as u8,
347 (this_length & 0xff) as u8,
348 (this_length >> 8) as u8,
349 0,
350 ]
351 },
352 )
353 }
354
355 pub fn write_lcd_fill(&self, image_data: &[u8]) -> Result<(), StreamDeckError> {
364 match self.kind {
365 Kind::Neo => self.write_image_data_reports(
366 image_data,
367 WriteImageParameters {
368 image_report_length: 1024,
369 image_report_payload_length: 1024 - 8,
370 },
371 |page_number, this_length, last_package| {
372 vec![
373 0x02,
374 0x0b,
375 0,
376 if last_package { 1 } else { 0 },
377 (this_length & 0xff) as u8,
378 (this_length >> 8) as u8,
379 (page_number & 0xff) as u8,
380 (page_number >> 8) as u8,
381 ]
382 },
383 ),
384
385 Kind::Plus => {
386 let (w, h) = self.kind.lcd_strip_size().unwrap();
387
388 self.write_image_data_reports(
389 image_data,
390 WriteImageParameters {
391 image_report_length: 1024,
392 image_report_payload_length: 1024 - 16,
393 },
394 |page_number, this_length, last_package| {
395 vec![
396 0x02,
397 0x0c,
398 0,
399 0,
400 0,
401 0,
402 (w & 0xff) as u8,
403 (w >> 8) as u8,
404 (h & 0xff) as u8,
405 (h >> 8) as u8,
406 if last_package { 1 } else { 0 },
407 (page_number & 0xff) as u8,
408 (page_number >> 8) as u8,
409 (this_length & 0xff) as u8,
410 (this_length >> 8) as u8,
411 0,
412 ]
413 },
414 )
415 }
416
417 _ => Err(StreamDeckError::UnsupportedOperation),
418 }
419 }
420
421 pub fn clear_button_image(&self, key: u8) -> Result<(), StreamDeckError> {
424 self.send_image(key, &self.kind.blank_image())
425 }
426
427 pub fn clear_all_button_images(&self) -> Result<(), StreamDeckError> {
430 for i in 0..self.kind.key_count() {
431 self.clear_button_image(i)?
432 }
433 Ok(())
434 }
435
436 pub fn set_button_image(&self, key: u8, image: DynamicImage) -> Result<(), StreamDeckError> {
439 let image_data = convert_image(self.kind, image)?;
440 self.write_image(key, &image_data)?;
441 Ok(())
442 }
443
444 pub fn set_touchpoint_color(&self, point: u8, red: u8, green: u8, blue: u8) -> Result<(), StreamDeckError> {
446 if point >= self.kind.touchpoint_count() {
447 return Err(StreamDeckError::InvalidTouchPointIndex);
448 }
449
450 let mut buf = vec![0x03, 0x06];
451
452 let touchpoint_index: u8 = point + self.kind.key_count();
453 buf.extend(vec![touchpoint_index]);
454 buf.extend(vec![red, green, blue]);
455
456 Ok(send_feature_report(&self.device, buf.as_slice())?)
457 }
458
459 pub fn flush(&self) -> Result<(), StreamDeckError> {
461 if self.image_cache.read()?.is_empty() {
462 return Ok(());
463 }
464
465 for image in self.image_cache.read()?.iter() {
466 self.send_image(image.key, &image.image_data)?;
467 }
468
469 self.image_cache.write()?.clear();
470
471 Ok(())
472 }
473
474 pub fn get_reader(self: &Arc<Self>) -> Arc<DeviceStateReader> {
476 #[allow(clippy::arc_with_non_send_sync)]
477 Arc::new(DeviceStateReader {
478 device: self.clone(),
479 states: Mutex::new(DeviceState {
480 buttons: vec![false; self.kind.key_count() as usize + self.kind.touchpoint_count() as usize],
481 encoders: vec![false; self.kind.encoder_count() as usize],
482 }),
483 })
484 }
485
486 fn write_image_data_reports<T>(&self, image_data: &[u8], parameters: WriteImageParameters, header_fn: T) -> Result<(), StreamDeckError>
487 where
488 T: Fn(usize, usize, bool) -> Vec<u8>,
489 {
490 let image_report_length = parameters.image_report_length;
491 let image_report_payload_length = parameters.image_report_payload_length;
492
493 let mut page_number = 0;
494 let mut bytes_remaining = image_data.len();
495
496 while bytes_remaining > 0 {
497 let this_length = bytes_remaining.min(image_report_payload_length);
498 let bytes_sent = page_number * image_report_payload_length;
499
500 let mut buf: Vec<u8> = header_fn(page_number, this_length, this_length == bytes_remaining);
502
503 buf.extend(&image_data[bytes_sent..bytes_sent + this_length]);
504
505 buf.extend(vec![0u8; image_report_length - buf.len()]);
507
508 write_data(&self.device, &buf)?;
509
510 bytes_remaining -= this_length;
511 page_number += 1;
512 }
513
514 Ok(())
515 }
516}
517
518#[derive(Clone, Copy)]
519struct WriteImageParameters {
520 pub image_report_length: usize,
521 pub image_report_payload_length: usize,
522}
523
524impl WriteImageParameters {
525 pub fn for_key(kind: Kind, image_data_len: usize) -> Self {
526 let image_report_length = match kind {
527 Kind::Original => 8191,
528 _ => 1024,
529 };
530
531 let image_report_header_length = match kind {
532 Kind::Original | Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => 16,
533 _ => 8,
534 };
535
536 let image_report_payload_length = match kind {
537 Kind::Original => image_data_len / 2,
538 _ => image_report_length - image_report_header_length,
539 };
540
541 Self {
542 image_report_length,
543 image_report_payload_length,
544 }
545 }
546}
547
548#[derive(Debug)]
550pub enum StreamDeckError {
551 HidError(HidError),
553
554 Utf8Error(Utf8Error),
556
557 ImageError(ImageError),
559
560 #[cfg(feature = "async")]
561 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
562 JoinError(tokio::task::JoinError),
564
565 PoisonError,
567
568 NoScreen,
570
571 InvalidKeyIndex,
573
574 InvalidTouchPointIndex,
576
577 UnrecognizedPID,
579
580 UnsupportedOperation,
582
583 BadData,
585}
586
587impl Display for StreamDeckError {
588 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
589 write!(f, "{:?}", self)
590 }
591}
592
593impl Error for StreamDeckError {}
594
595impl From<HidError> for StreamDeckError {
596 fn from(e: HidError) -> Self {
597 Self::HidError(e)
598 }
599}
600
601impl From<Utf8Error> for StreamDeckError {
602 fn from(e: Utf8Error) -> Self {
603 Self::Utf8Error(e)
604 }
605}
606
607impl From<ImageError> for StreamDeckError {
608 fn from(e: ImageError) -> Self {
609 Self::ImageError(e)
610 }
611}
612
613#[cfg(feature = "async")]
614impl From<tokio::task::JoinError> for StreamDeckError {
615 fn from(e: tokio::task::JoinError) -> Self {
616 Self::JoinError(e)
617 }
618}
619
620impl<T> From<PoisonError<T>> for StreamDeckError {
621 fn from(_value: PoisonError<T>) -> Self {
622 Self::PoisonError
623 }
624}
625
626#[derive(Copy, Clone, Debug, Hash)]
628pub enum DeviceStateUpdate {
629 ButtonDown(u8),
631
632 ButtonUp(u8),
634
635 EncoderDown(u8),
637
638 EncoderUp(u8),
640
641 EncoderTwist(u8, i8),
643
644 TouchPointDown(u8),
646
647 TouchPointUp(u8),
649
650 TouchScreenPress(u16, u16),
652
653 TouchScreenLongPress(u16, u16),
655
656 TouchScreenSwipe((u16, u16), (u16, u16)),
658}
659
660#[derive(Default)]
661struct DeviceState {
662 pub buttons: Vec<bool>,
664 pub encoders: Vec<bool>,
665}
666
667pub struct DeviceStateReader {
669 device: Arc<StreamDeck>,
670 states: Mutex<DeviceState>,
671}
672
673impl DeviceStateReader {
674 pub fn read(&self, timeout: Option<Duration>) -> Result<Vec<DeviceStateUpdate>, StreamDeckError> {
676 let input = self.device.read_input(timeout)?;
677 let mut my_states = self.states.lock()?;
678
679 let mut updates = vec![];
680
681 match input {
682 StreamDeckInput::ButtonStateChange(buttons) => {
683 for (index, (their, mine)) in zip(buttons.iter(), my_states.buttons.iter()).enumerate() {
684 if their != mine {
685 let key_count = self.device.kind.key_count();
686 if index < key_count as usize {
687 if *their {
688 updates.push(DeviceStateUpdate::ButtonDown(index as u8));
689 } else {
690 updates.push(DeviceStateUpdate::ButtonUp(index as u8));
691 }
692 } else if *their {
693 updates.push(DeviceStateUpdate::TouchPointDown(index as u8 - key_count));
694 } else {
695 updates.push(DeviceStateUpdate::TouchPointUp(index as u8 - key_count));
696 }
697 }
698 }
699
700 my_states.buttons = buttons;
701 }
702
703 StreamDeckInput::EncoderStateChange(encoders) => {
704 for (index, (their, mine)) in zip(encoders.iter(), my_states.encoders.iter()).enumerate() {
705 if *their != *mine {
706 if *their {
707 updates.push(DeviceStateUpdate::EncoderDown(index as u8));
708 } else {
709 updates.push(DeviceStateUpdate::EncoderUp(index as u8));
710 }
711 }
712 }
713
714 my_states.encoders = encoders;
715 }
716
717 StreamDeckInput::EncoderTwist(twist) => {
718 for (index, change) in twist.iter().enumerate() {
719 if *change != 0 {
720 updates.push(DeviceStateUpdate::EncoderTwist(index as u8, *change));
721 }
722 }
723 }
724
725 StreamDeckInput::TouchScreenPress(x, y) => {
726 updates.push(DeviceStateUpdate::TouchScreenPress(x, y));
727 }
728
729 StreamDeckInput::TouchScreenLongPress(x, y) => {
730 updates.push(DeviceStateUpdate::TouchScreenLongPress(x, y));
731 }
732
733 StreamDeckInput::TouchScreenSwipe(s, e) => {
734 updates.push(DeviceStateUpdate::TouchScreenSwipe(s, e));
735 }
736
737 _ => {}
738 }
739
740 drop(my_states);
741
742 Ok(updates)
743 }
744}