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); diff --git a/src/info.rs b/src/info.rs index 4587b8a..a4a7972 100644 --- a/src/info.rs +++ b/src/info.rs @@ -7,9 +7,10 @@ pub enum Kind { RevisedMini, Xl, Mk2, - Plus, + Plus } + /// Stream Deck key layout direction #[derive(Debug, Copy, Clone, PartialEq)] pub enum KeyDirection { @@ -49,13 +50,28 @@ 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, Kind::Mini | Kind::RevisedMini => 6, Kind::Xl => 32, - Kind::Plus => 8, + Kind::Plus => 8 } } @@ -93,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, @@ -169,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 new file mode 100644 index 0000000..4756a82 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,236 @@ +use std::{collections::HashSet, time::Duration, vec}; + +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, + 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, + action: TouchAction, + }, +} + +///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))] +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 value is the delta of the turn. + ///Negative values are counter-clockwise, positive values are clockwise + Turned(i8), +} + +///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::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: &Kind) -> Result, crate::Error> { + let indices = kind.touch_data_indices(); + + if indices.is_none() { + return Err(crate::Error::UnsupportedInput); + } + 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::UnsupportedInput) + }; + + 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: &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: &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 1 + offset..offset + keys + 1 { + if cmd[i] == 0 { + continue; + } + + let button = match self.deck.kind.key_direction() { + 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 + 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, + action: ButtonAction::Pressed, + }); + } + + // 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, + action: ButtonAction::Released, + }); + return false; + } + true + }); + + // Add the fresh_presses HashSet to the pressed_keys HashSet + self.pressed_keys.extend(fresh_presses); + Ok(events) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8304f39..8fe14c0 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, } @@ -123,7 +127,6 @@ impl StreamDeck { pids::MK2 => Kind::Mk2, pids::REVISED_MINI => Kind::RevisedMini, pids::PLUS => Kind::Plus, - _ => return Err(Error::UnrecognisedPID), }; @@ -217,6 +220,7 @@ impl StreamDeck { Ok(()) } + /// Probe for connected devices. /// /// Returns a list of results, @@ -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 @@ -263,14 +283,15 @@ 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); } } let mut out = vec![0u8; keys]; + 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 eacd153..15b5b23 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}; +pub use streamdeck::{info, Colour, Error, Filter, ImageOptions, InputEvent, InputManager, Kind, StreamDeck}; + + #[derive(StructOpt)] #[structopt(name = "streamdeck-cli", about = "A CLI for the Elgato StreamDeck")] @@ -32,8 +34,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 @@ -49,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 @@ -67,7 +77,8 @@ pub enum Commands { #[structopt(flatten)] opts: ImageOptions, - } + }, + Probe, } fn main() { @@ -121,20 +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()?; - 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"), + 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)?;