packages feed

lifx-lan 0.1.0.2 → 0.2.0

raw patch · 3 files changed

+226/−52 lines, 3 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Lifx.Lan: SetColor :: HSBK -> Duration -> Message
- Lifx.Lan: SetLightPower :: Bool -> Duration -> Message
- Lifx.Lan: SetPower :: Bool -> Message
- Lifx.Lan: [ackRequired] :: Header -> Bool
- Lifx.Lan: [addressable] :: Header -> Bool
- Lifx.Lan: [brightness] :: HSBK -> Word16
- Lifx.Lan: [hue] :: HSBK -> Word16
- Lifx.Lan: [kelvin] :: HSBK -> Word16
- Lifx.Lan: [origin] :: Header -> Word8
- Lifx.Lan: [packetType] :: Header -> Word16
- Lifx.Lan: [protocol] :: Header -> Word16
- Lifx.Lan: [resRequired] :: Header -> Bool
- Lifx.Lan: [saturation] :: HSBK -> Word16
- Lifx.Lan: [sequenceCounter] :: Header -> Word8
- Lifx.Lan: [size] :: Header -> Word16
- Lifx.Lan: [source] :: Header -> Word32
- Lifx.Lan: [tagged] :: Header -> Bool
- Lifx.Lan: [target] :: Header -> Word64
- Lifx.Lan: [unLifxT] :: LifxT m a -> StateT Word8 (ReaderT (Socket, Word32) m) a
- Lifx.Lan: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader (Network.Socket.Types.Socket, GHC.Word.Word32) (Lifx.Lan.LifxT m)
- Lifx.Lan: instance GHC.Base.Monad m => Control.Monad.State.Class.MonadState GHC.Word.Word8 (Lifx.Lan.LifxT m)
+ Lifx.Lan: DecodeFailure :: ByteString -> ByteOffset -> String -> LifxError
+ Lifx.Lan: LightState :: HSBK -> Word16 -> ByteString -> LightState
+ Lifx.Lan: RecvTimeout :: LifxError
+ Lifx.Lan: StatePower :: Word16 -> StatePower
+ Lifx.Lan: WrongPacketType :: Word16 -> Word16 -> LifxError
+ Lifx.Lan: WrongSender :: SockAddr -> SockAddr -> LifxError
+ Lifx.Lan: WrongSequenceNumber :: Word8 -> Word8 -> LifxError
+ Lifx.Lan: [$sel:ackRequired:Header] :: Header -> Bool
+ Lifx.Lan: [$sel:addressable:Header] :: Header -> Bool
+ Lifx.Lan: [$sel:brightness:HSBK] :: HSBK -> Word16
+ Lifx.Lan: [$sel:hsbk:LightState] :: LightState -> HSBK
+ Lifx.Lan: [$sel:hue:HSBK] :: HSBK -> Word16
+ Lifx.Lan: [$sel:kelvin:HSBK] :: HSBK -> Word16
+ Lifx.Lan: [$sel:label:LightState] :: LightState -> ByteString
+ Lifx.Lan: [$sel:origin:Header] :: Header -> Word8
+ Lifx.Lan: [$sel:packetType:Header] :: Header -> Word16
+ Lifx.Lan: [$sel:power:LightState] :: LightState -> Word16
+ Lifx.Lan: [$sel:power:StatePower] :: StatePower -> Word16
+ Lifx.Lan: [$sel:protocol:Header] :: Header -> Word16
+ Lifx.Lan: [$sel:resRequired:Header] :: Header -> Bool
+ Lifx.Lan: [$sel:saturation:HSBK] :: HSBK -> Word16
+ Lifx.Lan: [$sel:sequenceCounter:Header] :: Header -> Word8
+ Lifx.Lan: [$sel:size:Header] :: Header -> Word16
+ Lifx.Lan: [$sel:source:Header] :: Header -> Word32
+ Lifx.Lan: [$sel:tagged:Header] :: Header -> Bool
+ Lifx.Lan: [$sel:target:Header] :: Header -> Word64
+ Lifx.Lan: [$sel:unLifxT:LifxT] :: LifxT m a -> StateT Word8 (ReaderT (Socket, Word32, Int) (ExceptT LifxError m)) a
+ Lifx.Lan: [GetColor] :: Message LightState
+ Lifx.Lan: [GetPower] :: Message StatePower
+ Lifx.Lan: [SetColor] :: HSBK -> Duration -> Message ()
+ Lifx.Lan: [SetLightPower] :: Bool -> Duration -> Message ()
+ Lifx.Lan: [SetPower] :: Bool -> Message ()
+ Lifx.Lan: data LifxError
+ Lifx.Lan: data LightState
+ Lifx.Lan: getTimeout :: MonadLifx m => m Int
+ Lifx.Lan: instance Data.Binary.Class.Binary Lifx.Lan.Header
+ Lifx.Lan: instance GHC.Classes.Eq (Lifx.Lan.Message a)
+ Lifx.Lan: instance GHC.Classes.Eq Lifx.Lan.Header
+ Lifx.Lan: instance GHC.Classes.Eq Lifx.Lan.LifxError
+ Lifx.Lan: instance GHC.Classes.Eq Lifx.Lan.LightState
+ Lifx.Lan: instance GHC.Classes.Eq Lifx.Lan.StatePower
+ Lifx.Lan: instance GHC.Classes.Ord (Lifx.Lan.Message a)
+ Lifx.Lan: instance GHC.Classes.Ord Lifx.Lan.Header
+ Lifx.Lan: instance GHC.Classes.Ord Lifx.Lan.LifxError
+ Lifx.Lan: instance GHC.Classes.Ord Lifx.Lan.LightState
+ Lifx.Lan: instance GHC.Classes.Ord Lifx.Lan.StatePower
+ Lifx.Lan: instance GHC.Generics.Generic Lifx.Lan.Header
+ Lifx.Lan: instance GHC.Generics.Generic Lifx.Lan.LifxError
+ Lifx.Lan: instance GHC.Generics.Generic Lifx.Lan.LightState
+ Lifx.Lan: instance GHC.Generics.Generic Lifx.Lan.StatePower
+ Lifx.Lan: instance GHC.Show.Show (Lifx.Lan.Message a)
+ Lifx.Lan: instance GHC.Show.Show Lifx.Lan.Header
+ Lifx.Lan: instance GHC.Show.Show Lifx.Lan.LifxError
+ Lifx.Lan: instance GHC.Show.Show Lifx.Lan.LightState
+ Lifx.Lan: instance GHC.Show.Show Lifx.Lan.StatePower
+ Lifx.Lan: instance Lifx.Lan.Response ()
+ Lifx.Lan: instance Lifx.Lan.Response Lifx.Lan.LightState
+ Lifx.Lan: instance Lifx.Lan.Response Lifx.Lan.StatePower
+ Lifx.Lan: lifxThrow :: MonadLifx m => LifxError -> m a
+ Lifx.Lan: newtype StatePower
- Lifx.Lan: LifxT :: StateT Word8 (ReaderT (Socket, Word32) m) a -> LifxT m a
+ Lifx.Lan: LifxT :: StateT Word8 (ReaderT (Socket, Word32, Int) (ExceptT LifxError m)) a -> LifxT m a
- Lifx.Lan: data Message
+ Lifx.Lan: data Message a
- Lifx.Lan: encodeMessage :: Bool -> Word8 -> Word32 -> Message -> ByteString
+ Lifx.Lan: encodeMessage :: Bool -> Word8 -> Word32 -> Message a -> ByteString
- Lifx.Lan: runLifxT :: MonadIO m => LifxT m a -> m a
+ Lifx.Lan: runLifxT :: MonadIO m => Int -> LifxT m a -> m (Either LifxError a)
- Lifx.Lan: sendMessage :: MonadLifx m => HostAddress -> Message -> m ()
+ Lifx.Lan: sendMessage :: MonadLifx m => HostAddress -> Message a -> m a

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for lifx-lan -## 0.1.0.2 -- YYYY-mm-dd+## 0.2.0 -- 2021-06-18++* Enable querying state (colour or power level).++    * Various breaking changes to enable this.++## 0.1.0.2  * Basic. Only supports setting power and colour.
lifx-lan.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               lifx-lan-version:            0.1.0.2+version:            0.2.0 license:            BSD-3-Clause license-file:       LICENSE author:             George Thomas@@ -32,15 +32,18 @@         time ^>= 1.9.3,     default-language: Haskell2010     default-extensions:+        AllowAmbiguousTypes         BlockArguments         DeriveAnyClass         DeriveFunctor         DeriveGeneric         DerivingStrategies         DerivingVia+        DuplicateRecordFields         EmptyCase         FlexibleContexts         FlexibleInstances+        GADTs         GeneralizedNewtypeDeriving         ImportQualifiedPost         LambdaCase
src/Lifx/Lan.hs view
@@ -7,41 +7,77 @@     runLifx,     LifxT (..),     runLifxT,+    LifxError (..),     MonadLifx (..), +    -- * Responses+    LightState (..),+    StatePower (..),+     -- * Low-level     encodeMessage,     Header (..), ) where  import Control.Monad+import Control.Monad.Except import Control.Monad.Reader-import Control.Monad.State+import Control.Monad.State hiding (get, put) import Data.Binary+import Data.Binary.Get hiding (label) import Data.Binary.Put import Data.Bits+import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BL+import Data.Either.Extra+import Data.Function+import Data.Tuple.Extra import GHC.Generics (Generic)+import GHC.IO.Exception import Network.Socket import Network.Socket.ByteString import System.Random+import System.Timeout  {- Core -}  lifxPort :: PortNumber lifxPort = 56700 -sendMessage :: MonadLifx m => HostAddress -> Message -> m ()+sendMessage :: MonadLifx m => HostAddress -> Message a -> m a sendMessage lightAddr msg = do     sock <- getSocket     source <- getSource-    sequenceCounter <- getCounter+    timeoutDuration <- getTimeout+    counter <- getCounter     incrementCounter+    let receiver = SockAddrInet lifxPort lightAddr     void . liftIO $         sendTo             sock-            (BL.toStrict $ encodeMessage False sequenceCounter source msg)-            (SockAddrInet lifxPort lightAddr)+            (BL.toStrict $ encodeMessage False counter source msg)+            receiver+    getResponse' msg & either pure \(expectedPacketType, messageSize, getBody) -> do+        (bs, sender) <-+            throwEither . liftIO . fmap (maybeToEither RecvTimeout)+                . timeout timeoutDuration+                . recvFrom sock+                $ headerSize + messageSize+        when (sender /= receiver) $ lifxThrow $ WrongSender receiver sender+        case runGetOrFail get $ BL.fromStrict bs of+            Left e -> throwDecodeFailure e+            Right (bs', _, Header{packetType, sequenceCounter}) -> do+                when (sequenceCounter /= counter) $ lifxThrow $ WrongSequenceNumber counter sequenceCounter+                when (packetType /= expectedPacketType) $ lifxThrow $ WrongPacketType packetType expectedPacketType+                case runGetOrFail getBody bs' of+                    Left e -> throwDecodeFailure e+                    Right (_, _, res) -> pure res+  where+    throwDecodeFailure (bs, bo, e) = lifxThrow $ DecodeFailure (BL.toStrict bs) bo e+    throwEither x =+        x >>= \case+            Left e -> lifxThrow e+            Right r -> pure r  data HSBK = HSBK     { hue :: Word16@@ -53,53 +89,146 @@ newtype Duration = Duration Word32     deriving (Eq, Ord, Show, Generic) --- | https://lan.developer.lifx.com/docs/changing-a-device-data Message-    = SetPower Bool-    | SetColor HSBK Duration-    | SetLightPower Bool Duration+data Message a where+    GetPower :: Message StatePower+    SetPower :: Bool -> Message ()+    GetColor :: Message LightState+    SetColor :: HSBK -> Duration -> Message ()+    SetLightPower :: Bool -> Duration -> Message ()+deriving instance (Eq (Message a))+deriving instance (Ord (Message a))+deriving instance (Show (Message a)) +newtype StatePower = StatePower+    { power :: Word16+    }+    deriving (Eq, Ord, Show, Generic)+data LightState = LightState+    { hsbk :: HSBK+    , power :: Word16+    , label :: BS.ByteString+    }+    deriving (Eq, Ord, Show, Generic)++data LifxError+    = DecodeFailure BS.ByteString ByteOffset String+    | RecvTimeout+    | WrongPacketType Word16 Word16 -- expected, then actual+    | WrongSender SockAddr SockAddr -- expected, then actual+    | WrongSequenceNumber Word8 Word8 -- expected, then actual+    deriving (Eq, Ord, Show, Generic)++{- Message responses -}++class Response a where+    getResponse :: Either a (Word16, Int, Get a)++instance Response () where+    getResponse = Left ()+instance Response LightState where+    getResponse = Right $ (107,52,) do+        hsbk <- HSBK <$> getWord16le <*> getWord16le <*> getWord16le <*> getWord16le+        skip 2+        power <- getWord16le+        label <- BS.takeWhile (/= 0) <$> getByteString 32+        skip 8+        pure LightState{..}+instance Response StatePower where+    getResponse =+        Right . (22,2,) $+            StatePower <$> getWord16le++-- | Seeing as all `Message` response types are instances of `Response`, we can hide that type class from users.+getResponse' :: Message a -> Either a (Word16, Int, Get a)+getResponse' = \case+    GetPower{} -> getResponse+    SetPower{} -> getResponse+    GetColor{} -> getResponse+    SetColor{} -> getResponse+    SetLightPower{} -> getResponse+ {- Monad -}  type Lifx = LifxT IO-newtype LifxT m a = LifxT {unLifxT :: StateT Word8 (ReaderT (Socket, Word32) m) a}-    deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader (Socket, Word32), MonadState Word8)+newtype LifxT m a = LifxT+    { unLifxT ::+        StateT+            Word8+            ( ReaderT+                (Socket, Word32, Int)+                ( ExceptT+                    LifxError+                    m+                )+            )+            a+    }+    deriving newtype+        ( Functor+        , Applicative+        , Monad+        , MonadIO+        ) +{- | Note that this throws 'LifxError's as 'IOException's, and sets timeout to 1 second.+Use 'runLifxT' for more control.+-} runLifx :: Lifx a -> IO a-runLifx = runLifxT-runLifxT :: MonadIO m => LifxT m a -> m a-runLifxT (LifxT x) = do+runLifx m =+    runLifxT 1_000_000 m >>= \case+        Left e ->+            ioError+                IOError+                    { ioe_handle = Nothing+                    , ioe_type = OtherError+                    , ioe_location = "LIFX"+                    , ioe_description = show e+                    , ioe_errno = Nothing+                    , ioe_filename = Nothing+                    }+        Right x -> pure x++runLifxT :: MonadIO m => Int -> LifxT m a -> m (Either LifxError a)+runLifxT timeoutDuration (LifxT x) = do     sock <- liftIO $ socket AF_INET Datagram defaultProtocol     liftIO . bind sock $ SockAddrInet defaultPort 0     source <- randomIO-    runReaderT (evalStateT x 0) (sock, source)+    runExceptT $ runReaderT (evalStateT x 0) (sock, source, timeoutDuration)  class MonadIO m => MonadLifx m where     getSocket :: m Socket     getSource :: m Word32+    getTimeout :: m Int     incrementCounter :: m ()     getCounter :: m Word8+    lifxThrow :: LifxError -> m a instance MonadIO m => MonadLifx (LifxT m) where-    getSocket = asks fst-    getSource = asks snd-    incrementCounter = modify succ'-    getCounter = gets id+    getSocket = LifxT $ asks fst3+    getSource = LifxT $ asks snd3+    getTimeout = LifxT $ asks thd3+    incrementCounter = LifxT $ modify succ'+    getCounter = LifxT $ gets id+    lifxThrow = LifxT . throwError instance MonadLifx m => MonadLifx (StateT s m) where     getSocket = lift getSocket     getSource = lift getSource+    getTimeout = lift getTimeout     incrementCounter = lift incrementCounter     getCounter = lift getCounter+    lifxThrow = lift . lifxThrow instance MonadLifx m => MonadLifx (ReaderT e m) where     getSocket = lift getSocket     getSource = lift getSource+    getTimeout = lift getTimeout     incrementCounter = lift incrementCounter     getCounter = lift getCounter+    lifxThrow = lift . lifxThrow  {- Low level -} -encodeMessage :: Bool -> Word8 -> Word32 -> Message -> BL.ByteString+encodeMessage :: Bool -> Word8 -> Word32 -> Message a -> BL.ByteString encodeMessage ackRequired sequenceCounter source msg =-    runPut $ putHeader (messageHeader ackRequired sequenceCounter source msg) >> putMessagePayload msg+    runPut $ put (messageHeader ackRequired sequenceCounter source msg) >> putMessagePayload msg  -- | https://lan.developer.lifx.com/docs/encoding-a-packet data Header = Header@@ -115,38 +244,70 @@     , sequenceCounter :: Word8     , packetType :: Word16     }+    deriving (Eq, Ord, Show, Generic) -putHeader :: Header -> Put-putHeader Header{..} = do-    putWord16le size-    putWord16le $-        protocol-            .|. bitIf addressable 12-            .|. bitIf tagged 13-            .|. bitIf (testBit origin 0) 14-            .|. bitIf (testBit origin 1) 15-    putWord32le source-    putWord64be target-    replicateM_ 6 $ putWord8 0-    putWord8 $-        zeroBits-            .|. bitIf resRequired 0-            .|. bitIf ackRequired 1-    putWord8 sequenceCounter-    replicateM_ 8 $ putWord8 0-    putWord16le packetType-    replicateM_ 2 $ putWord8 0-  where-    bitIf b n = if b then bit n else zeroBits+instance Binary Header where+    get = do+        size <- getWord16le+        protBytes <- getWord16le+        let protocol = flip clearBit 12 . flip clearBit 13 . flip clearBit 14 . flip clearBit 15 $ protBytes+            addressable = testBit protBytes 12+            tagged = testBit protBytes 13+            origin = (if testBit protBytes 14 then 0 else 1) + (if testBit protBytes 15 then 0 else 2)+        source <- getWord32le+        target <- getWord64be+        skip 6+        resAckByte <- getWord8+        let resRequired = testBit resAckByte 0+            ackRequired = testBit resAckByte 1+        sequenceCounter <- getWord8+        skip 8+        packetType <- getWord16le+        skip 2+        pure Header{..} -messageHeader :: Bool -> Word8 -> Word32 -> Message -> Header+    put Header{..} = do+        putWord16le size+        putWord16le $+            protocol+                .|. bitIf addressable 12+                .|. bitIf tagged 13+                .|. bitIf (testBit origin 0) 14+                .|. bitIf (testBit origin 1) 15+        putWord32le source+        putWord64be target+        replicateM_ 6 $ putWord8 0+        putWord8 $+            zeroBits+                .|. bitIf resRequired 0+                .|. bitIf ackRequired 1+        putWord8 sequenceCounter+        replicateM_ 8 $ putWord8 0+        putWord16le packetType+        replicateM_ 2 $ putWord8 0+      where+        bitIf b n = if b then bit n else zeroBits++messageHeader :: Bool -> Word8 -> Word32 -> Message a -> Header messageHeader ackRequired sequenceCounter source = \case+    GetPower{} ->+        Header+            { size = headerSize+            , packetType = 20+            , ..+            }     SetPower{} ->         Header             { size = headerSize + 2             , packetType = 21             , ..             }+    GetColor{} ->+        Header+            { size = headerSize+            , packetType = 101+            , ..+            }     SetColor{} ->         Header             { size = headerSize + 13@@ -160,18 +321,19 @@             , ..             }   where-    target = 0-    headerSize = 36-    protocol = 1024+    target = 0 :: Word64+    protocol = 1024 :: Word16     tagged = True     addressable = True-    origin = 0+    origin = 0 :: Word8     resRequired = False -putMessagePayload :: Message -> Put+putMessagePayload :: Message a -> Put putMessagePayload = \case+    GetPower -> mempty     SetPower b ->         putWord16le if b then maxBound else minBound+    GetColor -> mempty     SetColor HSBK{..} (Duration d) -> do         putWord8 0         putWord16le hue@@ -190,3 +352,6 @@ succ' e     | e == maxBound = minBound     | otherwise = succ e++headerSize :: Num a => a+headerSize = 36