From 096f1d792a69955d78ba7aacbe18991562d3b33c Mon Sep 17 00:00:00 2001 From: jdelgado-dtlabs Date: Sun, 4 Jan 2026 00:34:17 -0800 Subject: [PATCH 1/2] Add Stream Deck Module 6 support (USB PID 0x00B8) --- src/StreamDeckSharp/Hardware.cs | 18 ++ .../HidComDriverStreamDeckModule6.cs | 158 ++++++++++++++++++ src/StreamDeckSharp/StreamDeckSharp.csproj | 2 +- 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs diff --git a/src/StreamDeckSharp/Hardware.cs b/src/StreamDeckSharp/Hardware.cs index 5530927..14ce3c4 100644 --- a/src/StreamDeckSharp/Hardware.cs +++ b/src/StreamDeckSharp/Hardware.cs @@ -92,6 +92,17 @@ static Hardware() ElgatoUsbId(0x0063), ElgatoUsbId(0x0090) ); + // .----------------------. + // | Stream Deck Module 6 | + // '----------------------' + + StreamDeckModule6 = + RegisterNewHardwareInternal( + "Stream Deck Module 6", + new GridKeyLayout(3, 2, 80, 25), + new HidComDriverStreamDeckModule6(), + ElgatoUsbId(0x00B8) + ); } /// @@ -118,6 +129,10 @@ static Hardware() /// Details about the Stream Deck Mini /// public static IUsbHidHardware StreamDeckMini { get; } + /// + /// Details about the Stream Deck Module 6 + /// + public static IUsbHidHardware StreamDeckModule6 { get; } /// /// This method registers a new (currently unknown to this library) hardware driver. @@ -196,3 +211,6 @@ internal static UsbHardwareIdAndDriver GetInternalHardwareInfos(UsbVendorProduct } } } + + + diff --git a/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs b/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs new file mode 100644 index 0000000..4686205 --- /dev/null +++ b/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs @@ -0,0 +1,158 @@ +using OpenMacroBoard.SDK; +using System; + +namespace StreamDeckSharp.Internals +{ + /// + /// HID communication driver for Stream Deck Module 6 Keys + /// Based on official Elgato HID documentation: https://docs.elgato.com/streamdeck/hid/module-6 + /// + public sealed class HidComDriverStreamDeckModule6 : IStreamDeckHidComDriver + { + private const int ColorChannels = 3; + private const int ImageSize = 80; // Module 6 uses 80×80 pixel keys per official specs + private readonly byte[] bmpHeader; + + public HidComDriverStreamDeckModule6() + { + this.bmpHeader = GenerateBmpHeader(ImageSize); + } + + /// + /// Generates a BMP header for 8080 pixel images (Module 6 specification) + /// + private static byte[] GenerateBmpHeader(int size) + { + int pixelDataSize = size * size * ColorChannels; + int fileSize = 54 + pixelDataSize; + + return new byte[] + { + // BMP Header (14 bytes) + 0x42, 0x4d, // 'BM' signature + (byte)(fileSize & 0xFF), (byte)((fileSize >> 8) & 0xFF), + (byte)((fileSize >> 16) & 0xFF), (byte)((fileSize >> 24) & 0xFF), + 0x00, 0x00, 0x00, 0x00, // Reserved + 0x36, 0x00, 0x00, 0x00, // Offset to pixel data (54 bytes) + + // DIB Header (40 bytes - BITMAPINFOHEADER) + 0x28, 0x00, 0x00, 0x00, // Header size + (byte)(size & 0xFF), (byte)((size >> 8) & 0xFF), 0x00, 0x00, // Width + (byte)(size & 0xFF), (byte)((size >> 8) & 0xFF), 0x00, 0x00, // Height + 0x01, 0x00, // Color planes + 0x18, 0x00, // Bits per pixel (24-bit RGB) + 0x00, 0x00, 0x00, 0x00, // No compression + (byte)(pixelDataSize & 0xFF), (byte)((pixelDataSize >> 8) & 0xFF), + (byte)((pixelDataSize >> 16) & 0xFF), 0x00, // Image size + 0xc4, 0x0e, 0x00, 0x00, // X pixels per meter + 0xc4, 0x0e, 0x00, 0x00, // Y pixels per meter + 0x00, 0x00, 0x00, 0x00, // Colors in palette + 0x00, 0x00, 0x00, 0x00, // Important colors + }; + } + + // Module 6 specifications per official docs + public int HeaderSize => 16; + public int ReportSize => 1024; // Module 6 uses 1024-byte output reports + public int ExpectedFeatureReportLength => 32; // Feature reports are 32 bytes + public int ExpectedOutputReportLength => 1024; + public int ExpectedInputReportLength => 65; // Input reports are 65 bytes + public int KeyReportOffset => 1; + public byte FirmwareVersionFeatureId => 0xA1; // AP2 (Primary firmware) + public byte SerialNumberFeatureId => 0x03; + public int FirmwareVersionReportSkip => 5; + public int SerialNumberReportSkip => 5; + public double BytesPerSecondLimit => double.PositiveInfinity; + + public byte[] GeneratePayload(KeyBitmap keyBitmap) + { + var rawData = keyBitmap.GetScaledVersion(ImageSize, ImageSize); + var bmp = new byte[ImageSize * ImageSize * ColorChannels + bmpHeader.Length]; + Array.Copy(bmpHeader, 0, bmp, 0, bmpHeader.Length); + + if (rawData.Length != 0) + { + // Rotate image 90 clockwise as per Module 6 specs + for (var y = 0; y < ImageSize; y++) + { + for (var x = 0; x < ImageSize; x++) + { + var src = (y * ImageSize + x) * ColorChannels; + var tar = ((ImageSize - x - 1) * ImageSize + y) * ColorChannels + bmpHeader.Length; + bmp[tar + 0] = rawData[src + 0]; + bmp[tar + 1] = rawData[src + 1]; + bmp[tar + 2] = rawData[src + 2]; + } + } + } + return bmp; + } + + public int ExtKeyIdToHardwareKeyId(int extKeyId) => extKeyId; + public int HardwareKeyIdToExtKeyId(int hardwareKeyId) => hardwareKeyId; + + /// + /// Prepares packet header for Module 6 image upload + /// Per official docs: https://docs.elgato.com/streamdeck/hid/module-6/#upload-data-to-image-memory-bank + /// Offset 0x00: Report ID (0x02) + /// Offset 0x01: Command (0x01) + /// Offset 0x02: Chunk Index + /// Offset 0x03: Reserved (0x00) + /// Offset 0x04: Show Image flag (0x01 to display immediately) + /// Offset 0x05: Key Index + /// Offset 0x06-0x0F: Reserved (10 bytes of 0x00) + /// Offset 0x10+: Chunk Data + /// + public void PrepareDataForTransmission(byte[] data, int pageNumber, int payloadLength, int keyId, bool isLast) + { + data[0] = 0x02; // Report ID + data[1] = 0x01; // Command: Upload Data to Image Memory Bank + data[2] = (byte)pageNumber; // Chunk Index + data[3] = 0x00; // Reserved + data[4] = (byte)(isLast ? 0x01 : 0x00); // Show Image flag (show on last packet) + data[5] = (byte)(keyId + 1); // Key Index (0-5 for Module 6) + + // Reserved space (bytes 6-15, total 10 bytes) + for (int i = 6; i < 16; i++) + { + data[i] = 0x00; + } + + // Payload data starts at offset 0x10 (byte 16) + } + + /// + /// Creates brightness control message for Module 6 + /// Per official docs Report ID: 0x05, Command: 0x55 + /// + public byte[] GetBrightnessMessage(byte percent) + { + if (percent > 100) throw new ArgumentOutOfRangeException(nameof(percent)); + + // Feature Report format for Set Backlight Brightness + var buffer = new byte[32]; // Feature reports are 32 bytes, zero-padded + buffer[0] = 0x05; // Report ID + buffer[1] = 0x55; // Command + buffer[2] = 0xAA; + buffer[3] = 0xD1; + buffer[4] = 0x01; + buffer[5] = percent; // Brightness value (0-100) + + return buffer; + } + + /// + /// Creates show logo message for Module 6 + /// Per official docs Report ID: 0x0B, Command: 0x63, Payload: 0x00 + /// + public byte[] GetLogoMessage() + { + var buffer = new byte[32]; // Feature reports are 32 bytes, zero-padded + buffer[0] = 0x0B; // Report ID + buffer[1] = 0x63; // Command + buffer[2] = 0x00; // Show Boot Logo + + return buffer; + } + } +} diff --git a/src/StreamDeckSharp/StreamDeckSharp.csproj b/src/StreamDeckSharp/StreamDeckSharp.csproj index b2a6bd5..bb3439c 100644 --- a/src/StreamDeckSharp/StreamDeckSharp.csproj +++ b/src/StreamDeckSharp/StreamDeckSharp.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net462;net8.0 From 6c58cbc1552caa8a95061b51fd417869928f8f4e Mon Sep 17 00:00:00 2001 From: jdelgado-dtlabs Date: Wed, 7 Jan 2026 12:46:39 -0800 Subject: [PATCH 2/2] Refactor Module 6 driver to inherit from Mini driver --- .../Internals/HidComDriverStreamDeckMini.cs | 20 ++-- .../HidComDriverStreamDeckModule6.cs | 100 ++++-------------- 2 files changed, 31 insertions(+), 89 deletions(-) diff --git a/src/StreamDeckSharp/Internals/HidComDriverStreamDeckMini.cs b/src/StreamDeckSharp/Internals/HidComDriverStreamDeckMini.cs index bd185a3..eb56ed6 100644 --- a/src/StreamDeckSharp/Internals/HidComDriverStreamDeckMini.cs +++ b/src/StreamDeckSharp/Internals/HidComDriverStreamDeckMini.cs @@ -6,7 +6,7 @@ namespace StreamDeckSharp.Internals /// /// HID Stream Deck communication driver for the Stream Deck Mini. /// - public sealed class HidComDriverStreamDeckMini + public class HidComDriverStreamDeckMini : IStreamDeckHidComDriver { private const int ColorChannels = 3; @@ -46,22 +46,22 @@ public HidComDriverStreamDeckMini(int imgSize) public int ReportSize => 1024; /// - public int ExpectedFeatureReportLength => 17; + public virtual int ExpectedFeatureReportLength => 17; /// - public int ExpectedOutputReportLength => 1024; + public virtual int ExpectedOutputReportLength => 1024; /// - public int ExpectedInputReportLength => 17; + public virtual int ExpectedInputReportLength => 17; /// - public int KeyReportOffset => 1; + public virtual int KeyReportOffset => 1; /// - public byte FirmwareVersionFeatureId => 4; + public virtual byte FirmwareVersionFeatureId => 4; /// - public byte SerialNumberFeatureId => 3; + public virtual byte SerialNumberFeatureId => 3; /// public int FirmwareVersionReportSkip => 5; @@ -112,7 +112,7 @@ public int HardwareKeyIdToExtKeyId(int hardwareKeyId) } /// - public void PrepareDataForTransmission( + public virtual void PrepareDataForTransmission( byte[] data, int pageNumber, int payloadLength, @@ -128,7 +128,7 @@ bool isLast } /// - public byte[] GetBrightnessMessage(byte percent) + public virtual byte[] GetBrightnessMessage(byte percent) { if (percent > 100) { @@ -147,7 +147,7 @@ public byte[] GetBrightnessMessage(byte percent) } /// - public byte[] GetLogoMessage() + public virtual byte[] GetLogoMessage() { return new byte[] { 0x0B, 0x63 }; } diff --git a/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs b/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs index 4686205..fe181e8 100644 --- a/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs +++ b/src/StreamDeckSharp/Internals/HidComDriverStreamDeckModule6.cs @@ -1,4 +1,3 @@ -using OpenMacroBoard.SDK; using System; namespace StreamDeckSharp.Internals @@ -6,90 +5,30 @@ namespace StreamDeckSharp.Internals /// /// HID communication driver for Stream Deck Module 6 Keys /// Based on official Elgato HID documentation: https://docs.elgato.com/streamdeck/hid/module-6 + /// Inherits shared image processing logic from . /// - public sealed class HidComDriverStreamDeckModule6 : IStreamDeckHidComDriver + public sealed class HidComDriverStreamDeckModule6 : HidComDriverStreamDeckMini { - private const int ColorChannels = 3; - private const int ImageSize = 80; // Module 6 uses 80×80 pixel keys per official specs - private readonly byte[] bmpHeader; - - public HidComDriverStreamDeckModule6() - { - this.bmpHeader = GenerateBmpHeader(ImageSize); - } + private const int ImageSize = 80; // Module 6 uses 80�80 pixel keys per official specs /// - /// Generates a BMP header for 8080 pixel images (Module 6 specification) + /// Initializes a new instance of the class. /// - private static byte[] GenerateBmpHeader(int size) + public HidComDriverStreamDeckModule6() + : base(ImageSize) { - int pixelDataSize = size * size * ColorChannels; - int fileSize = 54 + pixelDataSize; - - return new byte[] - { - // BMP Header (14 bytes) - 0x42, 0x4d, // 'BM' signature - (byte)(fileSize & 0xFF), (byte)((fileSize >> 8) & 0xFF), - (byte)((fileSize >> 16) & 0xFF), (byte)((fileSize >> 24) & 0xFF), - 0x00, 0x00, 0x00, 0x00, // Reserved - 0x36, 0x00, 0x00, 0x00, // Offset to pixel data (54 bytes) - - // DIB Header (40 bytes - BITMAPINFOHEADER) - 0x28, 0x00, 0x00, 0x00, // Header size - (byte)(size & 0xFF), (byte)((size >> 8) & 0xFF), 0x00, 0x00, // Width - (byte)(size & 0xFF), (byte)((size >> 8) & 0xFF), 0x00, 0x00, // Height - 0x01, 0x00, // Color planes - 0x18, 0x00, // Bits per pixel (24-bit RGB) - 0x00, 0x00, 0x00, 0x00, // No compression - (byte)(pixelDataSize & 0xFF), (byte)((pixelDataSize >> 8) & 0xFF), - (byte)((pixelDataSize >> 16) & 0xFF), 0x00, // Image size - 0xc4, 0x0e, 0x00, 0x00, // X pixels per meter - 0xc4, 0x0e, 0x00, 0x00, // Y pixels per meter - 0x00, 0x00, 0x00, 0x00, // Colors in palette - 0x00, 0x00, 0x00, 0x00, // Important colors - }; } - // Module 6 specifications per official docs - public int HeaderSize => 16; - public int ReportSize => 1024; // Module 6 uses 1024-byte output reports - public int ExpectedFeatureReportLength => 32; // Feature reports are 32 bytes - public int ExpectedOutputReportLength => 1024; - public int ExpectedInputReportLength => 65; // Input reports are 65 bytes - public int KeyReportOffset => 1; - public byte FirmwareVersionFeatureId => 0xA1; // AP2 (Primary firmware) - public byte SerialNumberFeatureId => 0x03; - public int FirmwareVersionReportSkip => 5; - public int SerialNumberReportSkip => 5; - public double BytesPerSecondLimit => double.PositiveInfinity; + // Module 6 specifications per official docs (override Mini's values where different) + + /// + public override int ExpectedFeatureReportLength => 32; // Feature reports are 32 bytes - public byte[] GeneratePayload(KeyBitmap keyBitmap) - { - var rawData = keyBitmap.GetScaledVersion(ImageSize, ImageSize); - var bmp = new byte[ImageSize * ImageSize * ColorChannels + bmpHeader.Length]; - Array.Copy(bmpHeader, 0, bmp, 0, bmpHeader.Length); + /// + public override int ExpectedInputReportLength => 65; // Input reports are 65 bytes - if (rawData.Length != 0) - { - // Rotate image 90 clockwise as per Module 6 specs - for (var y = 0; y < ImageSize; y++) - { - for (var x = 0; x < ImageSize; x++) - { - var src = (y * ImageSize + x) * ColorChannels; - var tar = ((ImageSize - x - 1) * ImageSize + y) * ColorChannels + bmpHeader.Length; - bmp[tar + 0] = rawData[src + 0]; - bmp[tar + 1] = rawData[src + 1]; - bmp[tar + 2] = rawData[src + 2]; - } - } - } - return bmp; - } - - public int ExtKeyIdToHardwareKeyId(int extKeyId) => extKeyId; - public int HardwareKeyIdToExtKeyId(int hardwareKeyId) => hardwareKeyId; + /// + public override byte FirmwareVersionFeatureId => 0xA1; // AP2 (Primary firmware) /// /// Prepares packet header for Module 6 image upload @@ -103,7 +42,7 @@ public byte[] GeneratePayload(KeyBitmap keyBitmap) /// Offset 0x06-0x0F: Reserved (10 bytes of 0x00) /// Offset 0x10+: Chunk Data /// - public void PrepareDataForTransmission(byte[] data, int pageNumber, int payloadLength, int keyId, bool isLast) + public override void PrepareDataForTransmission(byte[] data, int pageNumber, int payloadLength, int keyId, bool isLast) { data[0] = 0x02; // Report ID data[1] = 0x01; // Command: Upload Data to Image Memory Bank @@ -125,9 +64,12 @@ public void PrepareDataForTransmission(byte[] data, int pageNumber, int payloadL /// Creates brightness control message for Module 6 /// Per official docs Report ID: 0x05, Command: 0x55 /// - public byte[] GetBrightnessMessage(byte percent) + public override byte[] GetBrightnessMessage(byte percent) { - if (percent > 100) throw new ArgumentOutOfRangeException(nameof(percent)); + if (percent > 100) + { + throw new ArgumentOutOfRangeException(nameof(percent)); + } // Feature Report format for Set Backlight Brightness var buffer = new byte[32]; // Feature reports are 32 bytes, zero-padded @@ -145,7 +87,7 @@ public byte[] GetBrightnessMessage(byte percent) /// Creates show logo message for Module 6 /// Per official docs Report ID: 0x0B, Command: 0x63, Payload: 0x00 /// - public byte[] GetLogoMessage() + public override byte[] GetLogoMessage() { var buffer = new byte[32]; // Feature reports are 32 bytes, zero-padded buffer[0] = 0x0B; // Report ID