gpio 0.1.0.2 → 0.1.0.3
raw patch · 9 files changed
+332/−275 lines, 9 filesdep +directorydep +exceptionsdep +safedep −basic-preludedep −string-conversionsdep ~base
Dependencies added: directory, exceptions, safe
Dependencies removed: basic-prelude, string-conversions
Dependency ranges changed: base
Files
- README.md +14/−0
- exec/Main.hs +44/−0
- gpio.cabal +49/−38
- lib/System/GPIO.hs +112/−0
- lib/System/GPIO/Path.hs +31/−0
- lib/System/GPIO/Types.hs +82/−0
- src/gpio/Main.hs +0/−57
- src/lib/System/GPIO.hs +0/−103
- src/lib/System/GPIO/Types.hs +0/−77
+ README.md view
@@ -0,0 +1,14 @@+Haskell GPIO interface, designed specifically for the RaspberryPi.++This library also ships with an executable for directly running gpio commands. Note: when using the executable there are less guarantees about the current state of a pin. For example, when using the library we can be sure that an `ActivePin 'Out` has already been initialized and configured for writing values, when using the executable we are less sure, and therefore errors might occur.+++Note: this package has limited functionality, might be buggy and is not optimized. It works for simple read/write operations on a few pre-defined pins, but still needs a lot more testing.++TODO:+ * Remove `runLineHack` when reading GPIO value files.+ * Optimize file reading to only look at first char.+ * Support other pin modes (in/out/up/down/pwm)+ * Support edges+ * Look into GPIO supported file watching+ * Add all pin numbers
+ exec/Main.hs view
@@ -0,0 +1,44 @@+module Main (main) where++import Control.Monad+import Options.Generic++import System.GPIO++data Command+ = Init Int String+ | Read Int String+ | Write Int String+ | Close Int String+ deriving (Generic, Show)++instance ParseRecord Command++main :: IO ()+main = getRecord "GPIO" >>= \case+ Init x "in" -> void (initReaderPin (getPin x))+ Init x "out" -> void (initWriterPin (getPin x))+ Init _ _ -> error "Usage error: use 'in' or 'out' for export direction."++ Read x "in" -> reattachToReaderPin (getPin x) >>= readPin >>= print+ Read x "out" -> reattachToWriterPin (getPin x) >>= readPin >>= print+ Read _ _ -> error "Usage error: use 'in' or 'out' for export direction."++ Write x "hi" -> reattachToWriterPin (getPin x) >>= writePin HI+ Write x "lo" -> reattachToWriterPin (getPin x) >>= writePin LO+ Write _ _ -> error "Usage error: use 'hi' or 'lo' for write value."++ Close x "in" -> reattachToReaderPin (getPin x) >>= closePin+ Close x "out" -> reattachToWriterPin (getPin x) >>= closePin+ Close _ _ -> error "Usage error: use 'in' or 'out' for export direction."+ where+ getPin = throwLeft . parsePin+++parsePin :: Int -> Either String Pin+parsePin x = case fromInt x of+ Nothing -> Left ("Usage error: could not parse pin value from integer: " ++ show x)+ Just p -> Right p++throwLeft :: Either String a -> a+throwLeft = either error id
gpio.cabal view
@@ -1,45 +1,56 @@-name: gpio-version: 0.1.0.2-synopsis: Haskell GPIO interface, designed specifically for the RaspberryPi.-description: Please see README.md-homepage: http://github.com/tgolson/gpio-license: BSD3-license-file: LICENSE-author: Tyler Olson-maintainer: tydotg@gmail.com-copyright: 2016 Tyler Olson-category: Hardware-build-type: Simple-cabal-version: >=1.10+-- This file has been generated from package.yaml by hpack version 0.14.0.+--+-- see: https://github.com/sol/hpack +name: gpio+version: 0.1.0.3+synopsis: Haskell GPIO interface, designed specifically for the RaspberryPi.+description: Please see README.md+category: Hardware+homepage: http://github.com/tgolson/gpio+author: Tyler Olson+maintainer: tydotg@gmail.com+copyright: 2016 Tyler Olson+license: BSD3+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10 +extra-source-files:+ README.md+ library- hs-source-dirs: src/lib- ghc-options: -Wall- default-language: Haskell2010- exposed-modules: System.GPIO, System.GPIO.Types- default-extensions:- OverloadedStrings NoImplicitPrelude LambdaCase- StandaloneDeriving FlexibleContexts ScopedTypeVariables- KindSignatures DataKinds GADTs+ hs-source-dirs:+ lib+ default-extensions: DataKinds DeriveGeneric FlexibleContexts FlexibleInstances GADTs KindSignatures LambdaCase OverloadedStrings ScopedTypeVariables StandaloneDeriving+ ghc-options: -Wall build-depends:- base >= 4.5 && < 5,- basic-prelude,- monad-control,- string-conversions+ base >= 4.7 && < 5+ , directory+ , exceptions+ , monad-control+ , optparse-generic+ , safe+ exposed-modules:+ System.GPIO+ other-modules:+ System.GPIO.Path+ System.GPIO.Types+ Paths_gpio+ default-language: Haskell2010 executable gpio- hs-source-dirs: src/gpio- ghc-options: -Wall- default-language: Haskell2010- main-is: Main.hs+ main-is: Main.hs+ hs-source-dirs:+ exec+ default-extensions: DataKinds DeriveGeneric FlexibleContexts FlexibleInstances GADTs KindSignatures LambdaCase OverloadedStrings ScopedTypeVariables StandaloneDeriving+ ghc-options: -Wall build-depends:- base >= 4.5 && < 5,- gpio,- basic-prelude,- optparse-generic,- string-conversions- default-extensions:- OverloadedStrings NoImplicitPrelude LambdaCase- StandaloneDeriving FlexibleContexts ScopedTypeVariables- KindSignatures DataKinds GADTs DeriveGeneric+ base >= 4.7 && < 5+ , directory+ , exceptions+ , monad-control+ , optparse-generic+ , safe+ , gpio+ default-language: Haskell2010
+ lib/System/GPIO.hs view
@@ -0,0 +1,112 @@+module System.GPIO+ -- Re-exported types+ ( Pin(..)+ , fromInt+ , ActivePin+ , Value(..)+ , Direction++ -- Exported API+ , initReaderPin+ , initWriterPin+ , readPin+ , writePin+ , reattachToReaderPin+ , reattachToWriterPin+ , closePin+ ) where++import Control.Exception (SomeException (..))+import Control.Monad+import Control.Monad.Catch+import Control.Monad.IO.Class+import Data.Maybe+import Safe+import System.Directory++import System.GPIO.Path+import System.GPIO.Types+++data PinException+ = InitPinException Pin String+ | SetDirectionException Pin Direction String+ | ReadPinException Pin String+ | WritePinException Pin Value String+ | ReattachPinException Pin String+ | ClosePinException Pin String+ deriving (Show)++instance Exception PinException+++initReaderPin :: (MonadCatch m, MonadIO m) => Pin -> m (ActivePin 'In)+initReaderPin = initPin . ReaderPin++initWriterPin :: (MonadCatch m, MonadIO m) => Pin -> m (ActivePin 'Out)+initWriterPin = initPin . WriterPin++initPin :: (MonadCatch m, MonadIO m) => ActivePin a -> m (ActivePin a)+initPin pin = do+ withVerboseError (InitPinException (unpin pin)) $+ writeFileM exportPath (toData $ unpin pin)++ withVerboseError (SetDirectionException (unpin pin) (direction pin)) $+ writeFileM (directionPath $ unpin pin) (toData (direction pin))++ return pin+++readPin :: (MonadCatch m, MonadIO m) => ActivePin a -> m Value+readPin pin = do+ x <- readFirstLine $ valuePath (unpin pin)++ case fromData x of+ Right v -> return v+ Left e -> throwM $ ReadPinException (unpin pin) e+++writePin :: (MonadCatch m, MonadIO m) => Value -> ActivePin 'Out -> m ()+writePin value pin = withVerboseError (WritePinException (unpin pin) value)+ $ writeFileM (valuePath $ unpin pin) (toData value)+++-- Get an active pin from a pin, preserving the invariants required when a pin is initialized.+-- Useful for CLI type commands where pointers to an active pin can be lost between calls.+reattachToReaderPin :: (MonadCatch m, MonadIO m) => Pin -> m (ActivePin 'In)+reattachToReaderPin = reattachToPin . ReaderPin++reattachToWriterPin :: (MonadCatch m, MonadIO m) => Pin -> m (ActivePin 'Out)+reattachToWriterPin = reattachToPin . WriterPin++reattachToPin :: (MonadCatch m, MonadIO m) => ActivePin a -> m (ActivePin a)+reattachToPin pin = do+ let err = ReattachPinException (unpin pin)++ exists <- liftIO $ doesFileExist (directionPath (unpin pin))+ unless exists $ throwM (err "Pin was never initialized")++ v <- fromData <$> readFirstLine (directionPath (unpin pin))+ dir <- either (throwM . err) return v++ unless (dir == direction pin) $ throwM (err "Attempting to reattach to pin in wrong direction")++ return pin+++closePin :: (MonadCatch m, MonadIO m) => ActivePin a -> m ()+closePin pin = withVerboseError (ClosePinException (unpin pin))+ $ writeFileM unexportPath (toData $ unpin pin)+++withVerboseError :: MonadCatch m => (String -> PinException) -> m () -> m ()+withVerboseError pinException = handle $ \(e :: SomeException) -> throwM $ pinException (show e)++writeFileM :: MonadIO m => FilePath -> String -> m ()+writeFileM fp = liftIO . writeFile fp++readFileM :: MonadIO m => FilePath -> m String+readFileM = liftIO . readFile++readFirstLine :: MonadIO m => FilePath -> m String+readFirstLine = fmap (fromMaybe mempty . headMay . lines) . readFileM
+ lib/System/GPIO/Path.hs view
@@ -0,0 +1,31 @@+module System.GPIO.Path+ ( basePath+ , exportPath+ , unexportPath+ , pinPath+ , valuePath+ , directionPath+ ) where++import Data.Monoid ((<>))++import System.GPIO.Types+++basePath :: FilePath+basePath = "/sys/class/gpio"++exportPath :: FilePath+exportPath = basePath <> "/export"++unexportPath :: FilePath+unexportPath = basePath <> "/unexport"++pinPath :: Pin -> FilePath+pinPath pin = basePath <> "/gpio" <> toPath pin++valuePath :: Pin -> FilePath+valuePath pin = pinPath pin <> "/value"++directionPath :: Pin -> FilePath+directionPath pin = pinPath pin <> "/direction"
+ lib/System/GPIO/Types.hs view
@@ -0,0 +1,82 @@+module System.GPIO.Types+ ( Pin(..)+ , pinNum+ , fromInt+ , unpin+ , direction+ , ActivePin(..)+ , Direction(..)+ , Value(..)+ , toPath+ , toData+ , fromData+ ) where+++import Data.Monoid ((<>))+import Safe+++-- Available pins and ordering of the pin numbers corresponds to chart found here:+-- https://www.raspberrypi.org/documentation/usage/gpio-plus-and-raspi2/+data Pin+ = P2 | P3 | P4 | P17 | P27 | P22 | P10 | P9 | P11 | P5 | P6 | P13 | P19 | P26+ | P14 | P15 | P18 | P23 | P24 | P25 | P8 | P7 | P12 | P16 | P20 | P21+ deriving (Eq, Read, Show, Enum)++instance ToPath Pin where toPath = show . pinNum+instance ToData Pin where toData = toPath++pinNum :: Pin -> Int+pinNum = read . tail . show++fromInt :: Int -> Maybe Pin+fromInt x = readMay ("P" ++ show x)++data ActivePin (a :: Direction) where+ ReaderPin :: Pin -> ActivePin 'In+ WriterPin :: Pin -> ActivePin 'Out++deriving instance Show (ActivePin a)++unpin :: ActivePin a -> Pin+unpin = \case ReaderPin p -> p+ WriterPin p -> p++direction :: ActivePin a -> Direction+direction = \case ReaderPin _ -> In+ WriterPin _ -> Out+++data Direction = In | Out deriving (Eq, Show)++instance ToData Direction where+ toData = \case In -> "in"+ Out -> "out"++instance FromData Direction where+ fromData = \case "in" -> Right In+ "out" -> Right Out+ x -> Left ("Cannot parse \"Direction\" from \"" <> x <> "\"")+++data Value = HI | LO deriving (Eq, Show)++instance ToData Value where+ toData = \case HI -> "1"+ LO -> "0"++instance FromData Value where+ fromData = \case "1" -> Right HI+ "0" -> Right LO+ x -> Left ("Cannot parse \"Value\" from \"" <> x <> "\"")+++class ToPath a where+ toPath :: a -> FilePath++class ToData a where+ toData :: a -> String++class FromData a where+ fromData :: String -> Either String a
− src/gpio/Main.hs
@@ -1,57 +0,0 @@-module Main (main) where--import BasicPrelude-import Data.String.Conversions-import Options.Generic--import System.GPIO-import System.GPIO.Types--data InputCommand- = Export Int String- | Unexport Int- | Read Int- | Write Int String- deriving (Generic, Show)--instance ParseRecord InputCommand---- optparse-generic has trouble with nested types--- so we can't make 'Command' a parse record--- Instead we use RawCommand for the generic instance, then--- convert to the actual 'Command' type.-data Command- = CmdExport Pin Dir- | CmdUnexport Pin- | CmdRead Pin- | CmdWrite Pin Value- deriving (Generic, Show)---toCommand :: InputCommand -> Either Text Command-toCommand =- \case Export i "in" -> (`CmdExport` In) <$> parsePin i- Export i "out" -> (`CmdExport` Out) <$> parsePin i- Export _ _ -> Left "Usage error: use 'in' or 'out' for export direction."- Unexport i -> CmdUnexport <$> parsePin i- Read i -> CmdRead <$> parsePin i- Write i "hi" -> (`CmdWrite` HI) <$> parsePin i- Write i "lo" -> (`CmdWrite` LO) <$> parsePin i- Write _ _ -> Left "Usage error: use 'hi' or 'lo' for write value."- where- parsePin i = case fromInt i of- Nothing -> Left ("Usage error: could not parse pin value from integer: " ++ show i)- Just p -> Right p--main :: IO ()-main =- toCommand <$> getRecord "GPIO" >>= \case- Left e -> error (convertString e)- Right cmd -> case cmd of- CmdExport p In -> void (initReaderPin p)- CmdExport p Out -> void (initWriterPin p)- -- Note: chose a random pin type when closing the pin- -- the type doesn't actually matter...- CmdUnexport p -> closePin (ReaderPin p)- CmdRead p -> readPin (ReaderPin p) >>= print- CmdWrite p v -> writePin (WriterPin p) v
− src/lib/System/GPIO.hs
@@ -1,103 +0,0 @@-module System.GPIO- -- Re-exported types- ( Pin(..)- , ActivePin- , Value(..)- , Dir(..)-- -- Exported API- , initReaderPin- , initWriterPin- , readPin- , writePin- , closePin- ) where--import BasicPrelude-import Control.Monad.Trans.Control-import Data.String.Conversions--import System.GPIO.Types----- Exported API ------------------------------------------------------------------initReaderPin :: (MonadBaseControl IO m, MonadIO m) => Pin -> m (ActivePin 'In)-initReaderPin p = initPin activePin >> return activePin- where activePin = ReaderPin p--initWriterPin :: (MonadBaseControl IO m, MonadIO m) => Pin -> m (ActivePin 'Out)-initWriterPin p = initPin activePin >> return activePin- where activePin = WriterPin p--readPin :: (MonadBaseControl IO m, MonadIO m) => ActivePin a -> m Value-readPin p = do- x <- liftIO $ readFile (valuePath $ pin p)-- -- TODO: handle errors after cleaning this up...- case fromText (runLineHack x) of- Right v -> return v- Left e -> error $ convertString $- "Error reading value file for \"" <> show p <> "\": " <> e- where- -- Note: too lazy to properly handle new lines in the value files- -- it looks like the gpio interface appends newlines- -- so file is read as "1\n"- -- TODO: handle correctly, maybe use hGetChar or something...- runLineHack t = case lines t of- [] -> error "Error: runLineHack failed us."- (x:_) -> x--writePin :: (MonadBaseControl IO m, MonadIO m) => ActivePin 'Out -> Value -> m ()-writePin p v = withVerboseError- ("Error writing value \"" <> show v <> "\" to " <> show p <> ".")- $ liftIO (writeFile (valuePath $ pin p) (toText v))--closePin :: (MonadBaseControl IO m, MonadIO m) => ActivePin a -> m ()-closePin p = withVerboseError- ("Error closing " <> show p <> ". Was this pin already closed?")- $ liftIO (writeFile unexportPath (pinNumT $ pin p))----- Internal Pin Utils ------------------------------------------------------------initPin :: (MonadBaseControl IO m, MonadIO m) => ActivePin a -> m ()-initPin p = do- let exportErrorMsg = "Error initializing " <> show p <> ". Was this pin already initialized?"- setDirErrorMsg = "Error setting direction for " <> show p <> "."- withVerboseError exportErrorMsg export- withVerboseError setDirErrorMsg setDirection- where- export = liftIO $ writeFile exportPath (pinNumT $ pin p)- setDirection = liftIO $ writeFile (directionPath $ pin p) (toText dir)- dir :: Dir- dir = case p of ReaderPin _ -> In- WriterPin _ -> Out---withVerboseError :: (MonadBaseControl IO m) => Text -> m () -> m ()-withVerboseError msg = handle handleError- where- handleError :: SomeException -> m ()- handleError e = error $ convertString (msg <> "\nRaw Error: " <> show e)----- Path Utils --------------------------------------------------------------------basePath :: FilePath-basePath = "/sys/class/gpio"--exportPath :: FilePath-exportPath = basePath <> "/export"--unexportPath :: FilePath-unexportPath = basePath <> "/unexport"--pinPath :: Pin -> FilePath-pinPath p = basePath <> "/gpio" <> convertString (pinNumT p)--valuePath :: Pin -> FilePath-valuePath p = pinPath p <> "/value"--directionPath :: Pin -> FilePath-directionPath p = pinPath p <> "/direction"
− src/lib/System/GPIO/Types.hs
@@ -1,77 +0,0 @@-module System.GPIO.Types- ( Pin(..)- , pinNum- , pinNumT- , fromInt- , ActivePin(..)- , pin- , Dir(..)- , Value(..)- , fromText- , toText- ) where--import BasicPrelude-import Data.String.Conversions----- Core Types --------------------------------------------------------------------data Pin = P18 | P23 | P24 | P25 deriving (Eq, Show, Enum)--pinNumDict :: [(Pin, Int)]-pinNumDict = [ (P18, 18)- , (P23, 23)- , (P24, 24)- , (P25, 25)- ]--pinNum :: Pin -> Int-pinNum p = fromMaybe unexpectedError (lookup p pinNumDict)- where- unexpectedError = error $ convertString- ("Unexpected error. " ++ show p ++ " not defined in 'pinNumDict'")--pinNumT :: Pin -> Text-pinNumT = show . pinNum--fromInt :: Int -> Maybe Pin-fromInt i = lookup i (swap <$> pinNumDict)--data ActivePin (a :: Dir) where- ReaderPin :: Pin -> ActivePin 'In- WriterPin :: Pin -> ActivePin 'Out--deriving instance Show (ActivePin a)--pin :: ActivePin a -> Pin-pin = \case ReaderPin p -> p- WriterPin p -> p--data Dir = In | Out deriving (Show)--instance ToText Dir where- toText = \case In -> "in"- Out -> "out"-instance FromText Dir where- fromText = \case "in" -> Right In- "out" -> Right Out- x -> Left ("Cannot parse \"Dir\" from \"" <> x <> "\"")--data Value = HI | LO deriving (Show)--instance ToText Value where- toText = \case HI -> "1"- LO -> "0"-instance FromText Value where- fromText = \case "1" -> Right HI- "0" -> Right LO- x -> Left ("Cannot parse \"Value\" from \"" <> x <> "\"")---- We are going to do a lot of reading/writing to files w/ custom data types.--- Create a small interface to keep conversions consistent.-class ToText a where- toText :: a -> Text--class FromText a where- fromText :: Text -> Either Text a