From b91f2bf2aae9268bae94bbb7cedb6288ba5ff40d Mon Sep 17 00:00:00 2001 From: Johannes Franzen Date: Sat, 20 Jul 2024 01:27:42 +0200 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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); From 0f6995ceb3382b727bcfaefcd4422b8fcbc18633 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 25 May 2026 14:35:05 +0200 Subject: [PATCH 07/12] Add linux udev rule for new USB device with id 0084 (Plus) --- 40-streamdeck.rules | 1 + 1 file changed, 1 insertion(+) diff --git a/40-streamdeck.rules b/40-streamdeck.rules index 7b94676..3b057fe 100644 --- a/40-streamdeck.rules +++ b/40-streamdeck.rules @@ -3,3 +3,4 @@ SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="0063", MODE="0660", SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="006c", MODE="0660", GROUP="plugdev" SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="006d", MODE="0660", GROUP="plugdev" SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="0090", MODE="0660", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="0fd9", ATTR{idProduct}=="0084", MODE="0660", GROUP="plugdev" \ No newline at end of file From 5cd32fb96922e1df44826182c8c1a82cc818580d Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 25 May 2026 14:45:09 +0200 Subject: [PATCH 08/12] Refactor InputManager to own StreamDeck instance and update do_command to take ownership of the passed in deck --- src/input.rs | 8 ++++---- src/main.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/input.rs b/src/input.rs index 4756a82..07c1172 100644 --- a/src/input.rs +++ b/src/input.rs @@ -64,14 +64,14 @@ pub enum DialAction { } ///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, +pub struct InputManager { + deck: StreamDeck, pressed_keys: HashSet, pressed_dials: HashSet, } -impl <'a> InputManager<'a> { - pub fn new(deck: &'a mut StreamDeck) -> Self { +impl InputManager { + pub fn new(deck: StreamDeck) -> Self { InputManager { deck, pressed_keys: HashSet::new(), diff --git a/src/main.rs b/src/main.rs index 15b5b23..1c56477 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,12 +105,12 @@ fn main() { opts.filter.vid, opts.filter.pid, serial); // Run the command - if let Err(e) = do_command(&mut deck, opts.cmd) { + if let Err(e) = do_command(deck, opts.cmd) { error!("Command error: {:?}", e); } } -fn do_command(deck: &mut StreamDeck, cmd: Commands) -> Result<(), Error> { +fn do_command(mut deck: StreamDeck, cmd: Commands) -> Result<(), Error> { match cmd { Commands::Reset => { deck.reset()?; From 3be5ccec05148b789bd376519f242df949e42dd8 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 25 May 2026 15:00:11 +0200 Subject: [PATCH 09/12] Fix button release detection by adjusting command offset in InputManager --- src/input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input.rs b/src/input.rs index 07c1172..2dd294d 100644 --- a/src/input.rs +++ b/src/input.rs @@ -219,7 +219,7 @@ impl InputManager { // 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) { + if cmd[offset + 1 + *button as usize] == 0 && !fresh_presses.contains(button) { events.push(InputEvent::Button { index: *button, action: ButtonAction::Released, From 46495c8c6db202022fac8d9176cdc7d14dc2a074 Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 25 May 2026 15:12:08 +0200 Subject: [PATCH 10/12] Gate input manager and all related logic behind feature flag --- Cargo.toml | 1 + src/info.rs | 8 ++++++-- src/input.rs | 4 ++++ src/lib.rs | 17 +++++++++-------- src/main.rs | 6 +++++- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 55bf6c8..49e0f70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" [features] util = [ "structopt", "simplelog", "humantime" ] +input-manager = [] default = [ "util" ] [dependencies] diff --git a/src/info.rs b/src/info.rs index 2835064..828dd4f 100644 --- a/src/info.rs +++ b/src/info.rs @@ -55,6 +55,7 @@ pub enum Mirroring { #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg(feature = "input-manager")] pub(crate) struct TouchDataIndices { pub event_type_index: usize, pub x_low: usize, @@ -118,7 +119,7 @@ impl Kind { } } - + #[cfg(feature = "input-manager")] pub(crate) fn dials(&self) -> u8 { match self { Kind::Plus => 4, @@ -126,6 +127,7 @@ impl Kind { } } + #[cfg(feature = "input-manager")] pub(crate) fn dial_data_offset(&self) -> usize { match self { Kind::Plus => 5, @@ -133,6 +135,7 @@ impl Kind { } } + #[cfg(feature = "input-manager")] pub(crate) fn dial_press_flag_index(&self) -> usize { match self { Kind::Plus => 4, @@ -217,7 +220,8 @@ impl Kind { _ => false, } } - + + #[cfg(feature = "input-manager")] pub(crate) fn touch_data_indices(&self) -> Option { match self { Kind::Plus => Some(TouchDataIndices { diff --git a/src/input.rs b/src/input.rs index 2dd294d..eba849a 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,6 +8,7 @@ use crate::{KeyDirection, Kind, StreamDeck}; /// 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))] +#[serde(rename_all = "camelCase")] pub enum InputEvent { ///Button event, index is the zero-based index of the button, released is true if the button was released Button { @@ -30,6 +31,7 @@ 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))] +#[serde(rename_all = "camelCase")] pub enum ButtonAction { Pressed, Released, @@ -38,6 +40,7 @@ pub enum ButtonAction { ///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))] +#[serde(rename_all = "camelCase")] pub enum TouchAction { ///A short touch event Short, @@ -53,6 +56,7 @@ pub enum TouchAction { ///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))] +#[serde(rename_all = "camelCase")] pub enum DialAction { ///The dial was pressed Pressed, diff --git a/src/lib.rs b/src/lib.rs index 50b74ca..a7734bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,9 @@ pub mod info; pub use info::*; pub use info::Kind; +#[cfg(feature = "input-manager")] pub mod input; +#[cfg(feature = "input-manager")] pub use input::*; use imageproc::drawing::draw_text_mut; @@ -298,7 +300,8 @@ impl StreamDeck { /// Read input from the device /// - /// This is a raw read of the device input and is not recommended for general use. + /// This is a raw read of the device input intended for the input managerand is not recommended for general use. + #[cfg(feature = "input-manager")] pub fn read_input(&mut self, timeout: Option) -> Result<[u8; 36], Error> { let mut cmd = [0u8; 36]; @@ -333,14 +336,12 @@ impl StreamDeck { return Err(Error::NoData); } - if self.kind == Kind::Plus { - //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); - } + // 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 self.kind == Kind::Plus && cmd[1] != 0 { + return Err(Error::UnsupportedInput); } - + let mut out = vec![0u8; keys]; match self.kind.key_direction() { diff --git a/src/main.rs b/src/main.rs index 1c56477..5a61563 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,9 @@ use structopt::StructOpt; extern crate humantime; use humantime::Duration; -pub use streamdeck::{info, Colour, Error, Filter, ImageOptions, InputEvent, InputManager, Kind, StreamDeck}; +pub use streamdeck::{info, Colour, Error, Filter, ImageOptions, Kind, StreamDeck}; +#[cfg(feature = "input-manager")] +pub use streamdeck::{InputEvent, InputManager}; @@ -49,6 +51,7 @@ pub enum Commands { /// Read continuously continuous: bool, }, + #[cfg(feature = "input-manager")] /// Fetch input events GetInput { #[structopt(long)] @@ -132,6 +135,7 @@ fn do_command(mut deck: StreamDeck, cmd: Commands) -> Result<(), Error> { } } }, + #[cfg(feature = "input-manager")] Commands::GetInput { timeout, continuous, From c7ec9a16dce2b5111a7773c3e5a17ef488a46d8e Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 25 May 2026 15:29:25 +0200 Subject: [PATCH 11/12] Update README.md to include Stream Deck Plus support and usage instructions for InputManager --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c5cd394..b2f6ce7 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,11 @@ Features: - [x] Stream Deck Original (untested) - [x] Stream Deck Original V2 - [x] Stream Deck XL + - [x] Stream Deck Plus (dials and touchscreen require the `input-manager` feature; setting the touchscreen image is not yet supported) - [x] Stream Deck Module 6Keys - [x] Stream Deck Module 15Keys - [x] Stream Deck Module 32Keys (untested) - +- [x] Optional `input-manager` as a higher level wrapper for translating raw HID reports into high-level input events. Needs to be enabled using the `input-manager` feature flag. ## Getting started @@ -81,6 +82,56 @@ SUBCOMMANDS: ``` + +### Using the input manager + +The `InputManager` provides a stateful, high-level interface for reading input from Stream Deck devices. Instead of interpreting raw HID reports yourself, it tracks button, dial, and touchscreen state and emits discrete `InputEvent` values with press/release semantics. + +Enable it via the `input-manager` feature (on by default): + +```toml +streamdeck = { version = "0.10", features = ["input-manager"] } +``` + +The `InputManager` takes ownership of the `StreamDeck` instance: + +```rust +use streamdeck::{StreamDeck, InputManager}; + +let vendor_id = 0x0fd9; // Elgato +let product_id = 0x0084; // Stream Deck Plus + +let deck = StreamDeck::connect(vendor_id, product_id, None)?; +let mut manager = InputManager::new(deck); + +loop { + let events = manager.handle_input(None)?; + for event in events { + match event { + InputEvent::Button { index, action } => { + println!("Button {index}: {action:?}"); + } + InputEvent::Dial { index, action } => { + // Stream Deck Plus only + println!("Dial {index}: {action:?}"); + } + InputEvent::Touch { x, y, action } => { + // Stream Deck Plus only + println!("Touch at ({x}, {y}): {action:?}"); + } + } + } +} +``` + +`handle_input` accepts an optional `Duration` timeout. It returns a `Vec` which can contain any combination of: + +- **`InputEvent::Button`** — a button was pressed or released (all devices) +- **`InputEvent::Dial`** — a dial was pressed, released, or turned with a signed delta (Stream Deck Plus only) +- **`InputEvent::Touch`** — a short tap, long press, or drag on the touchscreen (Stream Deck Plus only) + +The manager internally tracks which buttons and dials are currently held down, so each physical press and release produces exactly one event. + ## Related Works This library stands on the shoulders of giants (who had already done all the reversing work)... From f1f8bfe822c9b7d39f043417469fa08ec90ed73b Mon Sep 17 00:00:00 2001 From: Johannes Date: Mon, 25 May 2026 15:45:21 +0200 Subject: [PATCH 12/12] Remove faulty attribute --- src/input.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/input.rs b/src/input.rs index eba849a..2dd294d 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,7 +8,6 @@ use crate::{KeyDirection, Kind, StreamDeck}; /// 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))] -#[serde(rename_all = "camelCase")] pub enum InputEvent { ///Button event, index is the zero-based index of the button, released is true if the button was released Button { @@ -31,7 +30,6 @@ 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))] -#[serde(rename_all = "camelCase")] pub enum ButtonAction { Pressed, Released, @@ -40,7 +38,6 @@ pub enum ButtonAction { ///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))] -#[serde(rename_all = "camelCase")] pub enum TouchAction { ///A short touch event Short, @@ -56,7 +53,6 @@ pub enum TouchAction { ///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))] -#[serde(rename_all = "camelCase")] pub enum DialAction { ///The dial was pressed Pressed,