From 8975fa08f49ae7ca9e8f8ce3f3148075bf9ee28d Mon Sep 17 00:00:00 2001 From: Jerry260102 <94995868+Jerry260102@users.noreply.github.com> Date: Fri, 22 May 2026 16:32:58 +0100 Subject: [PATCH] feat: add support for the Stream Deck Plus XL (#54) --- 40-streamdeck.rules | 2 ++ README.md | 1 + examples/async.rs | 17 +++++++++++++---- examples/simple.rs | 17 +++++++++++++---- src/images.rs | 4 ++-- src/info.rs | 28 ++++++++++++++++++++++++++-- src/lib.rs | 8 ++++---- 7 files changed, 61 insertions(+), 16 deletions(-) diff --git a/40-streamdeck.rules b/40-streamdeck.rules index 70d0ff6..42e4e18 100644 --- a/40-streamdeck.rules +++ b/40-streamdeck.rules @@ -13,6 +13,7 @@ SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00a5", MODE="0660" SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00b8", MODE="0660", TAG+="uaccess" SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00b9", MODE="0660", TAG+="uaccess" SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00ba", MODE="0660", TAG+="uaccess" +SUBSYSTEM=="usb", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00c6", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0060", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0063", MODE="0660", TAG+="uaccess" @@ -29,3 +30,4 @@ KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00b8", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00b9", MODE="0660", TAG+="uaccess" KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00ba", MODE="0660", TAG+="uaccess" +KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="00c6", MODE="0660", TAG+="uaccess" diff --git a/README.md b/README.md index e32da30..d919f60 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ As it stands, this library should support the following devices. - Stream Deck Mk2 - Stream Deck Pedal - Stream Deck Plus (thanks to [node-elgato-stream-deck](https://github.com/Julusian/node-elgato-stream-deck)) +- Stream Deck Plus XL (thanks to [@Jerry260102](https://github.com/Jerry260102)) - Stream Deck Neo (thanks to [@ejiektpobehuk](https://github.com/ejiektpobehuk), [@AkechiShiro](https://github.com/AkechiShiro) and [node-elgato-stream-deck](https://github.com/Julusian/node-elgato-stream-deck)) Support for non-Elgato (Mirabox/Ajazz) devices has been removed in v0.11. If you wish to use these devices, please use the `mirajazz` crate, or use v0.10.2 of this crate. diff --git a/examples/async.rs b/examples/async.rs index 1586e60..1224f34 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -4,6 +4,7 @@ compile_error!("The `async` feature must be enabled to compile this example."); use std::time::Duration; use image::open; +use elgato_streamdeck::info::{ImageFormat}; use elgato_streamdeck::{DeviceStateUpdate, list_devices, new_hidapi, AsyncStreamDeck}; use elgato_streamdeck::images::{convert_image_with_format, ImageRect}; use tokio::time::sleep; @@ -41,16 +42,24 @@ async fn main() { } if let Some(format) = device.kind().lcd_image_format() { - let scaled_image = image.clone().resize_to_fill(format.size.0 as u32, format.size.1 as u32, image::imageops::FilterType::Nearest); - let converted_image = convert_image_with_format(format, scaled_image).unwrap(); + let converted_image = convert_image_with_format(format, image.clone()).unwrap(); let _ = device.write_lcd_fill(&converted_image).await; } let small = match device.kind().lcd_strip_size() { Some((w, h)) => { let min = w.min(h) as u32; - let scaled_image = image.clone().resize_to_fill(min, min, image::imageops::Nearest); - Some(ImageRect::from_image(scaled_image).unwrap()) + let converted_image = convert_image_with_format( + ImageFormat { + mode: device.kind().lcd_image_format().unwrap().mode, + size: (min.try_into().unwrap(), min.try_into().unwrap()), + rotation: device.kind().lcd_image_format().unwrap().rotation, + mirror: device.kind().lcd_image_format().unwrap().mirror, + }, + image.clone(), + ) + .unwrap(); + Some(ImageRect::from_image(image::load_from_memory(&converted_image).unwrap()).unwrap()) } None => None, }; diff --git a/examples/simple.rs b/examples/simple.rs index 0c3b20e..26f92ef 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -3,6 +3,7 @@ use std::time::Duration; use image::open; +use elgato_streamdeck::info::{ImageFormat}; use elgato_streamdeck::{DeviceStateUpdate, list_devices, new_hidapi, StreamDeck}; use elgato_streamdeck::images::{convert_image_with_format, ImageRect}; @@ -36,16 +37,24 @@ fn main() { } if let Some(format) = device.kind().lcd_image_format() { - let scaled_image = image.clone().resize_to_fill(format.size.0 as u32, format.size.1 as u32, image::imageops::FilterType::Nearest); - let converted_image = convert_image_with_format(format, scaled_image).unwrap(); + let converted_image = convert_image_with_format(format, image.clone()).unwrap(); let _ = device.write_lcd_fill(&converted_image); } let small = match device.kind().lcd_strip_size() { Some((w, h)) => { let min = w.min(h) as u32; - let scaled_image = image.clone().resize_to_fill(min, min, image::imageops::Nearest); - Some(ImageRect::from_image(scaled_image).unwrap()) + let converted_image = convert_image_with_format( + ImageFormat { + mode: device.kind().lcd_image_format().unwrap().mode, + size: (min.try_into().unwrap(), min.try_into().unwrap()), + rotation: device.kind().lcd_image_format().unwrap().rotation, + mirror: device.kind().lcd_image_format().unwrap().mirror, + }, + image.clone(), + ) + .unwrap(); + Some(ImageRect::from_image(image::load_from_memory(&converted_image).unwrap()).unwrap()) } None => None, }; diff --git a/src/images.rs b/src/images.rs index ca58f3b..0215030 100644 --- a/src/images.rs +++ b/src/images.rs @@ -18,8 +18,6 @@ pub fn convert_image_with_format(image_format: ImageFormat, image: DynamicImage) // Ensuring size of the image let (ws, hs) = image_format.size; - let image = image.resize_exact(ws as u32, hs as u32, FilterType::Nearest); - // Applying rotation let image = match image_format.rotation { ImageRotation::Rot0 => image, @@ -28,6 +26,8 @@ pub fn convert_image_with_format(image_format: ImageFormat, image: DynamicImage) ImageRotation::Rot270 => image.rotate270(), }; + let image = image.resize_exact(ws as u32, hs as u32, FilterType::Nearest); + // Applying mirroring let image = match image_format.mirror { ImageMirroring::None => image, diff --git a/src/info.rs b/src/info.rs index 4e2e86f..064c885 100644 --- a/src/info.rs +++ b/src/info.rs @@ -28,6 +28,8 @@ pub const PID_STREAMDECK_NEO: u16 = 0x009a; pub const PID_STREAMDECK_PEDAL: u16 = 0x0086; /// Product ID of Stream Deck Plus pub const PID_STREAMDECK_PLUS: u16 = 0x0084; +/// Product ID of Stream Deck Plus XL +pub const PID_STREAMDECK_PLUS_XL: u16 = 0x00c6; /// Product ID of Stream Deck Mini Mk2 Module pub const PID_STREAMDECK_MINI_MK2_MODULE: u16 = 0x00b8; /// Product ID of Stream Deck Mk2 Module @@ -70,6 +72,8 @@ pub enum Kind { Pedal, /// Stream Deck Plus Plus, + /// Stream Deck Plus XL + PlusXl, /// Stream Deck Mini Mk2 Module MiniMk2Module, /// Stream Deck Mk2 Module @@ -95,6 +99,7 @@ impl Kind { PID_STREAMDECK_NEO => Some(Kind::Neo), PID_STREAMDECK_PEDAL => Some(Kind::Pedal), PID_STREAMDECK_PLUS => Some(Kind::Plus), + PID_STREAMDECK_PLUS_XL => Some(Kind::PlusXl), PID_STREAMDECK_MINI_MK2_MODULE => Some(Kind::MiniMk2Module), PID_STREAMDECK_MK2_MODULE => Some(Kind::Mk2Module), PID_STREAMDECK_XL_V2_MODULE => Some(Kind::XlV2Module), @@ -119,6 +124,7 @@ impl Kind { Kind::Neo => PID_STREAMDECK_NEO, Kind::Pedal => PID_STREAMDECK_PEDAL, Kind::Plus => PID_STREAMDECK_PLUS, + Kind::PlusXl => PID_STREAMDECK_PLUS_XL, Kind::MiniMk2Module => PID_STREAMDECK_MINI_MK2_MODULE, Kind::Mk2Module => PID_STREAMDECK_MK2_MODULE, Kind::XlV2Module => PID_STREAMDECK_XL_V2_MODULE, @@ -140,6 +146,7 @@ impl Kind { Kind::Neo => ELGATO_VENDOR_ID, Kind::Pedal => ELGATO_VENDOR_ID, Kind::Plus => ELGATO_VENDOR_ID, + Kind::PlusXl => ELGATO_VENDOR_ID, Kind::MiniMk2Module => ELGATO_VENDOR_ID, Kind::Mk2Module => ELGATO_VENDOR_ID, Kind::XlV2Module => ELGATO_VENDOR_ID, @@ -152,6 +159,7 @@ impl Kind { Kind::Original | Kind::OriginalV2 | Kind::Mk2 | Kind::Mk2Scissor | Kind::Mk2Module => 15, Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => 6, Kind::Xl | Kind::XlV2 | Kind::XlV2Module => 32, + Kind::PlusXl => 36, Kind::Pedal => 3, Kind::Neo | Kind::Plus => 8, } @@ -162,7 +170,7 @@ impl Kind { match self { Kind::Original | Kind::OriginalV2 | Kind::Mk2 | Kind::Mk2Scissor | Kind::Mk2Module => 3, Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => 2, - Kind::Xl | Kind::XlV2 | Kind::XlV2Module => 4, + Kind::Xl | Kind::XlV2 | Kind::XlV2Module | Kind::PlusXl => 4, Kind::Pedal => 1, Kind::Neo | Kind::Plus => 2, } @@ -174,6 +182,7 @@ impl Kind { Kind::Original | Kind::OriginalV2 | Kind::Mk2 | Kind::Mk2Scissor | Kind::Mk2Module => 5, Kind::Mini | Kind::MiniMk2 | Kind::MiniDiscord | Kind::MiniMk2Module => 3, Kind::Xl | Kind::XlV2 | Kind::XlV2Module => 8, + Kind::PlusXl => 9, Kind::Pedal => 3, Kind::Neo | Kind::Plus => 4, } @@ -183,6 +192,7 @@ impl Kind { pub fn encoder_count(&self) -> u8 { match self { Kind::Plus => 4, + Kind::PlusXl => 6, _ => 0, } } @@ -199,6 +209,7 @@ impl Kind { pub fn lcd_strip_size(&self) -> Option<(usize, usize)> { match self { Kind::Plus => Some((800, 100)), + Kind::PlusXl => Some((100, 1200)), Kind::Neo => Some((248, 58)), _ => None, } @@ -252,6 +263,13 @@ impl Kind { mirror: ImageMirroring::None, }, + Kind::PlusXl => ImageFormat { + mode: ImageMode::JPEG, + size: (120, 120), + rotation: ImageRotation::Rot270, + mirror: ImageMirroring::None, + }, + Kind::Pedal => ImageFormat::default(), } } @@ -271,6 +289,12 @@ impl Kind { rotation: ImageRotation::Rot0, mirror: ImageMirroring::None, }), + Kind::PlusXl => Some(ImageFormat { + mode: ImageMode::JPEG, + size: (100, 1200), + rotation: ImageRotation::Rot270, + mirror: ImageMirroring::None, + }), _ => None, } } @@ -348,7 +372,7 @@ impl Kind { 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x0f, 0xff, 0xd9, ], - Kind::Plus => vec![ + Kind::Plus | Kind::PlusXl => vec![ 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x78, 0x00, 0x78, 0x03, 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07, 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, diff --git a/src/lib.rs b/src/lib.rs index a292f78..a2c5bd7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -194,8 +194,8 @@ impl StreamDeck { /// Reads all possible input from Stream Deck device pub fn read_input(&self, timeout: Option) -> Result { match &self.kind { - Kind::Plus => { - let data = read_data(&self.device, 14.max(5 + self.kind.encoder_count() as usize), timeout)?; + Kind::Plus | Kind::PlusXl => { + let data = read_data(&self.device, (6 + self.kind.key_count()).max(5 + self.kind.encoder_count()) as usize, timeout)?; if data[0] == 0 { return Ok(StreamDeckInput::NoData); @@ -319,7 +319,7 @@ impl StreamDeck { /// Only Stream Deck Plus supports writing LCD regions, for Stream Deck Neo use write_lcd_fill pub fn write_lcd(&self, x: u16, y: u16, rect: &ImageRect) -> Result<(), StreamDeckError> { match self.kind { - Kind::Plus => (), + Kind::Plus | Kind::PlusXl => (), _ => return Err(StreamDeckError::UnsupportedOperation), } @@ -382,7 +382,7 @@ impl StreamDeck { }, ), - Kind::Plus => { + Kind::Plus | Kind::PlusXl => { let (w, h) = self.kind.lcd_strip_size().unwrap(); self.write_image_data_reports(