{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE OverloadedStrings #-} module System.Hardware.Streamdeck ( Deck (..) , Page (..) , ConnectedDecks (..) , Image , Pixel , enumerate , open , serialNumber, firmware , sendRaw , writeImage , update , setBrightness , readButtonState , solidRGB ) where import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BSC (pack) import qualified Data.List.Split as Split (chunksOf) import qualified Data.Word as DW (Word16, Word8) import qualified System.HIDAPI as HID import Prelude type MiniActiveRow = (Bool, Bool, Bool) type SDActiveRow = (Bool, Bool, Bool, Bool, Bool) type XLActiveRow = (Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool) type MiniActiveMap = (MiniActiveRow, MiniActiveRow) type SDActiveMap = (SDActiveRow, SDActiveRow, SDActiveRow) type XLActiveMap = (XLActiveRow, XLActiveRow, XLActiveRow, XLActiveRow) data ButtonsActive = MiniActive MiniActiveMap | SDActive SDActiveMap | XLActive XLActiveMap deriving (Show, Eq) type Image = BS.ByteString type PixelR = DW.Word8 type PixelG = DW.Word8 type PixelB = DW.Word8 type Pixel = (PixelR, PixelG, PixelB) type MiniRow = (Image, Image, Image) type SDRow = (Image, Image, Image, Image, Image) type XLRow = (Image, Image, Image, Image, Image, Image, Image, Image) data PageType = MiniPage | SDPage | XLPage data Page :: PageType -> * where MiniPage' :: (MiniRow, MiniRow) -> Page 'MiniPage SDPage' :: (SDRow, SDRow, SDRow) -> Page 'SDPage XLPage' :: (XLRow, XLRow, XLRow, XLRow) -> Page 'XLPage data DeckSpec = DeckSpec { _ref :: HID.Device , _serialNumber :: BS.ByteString , _firmware :: BS.ByteString } data DeckType = StreamDeckMini | StreamDeck | StreamDeckXL data Deck :: DeckType -> * where StreamDeckMini' :: (HID.DeviceInfo, Maybe DeckSpec, Page 'MiniPage) -> Deck 'StreamDeckMini StreamDeck' :: (HID.DeviceInfo, Maybe DeckSpec, Page 'SDPage) -> Deck 'StreamDeck StreamDeckXL' :: (HID.DeviceInfo, Maybe DeckSpec, Page 'XLPage) -> Deck 'StreamDeckXL data ConnectedDecks = ConnectedDecks { minis :: [Deck 'StreamDeckMini] , decks :: [Deck 'StreamDeck] , xls :: [Deck 'StreamDeckXL] } -- Example Device Info for an early Stream Deck: -- DeviceInfo { path = "/dev/hidraw%d" -- , vendorId = 4057 -- , produc tId = 96 -- , serialNumber = Just "" -- , releaseNumber = 256 -- , manufacturerString = Just "Elgato Systems" -- , productString = Just "Stream Deck" -- , usagePage = 13410 -- , usage = 13359 -- , interfaceNumber = 0 -- } -- Constant across all Elgato hardware vendorID :: Maybe DW.Word16 vendorID = Just 0x0fd9 -- Enumerate Stream Decks, differentiating by type enumerate :: IO ConnectedDecks enumerate = do m <- enum (Just 0x0063) s <- enum (Just 0x0060) x <- enum (Just 0x006c) return $ ConnectedDecks { minis = fmap fm m , decks = fmap fs s , xls = fmap fx x } where enum = HID.enumerate vendorID fm di = StreamDeckMini' (di, Nothing, emptyMiniPage) fs di = StreamDeck' (di, Nothing, emptySDPage) fx di = StreamDeckXL' (di, Nothing, emptyXLPage) -- Note: older firmware Stream Decks have a bug where "" is reported as the -- Serial number unless a feature report requesting the Serial Number is issued. -- Newer Stream Decks do not require this step. determineSN :: HID.DeviceInfo -> HID.Device -> IO BS.ByteString determineSN info dev = case HID.serialNumber info of Nothing -> requestSN dev Just "" -> requestSN dev Just sn -> return $ BSC.pack $ show sn requestSN :: HID.Device -> IO BS.ByteString requestSN dev = do sn <- HID.getFeatureReport dev 3 17 return $ BS.takeWhile (/= 0) $ BS.drop 5 $ snd sn requestFW :: HID.Device -> IO BS.ByteString requestFW dev = do fw <- HID.getFeatureReport dev 4 17 return $ BS.takeWhile (/= 0) $ BS.drop 5 $ snd fw open :: Deck a -> IO (Deck a) open (StreamDeckMini' (device, Nothing, _)) = open' device (\deck sn fw -> StreamDeckMini' (device, Just $ DeckSpec { _ref = deck, _serialNumber = sn, _firmware = fw }, emptyMiniPage) ) open (StreamDeck' (device, Nothing, _)) = open' device (\deck sn fw -> StreamDeck' (device, Just $ DeckSpec { _ref = deck, _serialNumber = sn, _firmware = fw }, emptySDPage) ) open (StreamDeckXL' (device, Nothing, _)) = open' device (\deck sn fw -> StreamDeckXL' (device, Just $ DeckSpec { _ref = deck, _serialNumber = sn, _firmware = fw }, emptyXLPage) ) open _ = undefined -- Opening an already-opened Stream Deck is undefined open' :: HID.DeviceInfo -> (HID.Device -> BS.ByteString -> BS.ByteString -> Deck a) -> IO (Deck a) open' device f = HID.withHIDAPI $ do deck <- HID.openDeviceInfo device sn <- determineSN device deck fw <- requestFW deck return $ f deck sn fw serialNumber :: Deck a -> BS.ByteString serialNumber (StreamDeckMini' (_, Just ds, _)) = _serialNumber ds serialNumber (StreamDeck' (_, Just ds, _)) = _serialNumber ds serialNumber (StreamDeckXL' (_, Just ds, _)) = _serialNumber ds serialNumber _ = undefined firmware :: Deck a -> BS.ByteString firmware (StreamDeckMini' (_, Just ds, _)) = _firmware ds firmware (StreamDeck' (_, Just ds, _)) = _firmware ds firmware (StreamDeckXL' (_, Just ds, _)) = _firmware ds firmware _ = undefined -- Constants useful in communicating with Stream Decks packetSize :: Int packetSize = 4096 page1Pixels :: Int page1Pixels = 2583 page2Pixels :: Int page2Pixels = 2601 buttonPixels :: Int buttonPixels = page1Pixels + page2Pixels solidRGB :: PixelR -> PixelG -> PixelB -> Image solidRGB r g b = BS.pack $ take (3 * (buttonPixels - 1)) $ cycle [b, g, r] emptyMiniRow :: MiniRow emptyMiniRow = (b, b, b) where b = solidRGB 0 0 0 emptySDRow :: SDRow emptySDRow = (b, b, b, b, b) where b = solidRGB 0 0 0 emptyXLRow :: XLRow emptyXLRow = (b, b, b, b, b, b, b, b) where b = solidRGB 0 0 0 emptyMiniPage :: Page 'MiniPage emptyMiniPage = MiniPage' (r, r) where r = emptyMiniRow emptySDPage :: Page 'SDPage emptySDPage = SDPage' (r, r, r) where r = emptySDRow emptyXLPage :: Page 'XLPage emptyXLPage = XLPage' (r, r, r, r) where r = emptyXLRow -- Helper method to return the device for a given Deck ref :: Deck a -> HID.Device ref (StreamDeckMini' (_, Just spec, _)) = _ref spec ref (StreamDeck' (_, Just spec, _)) = _ref spec ref (StreamDeckXL' (_, Just spec, _)) = _ref spec ref _ = undefined -- requesting a reference to an unopened Deck is undefined -- Send raw bytes to a Deck, broken up by packets sendRaw :: Deck a -> BS.ByteString -> IO () sendRaw deck bs = if BS.length bs > packetSize then (do _ <- HID.write (ref deck) $ BS.take packetSize bs sendRaw deck $ fixContinuationPacket bs) else (do _ <- HID.write (ref deck) bs return ()) -- In cases where the first byte of a continuation packet is equal to 0, the -- byte is discarded entirely, resulting in discoloration. Continuation packets -- only get used in cases where data > 4096 bytes is being sent, so in practice -- this is only a concern for sending image data. fixContinuationPacket :: BS.ByteString -> BS.ByteString fixContinuationPacket b | BS.length b < packetSize = BS.pack [] | otherwise = let rest = BS.drop packetSize b byte = BS.head rest in if byte > 0 then rest else BS.cons ((B..|.) 1 byte) (BS.drop 1 rest) -- Generate a packet to set the Deck's brightness. Values passed are bracketed -- to 0 >= x >= 100. Full brightness is defaulted to otherwise. setBrightness :: DW.Word8 -> BS.ByteString setBrightness b | b <= 100 && b >= 0 = setBrightness' b | otherwise = setBrightness' 100 setBrightness' :: DW.Word8 -> BS.ByteString setBrightness' b = BS.pack [ 0x05 -- Report 0x05 , 0x55, 0xAA, 0xD1, 0x01 -- Command (brightness) , b, 0x00, 0x00, 0x00 -- brightness , 0x00, 0x00, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00 ] -- Write a given page of data to a Deck. writePage :: Deck a -> Int -> DW.Word8 -> BS.ByteString -> IO () writePage deck p i bs = sendRaw deck $ BS.append (page p i) bs -- Generate a given page of data to be sent to a Deck. The first page of a -- command (for commands requiring 2 pages - typically sending image data) has -- a longer prologue than the second page. No commands with more than 2 pages -- exist. page :: Int -> DW.Word8 -> BS.ByteString page 1 i = BS.pack [ 0x02, 0x01, 0x01, 0x00, 0x00, i+1, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 , 0x42, 0x4d, 0xf6, 0x3c, 0x00, 0x00, 0x00, 0x00 , 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00 , 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00 , 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00 , 0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0xc4, 0x0e , 0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] page 2 i = BS.pack [ 0x02, 0x01, 0x02, 0x00, 0x01, i+1, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] page _ _ = BS.pack [] -- Helper: read data from a given Deck readRaw :: HID.Device -> Int -> IO BS.ByteString readRaw = HID.read -- Stream Deck reports button state ONLY upon button press/release -- Stream Deck will send a 16 byte message, with the following format: -- 01 AA BB CC DD EE FF GG HH II JJ KK LL MM NN OO -- * Byte 0 being set to 0x01 is static, indicating a "button event" message. -- * AA-OO are 1 byte, if the low bit is set, the button is pressed. Bits 1-7 -- appear to be unused. -- Note: The Stream Deck Mini will not send data beyond the 8th byte -- Note: The Stream Deck XL's format is slightly different. The Stream Deck XL -- sends a prelude of 01 00 20 00, followed by 32 bytes, following the -- above pattern - first byte for the first (top-left) button, next for -- the button to its right, and so on for a total of 36 bytes. readButtonState :: Deck a -> IO ButtonsActive readButtonState (StreamDeck' (_, Just ds, _)) = bytesToActiveMap . BS.unpack <$> readRaw (_ref ds) 16 readButtonState (StreamDeckMini' (_, Just ds, _)) = bytesToActiveMap . BS.unpack <$> readRaw (_ref ds) 7 readButtonState (StreamDeckXL' (_, Just ds, _)) = bytesToActiveMap . BS.unpack <$> readRaw (_ref ds) 36 readButtonState _ = undefined bytesToActiveMap :: [DW.Word8] -> ButtonsActive bytesToActiveMap xs | length xs == 16 = sdMap $ map (== 1) $ drop 1 xs | length xs == 7 = miniMap $ map (== 1) $ drop 1 xs | length xs == 36 = xlMap $ map (== 1) $ drop 4 xs | otherwise = undefined sdMap :: [Bool] -> ButtonsActive sdMap bs = SDActive $ sdam $ sdar <$> Split.chunksOf 5 bs where sdar :: [Bool] -> SDActiveRow sdar [a, b, c, d, e] = (a,b,c,d,e) sdar _ = undefined sdam :: [SDActiveRow] -> SDActiveMap sdam [a, b, c] = (a,b,c) sdam _ = undefined miniMap :: [Bool] -> ButtonsActive miniMap bs = MiniActive $ miniam $ miniar <$> Split.chunksOf 3 bs where miniar :: [Bool] -> MiniActiveRow miniar [a, b, c] = (a,b,c) miniar _ = undefined miniam :: [MiniActiveRow] -> MiniActiveMap miniam [a, b] = (a,b) miniam _ = undefined xlMap :: [Bool] -> ButtonsActive xlMap bs = XLActive $ xlam $ xlar <$> Split.chunksOf 8 bs where xlar :: [Bool] -> XLActiveRow xlar [a, b, c, d, e, f, g, h] = (a,b,c,d,e,f,g,h) xlar _ = undefined xlam :: [XLActiveRow] -> XLActiveMap xlam [a, b, c, d] = (a,b,c,d) xlam _ = undefined writeImage :: Deck a -> DW.Word8 -> Image -> IO () writeImage deck button img = let page1 = BS.take (3 * page1Pixels) img page2 = BS.take (3 * page2Pixels) $ BS.drop (3 * page1Pixels) img in writePage deck 1 button page1 >> writePage deck 2 button page2 drawMiniRow :: Deck 'StreamDeckMini -> DW.Word8 -> MiniRow -> IO () drawMiniRow d r (i0, i1, i2) = do writeImage d (r * 3) i0 writeImage d (r * 3 + 1) i1 writeImage d (r * 3 + 2) i2 drawSDRow :: Deck 'StreamDeck -> DW.Word8 -> SDRow -> IO () drawSDRow d r (i0, i1, i2, i3, i4) = do writeImage d (r * 5) i0 writeImage d (r * 5 + 1) i1 writeImage d (r * 5 + 2) i2 writeImage d (r * 5 + 3) i3 writeImage d (r * 5 + 4) i4 drawXLRow :: Deck 'StreamDeckXL -> DW.Word8 -> XLRow -> IO () drawXLRow d r (i0, i1, i2, i3, i4, i5, i6, i7) = do writeImage d (r * 8) i0 writeImage d (r * 8 + 1) i1 writeImage d (r * 8 + 2) i2 writeImage d (r * 8 + 3) i3 writeImage d (r * 8 + 4) i4 writeImage d (r * 8 + 5) i5 writeImage d (r * 8 + 6) i6 writeImage d (r * 8 + 7) i7 render :: Deck a -> IO () render (StreamDeckMini' (dev, Just ds, display)) = drawRow d 0 r0 >> drawRow d 1 r1 where MiniPage' (r0, r1) = display drawRow = drawMiniRow d = StreamDeckMini' (dev, Just ds, display) render (StreamDeck' (dev, Just ds, display)) = drawRow d 0 r0 >> drawRow d 1 r1 >> drawRow d 2 r2 where SDPage' (r0, r1, r2) = display drawRow = drawSDRow d = StreamDeck' (dev, Just ds, display) render (StreamDeckXL' (dev, Just ds, display)) = drawRow d 0 r0 >> drawRow d 1 r1 >> drawRow d 2 r2 >> drawRow d 3 r3 where XLPage' (r0, r1, r2, r3) = display drawRow = drawXLRow d = StreamDeckXL' (dev, Just ds, display) render _ = undefined update :: Deck a -> Page b -> IO (Deck a) update (StreamDeckMini' (di, Just ds, _)) (MiniPage' p) = do let new = StreamDeckMini' (di, Just ds, MiniPage' p) render new >> return new update (StreamDeck' (di, Just ds, _)) (SDPage' p) = do let new = StreamDeck' (di, Just ds, SDPage' p) render new >> return new update (StreamDeckXL' (di, Just ds, _)) (XLPage' p) = do let new = StreamDeckXL' (di, Just ds, XLPage' p) render new >> return new update _ _ = undefined