Skip to main content

ajazz_rs/
lib.rs

1//! Ajazz library
2//!
3//! Library for interacting with Ajazz devices through [hidapi](https://crates.io/crates/hidapi).
4
5#![cfg_attr(docsrs, feature(doc_cfg))]
6#![warn(missing_docs)]
7
8use std::collections::HashSet;
9use std::error::Error;
10use std::fmt::{Display, Formatter};
11use std::iter::zip;
12use std::str::Utf8Error;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::RwLock;
15use std::sync::{Arc, Mutex, PoisonError};
16use std::time::Duration;
17
18use crate::images::{convert_image, ImageRect};
19use hidapi::{HidApi, HidDevice, HidError, HidResult};
20use image::{DynamicImage, ImageError};
21
22use crate::info::{is_vendor_familiar, Kind};
23use crate::util::{ajazz03_read_input, mirabox_extend_packet, ajazz153_to_elgato_input, elgato_to_ajazz153, extract_str, inverse_key_index, get_feature_report, read_button_states, read_data, write_data};
24
25/// Various information about Ajazz devices
26pub mod info;
27/// Utility functions for working with Ajazz devices
28pub mod util;
29/// Image processing functions
30pub mod images;
31
32/// Async Ajazz
33#[cfg(feature = "async")]
34#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
35pub mod asynchronous;
36#[cfg(feature = "async")]
37#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
38pub use asynchronous::AsyncAjazz;
39
40/// Creates an instance of the HidApi
41///
42/// Can be used if you don't want to link hidapi crate into your project
43pub fn new_hidapi() -> HidResult<HidApi> {
44    HidApi::new()
45}
46
47/// Actually refreshes the device list
48pub fn refresh_device_list(hidapi: &mut HidApi) -> HidResult<()> {
49    hidapi.refresh_devices()
50}
51
52/// Returns a list of devices as (Kind, Serial Number) that could be found using HidApi.
53///
54/// **WARNING:** To refresh the list, use [refresh_device_list]
55pub fn list_devices(hidapi: &HidApi) -> Vec<(Kind, String)> {
56    hidapi
57        .device_list()
58        .filter_map(|d| {
59            if !is_vendor_familiar(&d.vendor_id()) {
60                return None;
61            }
62
63            if let Some(serial) = d.serial_number() {
64                Some((Kind::from_vid_pid(d.vendor_id(), d.product_id())?, serial.to_string()))
65            } else {
66                None
67            }
68        })
69        .collect::<HashSet<_>>()
70        .into_iter()
71        .collect()
72}
73
74/// Type of input that the device produced
75#[derive(Clone, Debug)]
76pub enum AjazzInput {
77    /// No data was passed from the device
78    NoData,
79
80    /// Button was pressed
81    ButtonStateChange(Vec<bool>),
82
83    /// Encoder/Knob was pressed
84    EncoderStateChange(Vec<bool>),
85
86    /// Encoder/Knob was twisted/turned
87    EncoderTwist(Vec<i8>),
88}
89
90impl AjazzInput {
91    /// Checks if there's data received or not
92    pub fn is_empty(&self) -> bool {
93        matches!(self, AjazzInput::NoData)
94    }
95}
96
97/// Interface for an Ajazz device
98pub struct Ajazz {
99    /// Kind of the device
100    kind: Kind,
101    /// Connected HIDDevice
102    device: HidDevice,
103    /// Temporarily cache the image before sending it to the device
104    image_cache: RwLock<Vec<ImageCache>>,
105    /// Device needs to be initialized
106    initialized: AtomicBool,
107}
108
109struct ImageCache {
110    key: u8,
111    image_data: Vec<u8>,
112}
113
114/// Static functions of the struct
115impl Ajazz {
116    /// Attempts to connect to the device
117    pub fn connect(hidapi: &HidApi, kind: Kind, serial: &str) -> Result<Ajazz, AjazzError> {
118        let device = hidapi.open_serial(kind.vendor_id(), kind.product_id(), serial)?;
119
120        Ok(Ajazz {
121            kind,
122            device,
123            image_cache: RwLock::new(vec![]),
124            initialized: false.into(),
125        })
126    }
127}
128
129/// Instance methods of the struct
130impl Ajazz {
131    /// Returns kind of the Ajazz device
132    pub fn kind(&self) -> Kind {
133        self.kind
134    }
135
136    /// Returns manufacturer string of the device
137    pub fn manufacturer(&self) -> Result<String, AjazzError> {
138        Ok(self.device.get_manufacturer_string()?.unwrap_or_else(|| "Unknown".to_string()))
139    }
140
141    /// Returns product string of the device
142    pub fn product(&self) -> Result<String, AjazzError> {
143        Ok(self.device.get_product_string()?.unwrap_or_else(|| "Unknown".to_string()))
144    }
145
146    /// Returns serial number of the device
147    pub fn serial_number(&self) -> Result<String, AjazzError> {
148        let serial = self.device.get_serial_number_string()?;
149        match serial {
150            Some(serial) => {
151                if serial.is_empty() {
152                    Ok("Unknown".to_string())
153                } else {
154                    Ok(serial)
155                }
156            }
157            None => Ok("Unknown".to_string()),
158        }
159    }
160
161    /// Returns firmware version of the device
162    pub fn firmware_version(&self) -> Result<String, AjazzError> {
163        let bytes = get_feature_report(&self.device, 0x01, 20)?;
164        Ok(extract_str(&bytes[0..])?)
165    }
166
167    /// Initializes the device
168    fn initialize(&self) -> Result<(), AjazzError> {
169        if self.initialized.load(Ordering::Acquire) {
170            return Ok(());
171        }
172
173        self.initialized.store(true, Ordering::Release);
174
175        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x44, 0x49, 0x53];
176        mirabox_extend_packet(&self.kind, &mut buf);
177        write_data(&self.device, buf.as_slice())?;
178
179        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x4c, 0x49, 0x47, 0x00, 0x00, 0x00, 0x00];
180        mirabox_extend_packet(&self.kind, &mut buf);
181        write_data(&self.device, buf.as_slice())?;
182
183        Ok(())
184    }
185
186    /// Reads all possible input from Ajazz device
187    pub fn read_input(&self, timeout: Option<Duration>) -> Result<AjazzInput, AjazzError> {
188        self.initialize()?;
189        match &self.kind {
190            kind if kind.is_ajazz_v1() => {
191                let data = read_data(&self.device, 512, timeout)?;
192
193                if data[0] == 0 {
194                    return Ok(AjazzInput::NoData);
195                }
196
197                let mut states = vec![0x01];
198                states.extend(vec![0u8; (self.kind.key_count() + 1) as usize]);
199
200                if data[9] != 0 {
201                    let key = match self.kind {
202                        Kind::Akp815 => inverse_key_index(&self.kind, data[9] - 1),
203                        Kind::Akp153 | Kind::Akp153E | Kind::Akp153R => ajazz153_to_elgato_input(&self.kind, data[9] - 1),
204                        _ => unimplemented!(),
205                    };
206
207                    states[(key + 1) as usize] = 0x1u8;
208                }
209
210                Ok(AjazzInput::ButtonStateChange(read_button_states(&self.kind, &states)))
211            }
212
213            kind if kind.is_ajazz_v2() => {
214                let data = read_data(&self.device, 512, timeout)?;
215
216                if data[0] == 0 {
217                    return Ok(AjazzInput::NoData);
218                }
219
220                // Devices not returning a state for the input
221                ajazz03_read_input(&self.kind, data[9], 0x01)
222            }
223
224            _ => Err(AjazzError::UnsupportedOperation),
225        }
226    }
227
228    /// Resets the device
229    pub fn reset(&self) -> Result<(), AjazzError> {
230        self.initialize()?;
231        self.set_brightness(100)?;
232        self.clear_all_button_images()?;
233        Ok(())
234    }
235
236    /// Sets brightness of the device, value range is 0 - 100
237    pub fn set_brightness(&self, percent: u8) -> Result<(), AjazzError> {
238        self.initialize()?;
239        let percent = percent.clamp(0, 100);
240
241        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x4c, 0x49, 0x47, 0x00, 0x00, percent];
242
243        mirabox_extend_packet(&self.kind, &mut buf);
244
245        write_data(&self.device, buf.as_slice())?;
246
247        Ok(())
248    }
249
250    fn send_image(&self, key: u8, image_data: &[u8]) -> Result<(), AjazzError> {
251        if key >= self.kind.key_count() {
252            return Err(AjazzError::InvalidKeyIndex);
253        }
254
255        let key = match self.kind {
256            Kind::Akp153 | Kind::Akp153E | Kind::Akp153R => elgato_to_ajazz153(&self.kind, key),
257            Kind::Akp815 => inverse_key_index(&self.kind, key),
258            _ => key,
259        };
260
261        let mut buf = vec![
262            0x00,
263            0x43,
264            0x52,
265            0x54,
266            0x00,
267            0x00,
268            0x42,
269            0x41,
270            0x54,
271            0x00,
272            0x00,
273            (image_data.len() >> 8) as u8,
274            image_data.len() as u8,
275            key + 1,
276        ];
277
278        mirabox_extend_packet(&self.kind, &mut buf);
279
280        write_data(&self.device, buf.as_slice())?;
281
282        self.write_image_data_reports(image_data, WriteImageParameters::for_key(self.kind, image_data.len()), |page_number, this_length, last_package| {
283            vec![0x00]
284        })?;
285        Ok(())
286    }
287
288    /// Writes image data to Ajazz device, changes must be flushed with `.flush()` before
289    /// they will appear on the device!
290    pub fn write_image(&self, key: u8, image_data: &[u8]) -> Result<(), AjazzError> {
291        // Key count is 9 for AKP03x, but only the first 6 (0-5) have screens, so don't output anything for keys 6, 7, 8
292        if matches!(self.kind, Kind::Akp03 | Kind::Akp03E | Kind::Akp03R | Kind::Akp03RRev2) && key >= 6 {
293            return Ok(());
294        }
295
296        let cache_entry = ImageCache {
297            key,
298            image_data: image_data.to_vec(), // Convert &[u8] to Vec<u8>
299        };
300
301        self.image_cache.write()?.push(cache_entry);
302
303        Ok(())
304    }
305
306    /// Sets button's image to blank, changes must be flushed with `.flush()` before
307    /// they will appear on the device!
308    pub fn clear_button_image(&self, key: u8) -> Result<(), AjazzError> {
309        self.initialize()?;
310
311        let key = match self.kind {
312            Kind::Akp815 => inverse_key_index(&self.kind, key),
313            Kind::Akp153 | Kind::Akp153E | Kind::Akp153R => elgato_to_ajazz153(&self.kind, key),
314            _ => key,
315        };
316
317        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4c, 0x45, 0x00, 0x00, 0x00, if key == 0xff { 0xff } else { key + 1 }];
318
319        mirabox_extend_packet(&self.kind, &mut buf);
320
321        write_data(&self.device, buf.as_slice())?;
322
323        Ok(())
324    }
325
326    /// Sets blank images to every button, changes must be flushed with `.flush()` before
327    /// they will appear on the device!
328    pub fn clear_all_button_images(&self) -> Result<(), AjazzError> {
329        self.initialize()?;
330        match self.kind {
331            kind if kind.is_ajazz_v1() => self.clear_button_image(0xff),
332            kind if kind.is_ajazz_v2() => {
333                self.clear_button_image(0xFF)?;
334
335                // Mirabox "v2" requires STP to commit clearing the screen
336                let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x53, 0x54, 0x50];
337                mirabox_extend_packet(&self.kind, &mut buf);
338                write_data(&self.device, buf.as_slice())?;
339
340                Ok(())
341            }
342            _ => {
343                for i in 0..self.kind.key_count() {
344                    self.clear_button_image(i)?
345                }
346                Ok(())
347            }
348        }
349    }
350
351    /// Sets specified button's image, changes must be flushed with `.flush()` before
352    /// they will appear on the device!
353    pub fn set_button_image(&self, key: u8, image: DynamicImage) -> Result<(), AjazzError> {
354        self.initialize()?;
355        let image_data = convert_image(self.kind, image)?;
356        self.write_image(key, &image_data)?;
357        Ok(())
358    }
359
360    /// Set logo image
361    pub fn set_logo_image(&self, image: DynamicImage) -> Result<(), AjazzError> {
362        self.initialize()?;
363
364        if self.kind.lcd_strip_size().is_none() {
365            return Err(AjazzError::UnsupportedOperation);
366        }
367        // 854 * 480 * 3
368        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x4c, 0x4f, 0x47, 0x00, 0x12, 0xc3, 0xc0, 0x01];
369
370        mirabox_extend_packet(&self.kind, &mut buf);
371
372        write_data(&self.device, buf.as_slice())?;
373
374        let mut image_buffer: DynamicImage = DynamicImage::new_rgb8(854, 480);
375
376        let ratio = 854.0 / 480.0;
377
378        let mode = "cover";
379
380        match mode {
381            "contain" => {
382                let (image_w, image_h) = (image.width(), image.height());
383                let image_ratio = image_w as f32 / image_h as f32;
384
385                let (ws, hs) = if image_ratio > ratio {
386                    (854, (854.0 / image_ratio) as u32)
387                } else {
388                    ((480.0 * image_ratio) as u32, 480)
389                };
390
391                let resized_image = image.resize(ws, hs, image::imageops::FilterType::Nearest);
392                image::imageops::overlay(
393                    &mut image_buffer,
394                    &resized_image,
395                    ((854 - resized_image.width()) / 2) as i64,
396                    ((480 - resized_image.height()) / 2) as i64,
397                );
398            }
399            "cover" => {
400                let resized_image = image.resize_to_fill(854, 480, image::imageops::FilterType::Nearest);
401                image::imageops::overlay(
402                    &mut image_buffer,
403                    &resized_image,
404                    ((854 - resized_image.width()) / 2) as i64,
405                    ((480 - resized_image.height()) / 2) as i64,
406                );
407            }
408            _ => {
409                let (image_w, image_h) = (image.width(), image.height());
410                let image_ratio = image_w as f32 / image_h as f32;
411
412                let (ws, hs) = if image_ratio > ratio {
413                    ((480.0 * image_ratio) as u32, 480)
414                } else {
415                    (854, (854.0 / image_ratio) as u32)
416                };
417
418                let resized_image = image.resize(ws, hs, image::imageops::FilterType::Nearest);
419                image::imageops::overlay(
420                    &mut image_buffer,
421                    &resized_image,
422                    ((854 - resized_image.width()) / 2) as i64,
423                    ((480 - resized_image.height()) / 2) as i64,
424                );
425            }
426        }
427
428        let mut image_data = image_buffer.rotate90().fliph().flipv().into_rgb8().to_vec();
429        for x in (0..image_data.len()).step_by(3) {
430            (image_data[x], image_data[x + 2]) = (image_data[x + 2], image_data[x])
431        }
432
433        let image_report_length = match self.kind {
434            kind if kind.is_ajazz_v1() => 513,
435            kind if kind.is_ajazz_v2() => 1025,
436            _ => 1024,
437        };
438
439        let image_report_header_length = 1;
440
441        let image_report_payload_length = image_report_length - image_report_header_length;
442
443        let mut page_number = 0;
444        let mut bytes_remaining = image_data.len();
445
446        while bytes_remaining > 0 {
447            let this_length = bytes_remaining.min(image_report_payload_length);
448            let bytes_sent = page_number * image_report_payload_length;
449
450            // Create buffer with Report ID as first byte
451            let mut buf: Vec<u8> = vec![0x00];
452
453            // Selecting header based on device
454            buf.extend(&image_data[bytes_sent..bytes_sent + this_length]);
455
456            // Adding padding
457            buf.extend(vec![0u8; image_report_length - buf.len()]);
458
459            write_data(&self.device, &buf)?;
460
461            bytes_remaining -= this_length;
462            page_number += 1;
463        }
464
465        Ok(())
466    }
467
468    /// Sleeps the device
469    pub fn sleep(&self) -> Result<(), AjazzError> {
470        self.initialize()?;
471
472        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x48, 0x41, 0x4e];
473
474        mirabox_extend_packet(&self.kind, &mut buf);
475
476        write_data(&self.device, buf.as_slice())?;
477
478        Ok(())
479    }
480
481    /// Make periodic events to the device, to keep it alive
482    pub fn keep_alive(&self) -> Result<(), AjazzError> {
483        self.initialize()?;
484
485        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4F, 0x4E, 0x4E, 0x45, 0x43, 0x54];
486        mirabox_extend_packet(&self.kind, &mut buf);
487        write_data(&self.device, buf.as_slice())?;
488        Ok(())
489    }
490
491    /// Shutdown the device
492    pub fn shutdown(&self) -> Result<(), AjazzError> {
493        self.initialize()?;
494
495        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4c, 0x45, 0x00, 0x00, 0x44, 0x43];
496        mirabox_extend_packet(&self.kind, &mut buf);
497        write_data(&self.device, buf.as_slice())?;
498
499        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x48, 0x41, 0x4E];
500        mirabox_extend_packet(&self.kind, &mut buf);
501        write_data(&self.device, buf.as_slice())?;
502
503        Ok(())
504    }
505
506    /// Flushes the button's image to the device
507    pub fn flush(&self) -> Result<(), AjazzError> {
508        self.initialize()?;
509
510        if self.image_cache.write()?.is_empty() {
511            return Ok(());
512        }
513
514        for image in self.image_cache.read()?.iter() {
515            self.send_image(image.key, &image.image_data)?;
516        }
517
518        let mut buf = vec![0x00, 0x43, 0x52, 0x54, 0x00, 0x00, 0x53, 0x54, 0x50];
519
520        mirabox_extend_packet(&self.kind, &mut buf);
521
522        write_data(&self.device, buf.as_slice())?;
523
524        self.image_cache.write()?.clear();
525
526        Ok(())
527    }
528
529    /// Returns button state reader for this device
530    pub fn get_reader(self: &Arc<Self>) -> Arc<DeviceStateReader> {
531        #[allow(clippy::arc_with_non_send_sync)]
532        Arc::new(DeviceStateReader {
533            device: self.clone(),
534            states: Mutex::new(DeviceState {
535                buttons: vec![false; self.kind.key_count() as usize],
536                encoders: vec![false; self.kind.encoder_count() as usize],
537            }),
538        })
539    }
540
541    fn write_image_data_reports<T>(&self, image_data: &[u8], parameters: WriteImageParameters, header_fn: T) -> Result<(), AjazzError>
542    where
543        T: Fn(usize, usize, bool) -> Vec<u8>,
544    {
545        let image_report_length = parameters.image_report_length;
546        let image_report_payload_length = parameters.image_report_payload_length;
547
548        let mut page_number = 0;
549        let mut bytes_remaining = image_data.len();
550
551        while bytes_remaining > 0 {
552            let this_length = bytes_remaining.min(image_report_payload_length);
553            let bytes_sent = page_number * image_report_payload_length;
554
555            // Selecting header based on device
556            let mut buf: Vec<u8> = header_fn(page_number, this_length, this_length == bytes_remaining);
557
558            buf.extend(&image_data[bytes_sent..bytes_sent + this_length]);
559
560            // Adding padding
561            buf.extend(vec![0u8; image_report_length - buf.len()]);
562
563            write_data(&self.device, &buf)?;
564
565            bytes_remaining -= this_length;
566            page_number += 1;
567        }
568
569        Ok(())
570    }
571}
572
573#[derive(Clone, Copy)]
574struct WriteImageParameters {
575    pub image_report_length: usize,
576    pub image_report_payload_length: usize,
577}
578
579impl WriteImageParameters {
580    pub fn for_key(kind: Kind, image_data_len: usize) -> Self {
581        let image_report_length = match kind {
582            kind if kind.is_ajazz_v1() => 513,
583            kind if kind.is_ajazz_v2() => 1025,
584            _ => 1024,
585        };
586
587        let image_report_header_length = 1;
588
589        let image_report_payload_length = image_report_length - image_report_header_length;
590
591        Self {
592            image_report_length,
593            image_report_payload_length,
594        }
595    }
596}
597
598/// Errors that can occur while working with Ajazz devices
599#[derive(Debug)]
600pub enum AjazzError {
601    /// HidApi error
602    HidError(HidError),
603
604    /// Failed to convert bytes into string
605    Utf8Error(Utf8Error),
606
607    /// Failed to encode image
608    ImageError(ImageError),
609
610    #[cfg(feature = "async")]
611    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
612    /// Tokio join error
613    JoinError(tokio::task::JoinError),
614
615    /// Reader mutex was poisoned
616    PoisonError,
617
618    /// Key index is invalid
619    InvalidKeyIndex,
620
621    /// Unrecognized Product ID
622    UnrecognizedPID,
623
624    /// The device doesn't support doing that
625    UnsupportedOperation,
626
627    /// Device sent unexpected data
628    BadData,
629}
630
631impl Display for AjazzError {
632    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
633        write!(f, "{:?}", self)
634    }
635}
636
637impl Error for AjazzError {}
638
639impl From<HidError> for AjazzError {
640    fn from(e: HidError) -> Self {
641        Self::HidError(e)
642    }
643}
644
645impl From<Utf8Error> for AjazzError {
646    fn from(e: Utf8Error) -> Self {
647        Self::Utf8Error(e)
648    }
649}
650
651impl From<ImageError> for AjazzError {
652    fn from(e: ImageError) -> Self {
653        Self::ImageError(e)
654    }
655}
656
657#[cfg(feature = "async")]
658impl From<tokio::task::JoinError> for AjazzError {
659    fn from(e: tokio::task::JoinError) -> Self {
660        Self::JoinError(e)
661    }
662}
663
664impl<T> From<PoisonError<T>> for AjazzError {
665    fn from(_value: PoisonError<T>) -> Self {
666        Self::PoisonError
667    }
668}
669
670/// Tells what changed in button states
671#[derive(Copy, Clone, Debug, Hash)]
672pub enum DeviceStateUpdate {
673    /// Button got pressed down
674    ButtonDown(u8),
675
676    /// Button got released
677    ButtonUp(u8),
678
679    /// Encoder got pressed down
680    EncoderDown(u8),
681
682    /// Encoder was released from being pressed down
683    EncoderUp(u8),
684
685    /// Encoder was twisted
686    EncoderTwist(u8, i8),
687}
688
689#[derive(Default)]
690struct DeviceState {
691    pub buttons: Vec<bool>,
692    pub encoders: Vec<bool>,
693}
694
695/// Button reader that keeps state of the Ajazz and returns events instead of full states
696pub struct DeviceStateReader {
697    device: Arc<Ajazz>,
698    states: Mutex<DeviceState>,
699}
700
701impl DeviceStateReader {
702    /// Reads states and returns updates
703    pub fn read(&self, timeout: Option<Duration>) -> Result<Vec<DeviceStateUpdate>, AjazzError> {
704        let input = self.device.read_input(timeout)?;
705        let mut my_states = self.states.lock()?;
706
707        let mut updates = vec![];
708
709        match input {
710            AjazzInput::ButtonStateChange(buttons) => {
711                for (index, (their, mine)) in zip(buttons.iter(), my_states.buttons.iter()).enumerate() {
712                    if *their && !*mine {
713                        updates.push(DeviceStateUpdate::ButtonDown(index as u8));
714                    } else if *their && *mine {
715                        updates.push(DeviceStateUpdate::ButtonUp(index as u8));
716                    }
717                }
718
719                my_states.buttons = buttons;
720            }
721
722            AjazzInput::EncoderStateChange(encoders) => {
723                for (index, (their, mine)) in zip(encoders.iter(), my_states.encoders.iter()).enumerate() {
724                    if *their {
725                        updates.push(DeviceStateUpdate::EncoderDown(index as u8));
726                        updates.push(DeviceStateUpdate::EncoderUp(index as u8));
727                    }
728                }
729
730                my_states.encoders = encoders;
731            }
732
733            AjazzInput::EncoderTwist(twist) => {
734                for (index, change) in twist.iter().enumerate() {
735                    if *change != 0 {
736                        updates.push(DeviceStateUpdate::EncoderTwist(index as u8, *change));
737                    }
738                }
739            }
740
741            _ => {}
742        }
743
744        drop(my_states);
745
746        Ok(updates)
747    }
748}