From b91f2bf2aae9268bae94bbb7cedb6288ba5ff40d Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Sat, 20 Jul 2024 01:27:42 +0200 Subject: [PATCH 1/6] Add basic support for Stream Deck Plus model --- src/info.rs | 17 +++++++++++------ src/lib.rs | 36 +++++++++++------------------------- src/main.rs | 16 ---------------- 3 files changed, 22 insertions(+), 47 deletions(-) diff --git a/src/info.rs b/src/info.rs index 6f6a693..759c403 100644 --- a/src/info.rs +++ b/src/info.rs @@ -7,6 +7,7 @@ pub enum Kind { RevisedMini, Xl, Mk2, + Plus } /// Stream Deck key layout direction @@ -54,6 +55,7 @@ impl Kind { Kind::Original | Kind::OriginalV2 | Kind::Mk2 => 15, Kind::Mini | Kind::RevisedMini => 6, Kind::Xl => 32, + Kind::Plus => 8 } } @@ -64,6 +66,7 @@ impl Kind { Kind::OriginalV2 | Kind::Mk2 => 3, Kind::Mini | Kind::RevisedMini => 0, Kind::Xl => 3, + Kind::Plus => 3, } } @@ -86,13 +89,14 @@ impl Kind { Kind::Mini | Kind::RevisedMini => 3, Kind::Original | Kind::OriginalV2 | Kind::Mk2 => 5, Kind::Xl => 8, + Kind::Plus => 4, } } pub fn image_mode(&self) -> ImageMode { match self { Kind::Original | Kind::Mini | Kind::RevisedMini => ImageMode::Bmp, - Kind::OriginalV2 | Kind::Xl | Kind::Mk2 => ImageMode::Jpeg, + Kind::OriginalV2 | Kind::Xl | Kind::Mk2 | Kind::Plus => ImageMode::Jpeg, } } @@ -101,6 +105,7 @@ impl Kind { Kind::Original | Kind::OriginalV2 | Kind::Mk2 => (72, 72), Kind::Mini | Kind::RevisedMini => (80, 80), Kind::Xl => (96, 96), + Kind::Plus => (120, 120), } } @@ -114,7 +119,7 @@ impl Kind { pub fn image_mirror(&self) -> Mirroring { match self { // Mini has rotation, not mirror - Kind::Mini | Kind::RevisedMini => Mirroring::None, + Kind::Mini | Kind::RevisedMini | Kind::Plus => Mirroring::None, // On the original the image is flipped across the Y axis Kind::Original => Mirroring::Y, // On the V2 devices, both X and Y need to flip @@ -137,7 +142,7 @@ impl Kind { pub(crate) fn image_report_header_len(&self) -> usize { match self { Kind::Original | Kind::Mini | Kind::RevisedMini => 16, - Kind::OriginalV2 | Kind::Xl | Kind::Mk2 => 8, + Kind::OriginalV2 | Kind::Xl | Kind::Mk2 | Kind::Plus => 8, } } @@ -147,20 +152,20 @@ impl Kind { Kind::Original => &ORIGINAL_IMAGE_BASE, Kind::Mini | Kind::RevisedMini => &MINI_IMAGE_BASE, - Kind::OriginalV2 | Kind::Xl | Kind::Mk2 => &[], + Kind::OriginalV2 | Kind::Xl | Kind::Mk2 | Kind::Plus => &[], } } pub(crate) fn image_colour_order(&self) -> ColourOrder { match self { Kind::Original | Kind::Mini | Kind::RevisedMini => ColourOrder::BGR, - Kind::OriginalV2 | Kind::Xl | Kind::Mk2 => ColourOrder::RGB, + Kind::OriginalV2 | Kind::Xl | Kind::Mk2 | Kind::Plus => ColourOrder::RGB, } } pub(crate) fn is_v2(&self) -> bool { match self { - Kind::OriginalV2 | Kind::Xl | Kind::Mk2 => true, + Kind::OriginalV2 | Kind::Xl | Kind::Mk2 | Kind::Plus => true, _ => false, } } diff --git a/src/lib.rs b/src/lib.rs index 5427cfc..47a4363 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,7 @@ pub mod pids { pub const XL: u16 = 0x006c; pub const MK2: u16 = 0x0080; pub const REVISED_MINI: u16 = 0x0090; + pub const PLUS: u16 = 0x0084; } impl StreamDeck { @@ -119,7 +120,7 @@ impl StreamDeck { pids::XL => Kind::Xl, pids::MK2 => Kind::Mk2, pids::REVISED_MINI => Kind::RevisedMini, - + pids::PLUS => Kind::Plus, _ => return Err(Error::UnrecognisedPID), }; @@ -213,30 +214,6 @@ impl StreamDeck { Ok(()) } - /// Probe for connected devices. - /// - /// Returns a list of results, - /// each containing the device kind and PID or an error if the PID is unrecognised - pub fn probe() -> Result>, Error> { - let api = HidApi::new()?; - let mut available_devices = vec![]; - for device in api.device_list() { - if device.vendor_id() == 0x0fd9 { - let deck = match device.product_id() { - pids::MK2 => Ok((Kind::Mk2, pids::MK2)), - pids::XL => Ok((Kind::Xl, pids::XL)), - pids::ORIGINAL_V2 => Ok((Kind::OriginalV2, pids::ORIGINAL_V2)), - pids::ORIGINAL => Ok((Kind::Original, pids::ORIGINAL)), - pids::MINI => Ok((Kind::Mini, pids::MINI)), - //pids::PLUS => Ok((Kind::Plus, pids::PLUS)), - _ => Err(Error::UnrecognisedPID) - }; - available_devices.push(deck); - } - } - Ok(available_devices) - } - /// Fetch button states /// /// In blocking mode this will wait until a report packet has been received @@ -258,6 +235,15 @@ impl StreamDeck { return Err(Error::NoData); } + if self.kind == Kind::Plus && cmd[1] != 0 { + // SD Plus specific + // if the second byte is not 0, the touchscreen or dials are being used + // This writes data in indices that are normally used for button data + // This will result in incorrect data being read. + warn!("Touchscreen or dials are not supported in this mode"); + return Ok([0u8; 8].to_vec()); + } + let mut out = vec![0u8; keys]; match self.kind.key_direction() { KeyDirection::RightToLeft => { diff --git a/src/main.rs b/src/main.rs index eacd153..fdc4947 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,8 +32,6 @@ pub enum Commands { Reset, /// Fetch the device firmware version Version, - /// Search for connected streamdecks - Probe, /// Set device display brightness SetBrightness{ /// Brightness value from 0 to 100 @@ -121,20 +119,6 @@ fn do_command(deck: &mut StreamDeck, cmd: Commands) -> Result<(), Error> { } } }, - Commands::Probe => { - let results = StreamDeck::probe()?; - if results.is_empty() { - info!("No devices found"); - return Ok(()); - } - info!("Found {} devices", results.len()); - for res in results { - match res { - Ok((device, pid)) => info!("Streamdeck: {:?} (pid: {:#x})", device, pid), - Err(_) => warn!("Found Elgato device with unsupported PID"), - } - } - } Commands::SetColour{key, colour} => { info!("Setting key {} colour to: ({:?})", key, colour); deck.set_button_rgb(key, &colour)?; From 3f205cf9da00ac3e07b2378ab258c8e70cbf7454 Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Sat, 20 Jul 2024 01:45:59 +0200 Subject: [PATCH 2/6] micro optimization in get-buttons method --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 47a4363..250e169 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,16 +235,17 @@ impl StreamDeck { return Err(Error::NoData); } + let mut out = vec![0u8; keys]; + if self.kind == Kind::Plus && cmd[1] != 0 { // SD Plus specific // if the second byte is not 0, the touchscreen or dials are being used // This writes data in indices that are normally used for button data // This will result in incorrect data being read. warn!("Touchscreen or dials are not supported in this mode"); - return Ok([0u8; 8].to_vec()); + return Ok(out); } - let mut out = vec![0u8; keys]; match self.kind.key_direction() { KeyDirection::RightToLeft => { for (i, val) in out.iter_mut().enumerate() { From 86ba659ab55742c086220011fb11dd7a5937454d Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Tue, 23 Jul 2024 11:30:35 +0200 Subject: [PATCH 3/6] added input manager --- src/input.rs | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 src/input.rs diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..e128bcc --- /dev/null +++ b/src/input.rs @@ -0,0 +1,232 @@ +use std::{collections::HashSet, time::Duration, vec}; + +use crate::{Kind, StreamDeck}; + +/// The InputEvent enum represents the different types of input events that can be generated by Streamdeck devices. +/// Most streamdeck devices only have buttons, the Streamdeck Plus also has dials and a touchscreen. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum InputEvent { + Button { + index: u8, + released: bool, + }, + Dial { + index: u8, + action: DialAction + }, + Touch { + x: u16, + y: u16, + action: TouchAction, + }, +} + +///Different types of touch events that can be generated by streamdecks with touchscreens +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum TouchAction { + ///A short touch event + Short, + ///A long touch event + Long, + ///A drag event, x and y are the end coordinates of the drag + Drag { + x: u16, + y: u16, + }, +} + +///Different types of dial events that can be generated by streamdecks with dials +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum DialAction { + ///The dial was pressed + Pressed, + ///The dial was released + Released, + ///The dial was turned, the i8 is the delta of the turn. + ///Negative values are counter-clockwise, positive values are clockwise + Turned(i8), +} + +///Different types of dial events that can be generated by streamdecks with dials +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) struct TouchDataIndices { + pub event_type_index: usize, + pub x_low: usize, + pub x_high: usize, + pub y_low: usize, + pub y_high: usize, + pub drag_x_low: usize, + pub drag_x_high: usize, + pub drag_y: usize, + +} + +///Manages inputs for the Streamdeck device. Keeps track of pressed keys and dials and touchscreens and generates InputEvents +pub struct InputManager<'a> { + deck: &'a mut StreamDeck, + pressed_keys: HashSet, + pressed_dials: HashSet, +} + +impl <'a> InputManager<'a> { + pub fn new(deck: &'a mut StreamDeck) -> Self { + InputManager { + deck, + pressed_keys: HashSet::new(), + pressed_dials: HashSet::new(), + } + } + + ///Handles input events for the Streamdeck device and returns a Vec of InputEvents + pub fn handle_input( + &mut self, + timeout: Option + ) -> Result, crate::Error> { + let cmd = self.deck.read_input(timeout)?; + let kind = self.deck.kind; + //SD Plus has Dials and Touchscreen, other models only have buttons + if kind == Kind::Plus { + return Ok(match cmd[1] { + 0 => self.handle_button_event(&cmd, &kind), + 2 => self.handle_touchscreen_event(&cmd, &kind)?, + 3 => self.handle_dial_event(&cmd, &kind), + _ => return Err(crate::Error::InvalidInputTypeIndex), + }); + } + Ok(self.handle_button_event(&cmd, &kind)) + } + + ///Handles touchscreen events (short touch, long touch, drag) and returns a Vec of InputEvents + fn handle_touchscreen_event(&self, cmd: &[u8; 36], kind: &crate::Kind) -> Result, crate::Error> { + let indices = kind.touch_data_indices(); + + if indices.is_none() { + return Err(crate::Error::InvalidTouchscreenIndex); + } + let indices = indices.unwrap(); + /* + * Indices are hardcoded for now, as the SD+ is the only one with a touchscreen. + * TODO: create a new fn in Kind struct to return the relevant indices for the current device + * if more Streamdeck models with touchscreens are released. + */ + let action = match cmd[indices.event_type_index] { + 1 => TouchAction::Short, + 2 => TouchAction::Long, + 3 => TouchAction::Drag{ + x: ((cmd[indices.drag_x_high] as u16) << 8) + cmd[indices.drag_x_low] as u16, + y: cmd[indices.drag_y] as u16, + }, + _ => return Err(crate::Error::InvalidTouchType) + }; + + Ok(vec![InputEvent::Touch { + action, + x: ((cmd[indices.x_high] as u16) << 8) + cmd[indices.x_low] as u16, + y: ((cmd[indices.y_high] as u16) << 8) + cmd[indices.y_low] as u16, + }]) + } + + ///Handles dial events (press, release, turn) and returns a Vec of InputEvents + fn handle_dial_event(&mut self, cmd: &[u8; 36], kind: &crate::Kind) -> Vec { + let offset = kind.dial_data_offset(); + let dials = kind.dials() as usize; + let press = cmd[kind.dial_press_flag_index()] == 0; + let mut events = Vec::new(); + + if !press { + for i in offset..offset + dials { + if cmd[i] == 0 { + continue; + } + let delta: i8; + if cmd[(i) as usize] > 127 { + //convert to i8 and invert. subtract 1 to make it 0-based + delta = -((255 - cmd[(i) as usize]) as i8) -1; + } + else { + delta = cmd[(i) as usize] as i8; + } + events.push(InputEvent::Dial { + index: (i - offset) as u8, + action: DialAction::Turned(delta), + }); + } + return events; + } + + let mut fresh_presses = HashSet::new(); + for i in offset..offset + dials { + if cmd[i] == 1 { + let dial = i - offset; + if self.pressed_dials.contains(&dial) { + continue; + } + fresh_presses.insert(dial); + events.push(InputEvent::Dial { + index: dial as u8, + action: DialAction::Pressed, + }); + } + } + + self.pressed_dials.retain(|dial| { + if cmd[(offset + *dial) as usize] == 0 && !fresh_presses.contains(dial) { + events.push(InputEvent::Dial { + index: *dial as u8, + action: DialAction::Released, + }); + return false; + } + true + }); + + self.pressed_dials.extend(fresh_presses); + events + } + + ///Handles button events (press, release) and returns a Vec of InputEvents + fn handle_button_event(&mut self, cmd: &[u8; 36], kind: &crate::Kind) -> Vec { + let mut fresh_presses = HashSet::new(); + let mut events = Vec::new(); + let keys = kind.keys() as usize; + let offset = kind.key_data_offset() + 1; + + for i in offset..offset + keys { + + if cmd[i] == 0 { + continue; + } + let button = (i - offset) as u8; + // If the button was already reported as pressed, skip it + if self.pressed_keys.contains(&button) { + continue; + } + // If the button press is fresh, add it to the fresh_presses HashSet and the events Vec + fresh_presses.insert(button); + events.push(InputEvent::Button { + index: button, + released: false, + }); + } + + // Remove released buttons from the pressed_keys HashSet and add them to the events Vec as released + self.pressed_keys.retain(|button| { + if cmd[offset + *button as usize] == 0 && !fresh_presses.contains(button) { + events.push(InputEvent::Button { + index: *button, + released: true, + }); + return false; + } + true + }); + + // Add the fresh_presses HashSet to the pressed_keys HashSet + self.pressed_keys.extend(fresh_presses); + events + } +} \ No newline at end of file From 811c536149ec5462f082355a8263175ea1fdb24f Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Sat, 21 Sep 2024 20:20:50 +0200 Subject: [PATCH 4/6] implemented Input Manager --- src/info.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++ src/input.rs | 61 ++++++++++++++++++++++++++++------------------------ src/lib.rs | 22 ++++++++++++++++++- src/main.rs | 40 ++++++++++++++++++++++++++++++++-- 4 files changed, 146 insertions(+), 31 deletions(-) diff --git a/src/info.rs b/src/info.rs index 328f1c9..a4a7972 100644 --- a/src/info.rs +++ b/src/info.rs @@ -50,7 +50,22 @@ pub enum Mirroring { Both, } +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) struct TouchDataIndices { + pub event_type_index: usize, + pub x_low: usize, + pub x_high: usize, + pub y_low: usize, + pub y_high: usize, + pub drag_x_low: usize, + pub drag_x_high: usize, + pub drag_y: usize, +} + impl Kind { + ///Different types of dial events that can be generated by streamdecks with dials + pub fn keys(&self) -> u8 { match self { Kind::Original | Kind::OriginalV2 | Kind::Mk2 => 15, @@ -94,6 +109,28 @@ impl Kind { } } + + pub(crate) fn dials(&self) -> u8 { + match self { + Kind::Plus => 4, + _ => 0, + } + } + + pub(crate) fn dial_data_offset(&self) -> usize { + match self { + Kind::Plus => 5, + _ => 0, + } + } + + pub(crate) fn dial_press_flag_index(&self) -> usize { + match self { + Kind::Plus => 4, + _ => 0, + } + } + pub fn image_mode(&self) -> ImageMode { match self { Kind::Original | Kind::Mini | Kind::RevisedMini => ImageMode::Bmp, @@ -170,6 +207,23 @@ impl Kind { _ => false, } } + + pub(crate) fn touch_data_indices(&self) -> Option { + match self { + Kind::Plus => Some(TouchDataIndices { + event_type_index: 4, + x_low: 6, + x_high: 7, + y_low: 8, + y_high: 9, //Irrelevant for SD Plus, as the touch area is only 100px high, but here for future proofing + drag_x_low: 10, + drag_x_high: 11, + drag_y: 12, + }), + _ => None, + } + } + } pub const ORIGINAL_IMAGE_BASE: [u8; 54] = [ diff --git a/src/input.rs b/src/input.rs index e128bcc..55811d7 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,20 +1,25 @@ use std::{collections::HashSet, time::Duration, vec}; -use crate::{Kind, StreamDeck}; +use crate::{KeyDirection, Kind, StreamDeck}; + + /// The InputEvent enum represents the different types of input events that can be generated by Streamdeck devices. /// Most streamdeck devices only have buttons, the Streamdeck Plus also has dials and a touchscreen. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum InputEvent { + ///Button event, index is the zero-based index of the button, released is true if the button was released Button { index: u8, - released: bool, + action: ButtonAction, }, + ///Dial event, index is the zero-based index of the dial, action is the performed DialAction Dial { index: u8, action: DialAction }, + ///Touch event, x and y are the coordinates of the touch event on the touchscreen, action is the performed TouchAction Touch { x: u16, y: u16, @@ -22,6 +27,14 @@ pub enum InputEvent { }, } +///Different types of button events that can be generated by streamdeck devices +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ButtonAction { + Pressed, + Released, +} + ///Different types of touch events that can be generated by streamdecks with touchscreens #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -45,26 +58,11 @@ pub enum DialAction { Pressed, ///The dial was released Released, - ///The dial was turned, the i8 is the delta of the turn. + ///The dial was turned, the value is the delta of the turn. ///Negative values are counter-clockwise, positive values are clockwise Turned(i8), } -///Different types of dial events that can be generated by streamdecks with dials -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub(crate) struct TouchDataIndices { - pub event_type_index: usize, - pub x_low: usize, - pub x_high: usize, - pub y_low: usize, - pub y_high: usize, - pub drag_x_low: usize, - pub drag_x_high: usize, - pub drag_y: usize, - -} - ///Manages inputs for the Streamdeck device. Keeps track of pressed keys and dials and touchscreens and generates InputEvents pub struct InputManager<'a> { deck: &'a mut StreamDeck, @@ -94,18 +92,18 @@ impl <'a> InputManager<'a> { 0 => self.handle_button_event(&cmd, &kind), 2 => self.handle_touchscreen_event(&cmd, &kind)?, 3 => self.handle_dial_event(&cmd, &kind), - _ => return Err(crate::Error::InvalidInputTypeIndex), + _ => return Err(crate::Error::UnsupportedInput), }); } Ok(self.handle_button_event(&cmd, &kind)) } ///Handles touchscreen events (short touch, long touch, drag) and returns a Vec of InputEvents - fn handle_touchscreen_event(&self, cmd: &[u8; 36], kind: &crate::Kind) -> Result, crate::Error> { + fn handle_touchscreen_event(&self, cmd: &[u8; 36], kind: &Kind) -> Result, crate::Error> { let indices = kind.touch_data_indices(); if indices.is_none() { - return Err(crate::Error::InvalidTouchscreenIndex); + return Err(crate::Error::UnsupportedInput); } let indices = indices.unwrap(); /* @@ -120,7 +118,7 @@ impl <'a> InputManager<'a> { x: ((cmd[indices.drag_x_high] as u16) << 8) + cmd[indices.drag_x_low] as u16, y: cmd[indices.drag_y] as u16, }, - _ => return Err(crate::Error::InvalidTouchType) + _ => return Err(crate::Error::UnsupportedInput) }; Ok(vec![InputEvent::Touch { @@ -131,7 +129,7 @@ impl <'a> InputManager<'a> { } ///Handles dial events (press, release, turn) and returns a Vec of InputEvents - fn handle_dial_event(&mut self, cmd: &[u8; 36], kind: &crate::Kind) -> Vec { + fn handle_dial_event(&mut self, cmd: &[u8; 36], kind: &Kind) -> Vec { let offset = kind.dial_data_offset(); let dials = kind.dials() as usize; let press = cmd[kind.dial_press_flag_index()] == 0; @@ -189,27 +187,34 @@ impl <'a> InputManager<'a> { } ///Handles button events (press, release) and returns a Vec of InputEvents - fn handle_button_event(&mut self, cmd: &[u8; 36], kind: &crate::Kind) -> Vec { + fn handle_button_event(&mut self, cmd: &[u8; 36], kind: &Kind) -> Vec { let mut fresh_presses = HashSet::new(); let mut events = Vec::new(); let keys = kind.keys() as usize; - let offset = kind.key_data_offset() + 1; + let offset = kind.key_data_offset(); for i in offset..offset + keys { if cmd[i] == 0 { continue; } - let button = (i - offset) as u8; + + let button = match self.deck.kind.key_direction() { + KeyDirection::RightToLeft => keys as u8 - (i - offset) as u8, + KeyDirection::LeftToRight => i as u8 + self.deck.kind.key_index_offset(), + }; + // If the button was already reported as pressed, skip it if self.pressed_keys.contains(&button) { continue; } + + // If the button press is fresh, add it to the fresh_presses HashSet and the events Vec fresh_presses.insert(button); events.push(InputEvent::Button { index: button, - released: false, + action: ButtonAction::Pressed, }); } @@ -218,7 +223,7 @@ impl <'a> InputManager<'a> { if cmd[offset + *button as usize] == 0 && !fresh_presses.contains(button) { events.push(InputEvent::Button { index: *button, - released: true, + action: ButtonAction::Released, }); return false; } diff --git a/src/lib.rs b/src/lib.rs index 17bbf25..3572aea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,10 @@ pub use crate::images::{Colour, ImageOptions}; pub mod info; pub use info::*; +pub use info::Kind; + +pub mod input; +pub use input::*; use imageproc::drawing::draw_text_mut; use std::str::FromStr; @@ -24,7 +28,7 @@ use thiserror::Error; /// StreamDeck object pub struct StreamDeck { - kind: Kind, + pub kind: Kind, device: HidDevice, } @@ -241,6 +245,22 @@ impl StreamDeck { Ok(available_devices) } + /// Read input from the device + /// + /// This is a raw read of the device input and is not recommended for general use. + pub fn read_input(&mut self, timeout: Option) -> Result<[u8; 36], Error> { + let mut cmd = [0u8; 36]; + + match timeout { + Some(t) => self + .device + .read_timeout(&mut cmd, t.as_millis() as i32)?, + None => self.device.read(&mut cmd)?, + }; + + Ok(cmd) + } + /// Fetch button states /// /// In blocking mode this will wait until a report packet has been received diff --git a/src/main.rs b/src/main.rs index fdc4947..d8ad27f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,9 @@ use structopt::StructOpt; extern crate humantime; use humantime::Duration; -use streamdeck::{StreamDeck, Filter, Colour, ImageOptions, Error}; +use streamdeck::{info, Colour, Error, Filter, ImageOptions, InputEvent, InputManager, Kind, StreamDeck}; + + #[derive(StructOpt)] #[structopt(name = "streamdeck-cli", about = "A CLI for the Elgato StreamDeck")] @@ -47,6 +49,16 @@ pub enum Commands { /// Read continuously continuous: bool, }, + /// Fetch input events + GetInput { + #[structopt(long)] + /// Timeout for input reading + timeout: Option, + + #[structopt(long)] + /// Read continuously + continuous: bool, + }, /// Set button colours SetColour { /// Index of button to be set @@ -65,7 +77,8 @@ pub enum Commands { #[structopt(flatten)] opts: ImageOptions, - } + }, + Probe, } fn main() { @@ -119,6 +132,29 @@ fn do_command(deck: &mut StreamDeck, cmd: Commands) -> Result<(), Error> { } } }, + Commands::GetInput { + timeout, + continuous, + } => { + let mut manager = InputManager::new(deck); + loop { + let input = manager.handle_input(timeout.map(|t| *t))?; + info!("input: {:?}", input); + + if !continuous { + break; + } + } + }, + Commands::Probe => { + let results = StreamDeck::probe()?; + for result in results { + match result { + Ok(deck) => info!("Found device: {:?} (pid: {:#X})", deck.0, deck.1), + Err(e) => error!("Error probing device: {:?}", e), + } + } + }, Commands::SetColour{key, colour} => { info!("Setting key {} colour to: ({:?})", key, colour); deck.set_button_rgb(key, &colour)?; From 969b4cc3873d988f6db98b2c60c8b9918e537f68 Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Sun, 22 Sep 2024 16:30:03 +0200 Subject: [PATCH 5/6] Handle RightToLeft in input manager & small cleanups --- src/input.rs | 17 ++++++++--------- src/lib.rs | 13 ++----------- src/main.rs | 2 +- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/input.rs b/src/input.rs index 55811d7..4756a82 100644 --- a/src/input.rs +++ b/src/input.rs @@ -89,13 +89,14 @@ impl <'a> InputManager<'a> { //SD Plus has Dials and Touchscreen, other models only have buttons if kind == Kind::Plus { return Ok(match cmd[1] { - 0 => self.handle_button_event(&cmd, &kind), + 0 => self.handle_button_event(&cmd, &kind)?, 2 => self.handle_touchscreen_event(&cmd, &kind)?, 3 => self.handle_dial_event(&cmd, &kind), _ => return Err(crate::Error::UnsupportedInput), }); } - Ok(self.handle_button_event(&cmd, &kind)) + + Ok(self.handle_button_event(&cmd, &kind)?) } ///Handles touchscreen events (short touch, long touch, drag) and returns a Vec of InputEvents @@ -187,21 +188,20 @@ impl <'a> InputManager<'a> { } ///Handles button events (press, release) and returns a Vec of InputEvents - fn handle_button_event(&mut self, cmd: &[u8; 36], kind: &Kind) -> Vec { + fn handle_button_event(&mut self, cmd: &[u8; 36], kind: &Kind) -> Result, crate::Error> { let mut fresh_presses = HashSet::new(); let mut events = Vec::new(); let keys = kind.keys() as usize; let offset = kind.key_data_offset(); - - for i in offset..offset + keys { + for i in 1 + offset..offset + keys + 1 { if cmd[i] == 0 { continue; } let button = match self.deck.kind.key_direction() { - KeyDirection::RightToLeft => keys as u8 - (i - offset) as u8, - KeyDirection::LeftToRight => i as u8 + self.deck.kind.key_index_offset(), + KeyDirection::RightToLeft => self.deck.translate_key_index((i - offset) as u8)? - 1, + KeyDirection::LeftToRight => (i - offset - 1) as u8, }; // If the button was already reported as pressed, skip it @@ -209,7 +209,6 @@ impl <'a> InputManager<'a> { continue; } - // If the button press is fresh, add it to the fresh_presses HashSet and the events Vec fresh_presses.insert(button); events.push(InputEvent::Button { @@ -232,6 +231,6 @@ impl <'a> InputManager<'a> { // Add the fresh_presses HashSet to the pressed_keys HashSet self.pressed_keys.extend(fresh_presses); - events + Ok(events) } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 3572aea..8fe14c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -283,8 +283,8 @@ impl StreamDeck { } if self.kind == Kind::Plus { - //If the second byte is not 0, a dial or the touchscreen was used, we don't support that here - //This would write to indices which represent buttons and thus create faulty output + //If the second byte on SD Plus is not 0, a dial or the touchscreen was used, we don't support that here + //This would write to indices which represent buttons here and thus create faulty output if cmd[1] != 0 { return Err(Error::UnsupportedInput); } @@ -292,15 +292,6 @@ impl StreamDeck { let mut out = vec![0u8; keys]; - if self.kind == Kind::Plus && cmd[1] != 0 { - // SD Plus specific - // if the second byte is not 0, the touchscreen or dials are being used - // This writes data in indices that are normally used for button data - // This will result in incorrect data being read. - warn!("Touchscreen or dials are not supported in this mode"); - return Ok(out); - } - match self.kind.key_direction() { KeyDirection::RightToLeft => { for (i, val) in out.iter_mut().enumerate() { diff --git a/src/main.rs b/src/main.rs index d8ad27f..15b5b23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use structopt::StructOpt; extern crate humantime; use humantime::Duration; -use streamdeck::{info, Colour, Error, Filter, ImageOptions, InputEvent, InputManager, Kind, StreamDeck}; +pub use streamdeck::{info, Colour, Error, Filter, ImageOptions, InputEvent, InputManager, Kind, StreamDeck}; From 1d83c920a193a625b7332670492c39ec3c422b29 Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Sun, 22 Sep 2024 16:31:05 +0200 Subject: [PATCH 6/6] Fix import warning --- src/images.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/images.rs b/src/images.rs index 7f72cd6..bce663c 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,9 +1,8 @@ use std::str::FromStr; use image::codecs::jpeg::JpegEncoder; -use image::io::Reader; use image::{imageops::FilterType, Pixel, Rgba}; -use image::{DynamicImage, ExtendedColorType}; +use image::{DynamicImage, ExtendedColorType, ImageReader}; use crate::info::{ColourOrder, Mirroring, Rotation}; use crate::{rgb_to_bgr, Error}; @@ -102,7 +101,7 @@ pub(crate) fn load_image( colour_order: ColourOrder, ) -> Result, Error> { // Open image reader - let reader = match Reader::open(path) { + let reader = match ImageReader::open(path) { Ok(v) => v, Err(e) => { error!("error loading file '{}': {:?}", path, e);