{-| Module : System.GPIO.Linux.Sysfs.Monad Description : Monads for Linux @sysfs@ GPIO operations Copyright : (c) 2017, Quixoftic, LLC License : BSD3 Maintainer : Drew Hess Stability : experimental Portability : non-portable Monad type classes and instances for Linux @sysfs@ GPIO operations. -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE Trustworthy #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module System.GPIO.Linux.Sysfs.Monad ( -- * MonadSysfs class MonadSysfs(..) -- * GPIO via @sysfs@ , PinDescriptor(..) , SysfsGpioT(..) -- * Convenient constraint synonyms for 'MonadSysfs' signatures. , CatchSysfsM , ThrowSysfsM , ThrowCatchSysfsM -- * Low-level @sysfs@ GPIO actions. -- -- If you wish, you can bypass the portable GPIO computation -- layer provided by 'MonadGpio' and program directly to the -- Linux @sysfs@ GPIO interface in the 'MonadSysfs' monad. -- This requires only one level of abstraction (choosing a -- 'MonadSysfs' instance) rather than two (both a -- 'MonadSysfs' instance /and/ the 'SysfsGpioT' 'MonadGpio' -- instance). , sysfsIsPresent , availablePins , pinIsExported , exportPin , exportPinChecked , unexportPin , unexportPinChecked , pinHasDirection , readPinDirection , writePinDirection , writePinDirectionWithValue , readPinValue , pollPinValue , pollPinValueTimeout , writePinValue , pinHasEdge , readPinEdge , writePinEdge , readPinActiveLow , writePinActiveLow ) where import Protolude hiding (readFile, writeFile) import Control.Monad.Base (MonadBase) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, catchIOError, throwM) import Control.Monad.Catch.Pure (CatchT) import Control.Monad.Cont (MonadCont, ContT) import Control.Monad.Fix (MonadFix) import Control.Monad.Logger (LoggingT, MonadLogger, MonadLoggerIO, NoLoggingT) import Control.Monad.RWS (MonadRWS) import Control.Monad.Trans.Class (MonadTrans) import Control.Monad.Trans.Control (ComposeSt, MonadBaseControl(..), MonadTransControl(..), defaultLiftBaseWith, defaultRestoreM) import Control.Monad.Trans.Identity (IdentityT) import "transformers" Control.Monad.Trans.List (ListT) import Control.Monad.Trans.Maybe (MaybeT) import qualified Control.Monad.Trans.RWS.Lazy as LazyRWS (RWST) import qualified Control.Monad.Trans.RWS.Strict as StrictRWS (RWST) import qualified Control.Monad.Trans.State.Lazy as LazyState (StateT) import qualified Control.Monad.Trans.State.Strict as StrictState (StateT) import qualified Control.Monad.Trans.Writer.Lazy as LazyWriter (WriterT) import qualified Control.Monad.Trans.Writer.Strict as StrictWriter (WriterT) import Control.Monad.Writer (MonadWriter) import qualified Data.ByteString.Char8 as C8 (readInt) import qualified Data.Set as Set (empty, fromList) import Foreign.C.Types (CInt(..)) import qualified GHC.IO.Exception as IO (IOErrorType(..)) import System.FilePath ((), takeFileName) import System.IO.Error (IOError, ioeGetErrorType, isAlreadyInUseError, isDoesNotExistError, isPermissionError) import System.GPIO.Linux.Sysfs.Types (SysfsEdge(..), SysfsException(..), toPinInterruptMode, toSysfsEdge) import System.GPIO.Linux.Sysfs.Util (intToBS, pinActiveLowFileName, pinDirectionFileName, pinEdgeFileName, pinValueFileName, pinDirName, activeLowToBS, pinDirectionToBS, pinDirectionValueToBS, sysfsEdgeToBS, pinValueToBS, sysfsPath, exportFileName, unexportFileName) import System.GPIO.Monad (MonadGpio(..), withPin) import System.GPIO.Types (Pin(..), PinActiveLevel(..), PinCapabilities(..), PinDirection(..), PinInputMode(..), PinOutputMode(..), PinValue(..), invertValue) -- | A type class for monads which implement (or mock) low-level Linux -- @sysfs@ GPIO operations. class (Monad m) => MonadSysfs m where -- | Equivalent to 'System.Directory.doesDirectoryExist'. doesDirectoryExist :: FilePath -> m Bool -- | Equivalent to 'System.Directory.doesFileExist'. doesFileExist :: FilePath -> m Bool -- | Equivalent to 'System.Directory.getDirectoryContents'. getDirectoryContents :: FilePath -> m [FilePath] -- | Equivalent to 'Data.ByteString.readFile'. readFile :: FilePath -> m ByteString -- | Equivalent to 'Data.ByteString.writeFile'. writeFile :: FilePath -> ByteString -> m () -- | @sysfs@ control files which are global shared resources may be -- written simultaneously by multiple threads. This is fine -- -- @sysfs@ can handle this -- but Haskell's -- 'Data.ByteString.writeFile' cannot, as it locks the file and -- prevents multiple writers. We don't want this behavior, so we use -- low-level operations to get around it. unlockedWriteFile :: FilePath -> ByteString -> m () -- | Poll a @sysfs@ file for reading, as in POSIX.1-2001 @poll(2)@. -- -- Note that the implementation of this action is only guaranteed to -- work for @sysfs@ files, which have a peculiar way of signaling -- readiness for reads. Do not use it for any other purpose. pollFile :: FilePath -> Int -> m CInt default doesDirectoryExist :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> m Bool default doesFileExist :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> m Bool default getDirectoryContents :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> m [FilePath] default readFile :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> m ByteString default writeFile :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> ByteString -> m () default unlockedWriteFile :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> ByteString -> m () default pollFile :: (MonadTrans t, MonadSysfs m', t m' ~ m) => FilePath -> Int -> m CInt doesDirectoryExist = lift . doesDirectoryExist {-# INLINE doesDirectoryExist #-} doesFileExist = lift . doesFileExist {-# INLINE doesFileExist #-} getDirectoryContents = lift . getDirectoryContents {-# INLINE getDirectoryContents #-} readFile = lift . readFile {-# INLINE readFile #-} writeFile fn bs = lift $ writeFile fn bs {-# INLINE writeFile #-} unlockedWriteFile fn bs = lift $ unlockedWriteFile fn bs {-# INLINE unlockedWriteFile #-} pollFile fn timeout = lift $ pollFile fn timeout {-# INLINE pollFile #-} instance (MonadSysfs m) => MonadSysfs (IdentityT m) instance (MonadSysfs m) => MonadSysfs (ContT r m) instance (MonadSysfs m) => MonadSysfs (CatchT m) instance (MonadSysfs m) => MonadSysfs (ExceptT e m) instance (MonadSysfs m) => MonadSysfs (ListT m) instance (MonadSysfs m) => MonadSysfs (MaybeT m) instance (MonadSysfs m) => MonadSysfs (ReaderT r m) instance (MonadSysfs m, Monoid w) => MonadSysfs (LazyRWS.RWST r w s m) instance (MonadSysfs m, Monoid w) => MonadSysfs (StrictRWS.RWST r w s m) instance (MonadSysfs m) => MonadSysfs (LazyState.StateT s m) instance (MonadSysfs m) => MonadSysfs (StrictState.StateT s m) instance (MonadSysfs m, Monoid w) => MonadSysfs (LazyWriter.WriterT w m) instance (MonadSysfs m, Monoid w) => MonadSysfs (StrictWriter.WriterT w m) instance (MonadSysfs m) => MonadSysfs (LoggingT m) instance (MonadSysfs m) => MonadSysfs (NoLoggingT m) -- | The @sysfs@ pin handle type. Currently it's just a newtype -- wrapper around a 'Pin'. The constructor is exported for -- convenience, but note that the implementation may change in future -- versions of the package. newtype PinDescriptor = PinDescriptor {_pin :: Pin} deriving (Show,Eq,Ord) -- | An instance of 'MonadGpio' which translates actions in that monad -- to operations on Linux's native @sysfs@ GPIO interface. newtype SysfsGpioT m a = SysfsGpioT { runSysfsGpioT :: m a } deriving ( Functor , Alternative , Applicative , Monad , MonadBase b , MonadFix , MonadPlus , MonadThrow , MonadCatch , MonadMask , MonadCont , MonadIO , MonadReader r , MonadError e , MonadWriter w , MonadState s , MonadRWS r w s , MonadLogger , MonadLoggerIO ) instance MonadTrans SysfsGpioT where lift = SysfsGpioT instance MonadBaseControl b m => MonadBaseControl b (SysfsGpioT m) where type StM (SysfsGpioT m) a = ComposeSt SysfsGpioT m a liftBaseWith = defaultLiftBaseWith restoreM = defaultRestoreM {-# INLINABLE liftBaseWith #-} {-# INLINABLE restoreM #-} instance MonadTransControl SysfsGpioT where type StT SysfsGpioT a = a liftWith f = SysfsGpioT $ f runSysfsGpioT restoreT = SysfsGpioT {-# INLINABLE liftWith #-} {-# INLINABLE restoreT #-} type CatchSysfsM m = (Functor m, MonadCatch m, MonadSysfs m) type ThrowSysfsM m = (Functor m, MonadThrow m, MonadSysfs m) type ThrowCatchSysfsM m = (Functor m, MonadThrow m, MonadCatch m, MonadSysfs m) instance (MonadMask m, ThrowCatchSysfsM m) => MonadGpio PinDescriptor (SysfsGpioT m) where pins = lift sysfsIsPresent >>= \case False -> return [] True -> lift availablePins -- The @sysfs@ GPIO interface is particularly information-poor. It -- is not currently possible, in a hardware-independent way, to -- determine which particular input and output modes a pin supports, -- for example. -- -- For input pins, therefore, we can only claim 'InputDefault' -- support. However, for output pins, it's possible to emulate both -- 'OutputOpenDrain' and 'OutputOpenSource' modes by switching the -- pin into input mode for 'High' (in the case of 'OutputOpenDrain') -- or 'Low' ('OutputOpenSource') values. We do not currently support -- this, but it's a planned feature. -- -- If a pin has no @direction@ attribute, it means there is no -- hardware-independent way to determine its hard-wired direction -- via @sysfs@. That means there's no practical way to use it with -- the cross-platform DSL, so in this case we simply report the pin -- as having no capabilities. pinCapabilities p = lift sysfsIsPresent >>= \case False -> throwM SysfsNotPresent True -> withPin p $ \_ -> do hasDir <- lift $ pinHasDirection p hasEdge <- lift $ pinHasEdge p if hasDir then return $ PinCapabilities (Set.fromList [InputDefault]) (Set.fromList [OutputDefault]) hasEdge else return $ PinCapabilities Set.empty Set.empty False openPin p = lift sysfsIsPresent >>= \case False -> throwM SysfsNotPresent True -> do lift $ exportPin p return $ PinDescriptor p closePin (PinDescriptor p) = lift $ unexportPin p getPinDirection (PinDescriptor p) = lift $ readPinDirection p getPinInputMode (PinDescriptor p) = do dir <- lift $ readPinDirection p if dir == In then return InputDefault else throwM $ InvalidOperation p setPinInputMode (PinDescriptor p) mode = if mode == InputDefault then lift $ writePinDirection p In else throwM $ UnsupportedInputMode mode p getPinOutputMode (PinDescriptor p) = do dir <- lift $ readPinDirection p if dir == Out then return OutputDefault else throwM $ InvalidOperation p setPinOutputMode (PinDescriptor p) mode v = if mode == OutputDefault then lift $ writePinDirectionWithValue p v else throwM $ UnsupportedOutputMode mode p readPin (PinDescriptor p) = lift $ readPinValue p pollPin (PinDescriptor p) = lift $ pollPinValue p pollPinTimeout (PinDescriptor p) timeout = lift $ pollPinValueTimeout p timeout writePin (PinDescriptor p) v = lift $ writePinValue p v togglePin h = do val <- readPin h let newVal = invertValue val void $ writePin h newVal return newVal getPinInterruptMode (PinDescriptor p) = do edge <- lift $ readPinEdge p return $ toPinInterruptMode edge setPinInterruptMode (PinDescriptor p) mode = lift $ writePinEdge p $ toSysfsEdge mode getPinActiveLevel (PinDescriptor p) = do activeLow <- lift $ readPinActiveLow p return $ activeLowToActiveLevel activeLow setPinActiveLevel (PinDescriptor p) l = lift $ writePinActiveLow p $ activeLevelToActiveLow l togglePinActiveLevel (PinDescriptor p) = do toggled <- not <$> lift (readPinActiveLow p) lift $ writePinActiveLow p toggled return $ activeLowToActiveLevel toggled activeLevelToActiveLow :: PinActiveLevel -> Bool activeLevelToActiveLow ActiveLow = True activeLevelToActiveLow ActiveHigh = False activeLowToActiveLevel :: Bool -> PinActiveLevel activeLowToActiveLevel False = ActiveHigh activeLowToActiveLevel True = ActiveLow -- | Test whether the @sysfs@ GPIO filesystem is available. sysfsIsPresent :: (MonadSysfs m) => m Bool sysfsIsPresent = doesDirectoryExist sysfsPath -- | Test whether the pin is already exported. pinIsExported :: (MonadSysfs m) => Pin -> m Bool pinIsExported = doesDirectoryExist . pinDirName -- | Export the given pin. -- -- Note that, if the pin is already exported, this is not an error; in -- this situation, the pin remains exported and its state unchanged. exportPin :: (CatchSysfsM m) => Pin -> m () exportPin pin@(Pin n) = catchIOError (unlockedWriteFile exportFileName (intToBS n)) mapIOError where mapIOError :: (MonadThrow m) => IOError -> m () mapIOError e | isAlreadyInUseError e = return () | isInvalidArgumentError e = throwM $ InvalidPin pin | isPermissionError e = throwM $ PermissionDenied pin | otherwise = throwM e -- | Export the given pin. -- -- Note that, unlike 'exportPin', it's an error to call this action to -- export a pin that's already been exported. This is the standard -- Linux @sysfs@ GPIO behavior. exportPinChecked :: (CatchSysfsM m) => Pin -> m () exportPinChecked pin@(Pin n) = catchIOError (unlockedWriteFile exportFileName (intToBS n)) mapIOError where mapIOError :: (MonadThrow m) => IOError -> m () mapIOError e | isAlreadyInUseError e = throwM $ AlreadyExported pin | isInvalidArgumentError e = throwM $ InvalidPin pin | isPermissionError e = throwM $ PermissionDenied pin | otherwise = throwM e -- | Unexport the given pin. -- -- Note that, if the pin is already unexported or cannot be -- unexported, this is not an error. In this situation, the pin -- remains exported and its state unchanged. unexportPin :: (CatchSysfsM m) => Pin -> m () unexportPin pin@(Pin n) = catchIOError (unlockedWriteFile unexportFileName (intToBS n)) mapIOError where mapIOError :: (MonadThrow m) => IOError -> m () mapIOError e | isInvalidArgumentError e = return () | isPermissionError e = throwM $ PermissionDenied pin | otherwise = throwM e -- | Unexport the given pin. -- -- Note that, unlike 'unexportPin', it is an error to call this action -- if the pin is not currently exported. This is the standard Linux -- @sysfs@ GPIO behavior. unexportPinChecked :: (CatchSysfsM m) => Pin -> m () unexportPinChecked pin@(Pin n) = catchIOError (unlockedWriteFile unexportFileName (intToBS n)) mapIOError where mapIOError :: (MonadThrow m) => IOError -> m () mapIOError e | isInvalidArgumentError e = throwM $ NotExported pin | isPermissionError e = throwM $ PermissionDenied pin | otherwise = throwM e -- | Test whether the pin's direction can be set via the @sysfs@ GPIO -- filesystem. (Some pins have a hard-wired direction, in which case -- their direction must be determined by some other mechanism, as the -- @direction@ attribute does not exist for such pins.) pinHasDirection :: (ThrowSysfsM m) => Pin -> m Bool pinHasDirection p = do exported <- pinIsExported p if exported then doesFileExist (pinDirectionFileName p) else throwM $ NotExported p -- | Read the pin's direction. -- -- It is an error to call this action if the pin has no @direction@ -- attribute. readPinDirection :: (ThrowCatchSysfsM m) => Pin -> m PinDirection readPinDirection p = catchIOError (readFile (pinDirectionFileName p) >>= \case "in\n" -> return In "out\n" -> return Out x -> throwM $ UnexpectedDirection p (decodeUtf8 x)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m PinDirection mapIOError e | isDoesNotExistError e = do exported <- pinIsExported p if exported then throwM $ NoDirectionAttribute p else throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Set the pin's direction. -- -- It is an error to call this action if the pin has no @direction@ -- attribute. -- -- Note that, in Linux @sysfs@ GPIO, changing a pin's direction to -- @out@ will also set its /physical/ signal level to @low@. -- -- NB: in Linux @sysfs@, if an input pin is cofigured for edge- or -- level-triggered reads, it's an error to set its direction to @out@. -- However, this action will handle that case gracefully by setting -- the pin's @edge@ attribute to @none@ before setting the pin's -- direction to @out@. writePinDirection :: (CatchSysfsM m) => Pin -> PinDirection -> m () writePinDirection p In = writeDirection p (pinDirectionToBS In) writePinDirection p Out = do resetEdge p writeDirection p (pinDirectionToBS Out) -- | Pins whose direction can be set may be configured for output by -- writing a 'PinValue' to their @direction@ attribute, such that the -- given value will be driven on the pin as soon as it's configured -- for output. This enables glitch-free output configuration, assuming -- the pin is currently configured for input, or some kind of -- tri-stated or floating high-impedance mode. -- -- It is an error to call this action if the pin has no @direction@ -- attribute. -- -- NB: for some unfathomable reason, writing @high@ or @low@ to a -- pin's @direction@ attribute sets its /physical/ signal level; i.e., -- it ignores the value of the pin's @active_low@ attribute. Contrast -- this behavior with the behavior of writing to the pin's @value@ -- attribute, which respects the value of the pin's @active_low@ -- attribute and sets the pin's /logical/ signal level. -- -- Rather than slavishly following the Linux @sysfs@ GPIO spec, we -- choose to be consistent by taking into account the pin's active -- level when writing the @direction@ attribute. In other words, the -- 'PinValue' argument to this action is the /logical/ signal level -- that will be set on the pin. If you're using this action to program -- directly to the Linux @sysfs@ GPIO interface and expecting things -- to behave as they do with raw @sysfs@ GPIO operations, keep this in -- mind! writePinDirectionWithValue :: (CatchSysfsM m) => Pin -> PinValue -> m () writePinDirectionWithValue p v = do activeLow <- readPinActiveLow p let f = if activeLow then invertValue else identity resetEdge p writeDirection p (pinDirectionValueToBS $ f v) resetEdge :: (CatchSysfsM m) => Pin -> m () resetEdge p = maybeReadPinEdge >>= \case Nothing -> return () Just None -> return () _ -> writePinEdge p None where maybeReadPinEdge :: (CatchSysfsM m) => m (Maybe SysfsEdge) maybeReadPinEdge = pinHasEdge p >>= \case False -> return Nothing True -> Just <$> readPinEdge p writeDirection :: (CatchSysfsM m) => Pin -> ByteString -> m () writeDirection p bs = catchIOError (writeFile (pinDirectionFileName p) bs) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m () mapIOError e | isDoesNotExistError e = do exported <- pinIsExported p if exported then throwM $ NoDirectionAttribute p else throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Read the pin's signal level. -- -- Note that this action never blocks, regardless of the pin's @edge@ -- attribute setting. readPinValue :: (ThrowCatchSysfsM m) => Pin -> m PinValue readPinValue p = catchIOError (readFile (pinValueFileName p) >>= \case "0\n" -> return Low "1\n" -> return High x -> throwM $ UnexpectedValue p (decodeUtf8 x)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m PinValue mapIOError e | isDoesNotExistError e = throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | A blocking version of 'readPinValue'. The current thread will -- block until an event occurs on the pin as specified by the pin's -- current @edge@ attribute setting. -- -- If the pin has no @edge@ attribute, then this action's behavior is -- undefined. (Most likely, it will block indefinitely.) pollPinValue :: (ThrowCatchSysfsM m) => Pin -> m PinValue pollPinValue p = pollPinValueTimeout p (-1) >>= \case Just v -> return v -- 'Nothing' can only occur when the poll has timed out, but the -- (-1) timeout value above means the poll must either wait -- forever or fail; so this indicates a major problem. Nothing -> throwM $ InternalError "pollPinValue timed out, and it should not have. Please file a bug at https://github.com/quixoftic/gpio" -- | Same as 'pollPinValue', except that a timeout value, -- specified in microseconds, is provided. If no event occurs before -- the timeout expires, this action returns 'Nothing'; otherwise, it -- returns the pin's value wrapped in a 'Just'. -- -- If the timeout value is negative, this action behaves just like -- 'pollPinValue'. -- -- When specifying a timeout value, be careful not to exceed -- 'maxBound'. -- -- If the pin has no @edge@ attribute, then this action's behavior is -- undefined. (Most likely, it will time out after the specified delay -- and return 'Nothing'.) -- -- NB: the curent implementation of this action limits the timeout -- precision to 1 millisecond, rather than 1 microsecond as the -- timeout parameter implies. pollPinValueTimeout :: (ThrowCatchSysfsM m) => Pin -> Int -> m (Maybe PinValue) pollPinValueTimeout p timeout = catchIOError (do pollResult <- pollFile (pinValueFileName p) timeout if pollResult > 0 then Just <$> readPinValue p else return Nothing) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m (Maybe PinValue) mapIOError e | isDoesNotExistError e = throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Set the pin's signal level. -- -- It is an error to call this action if the pin is configured as an -- input pin. writePinValue :: (CatchSysfsM m) => Pin -> PinValue -> m () writePinValue p v = catchIOError (writeFile (pinValueFileName p) (pinValueToBS v)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m () mapIOError e | isDoesNotExistError e = throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Test whether the pin has an @edge@ attribute, i.e., whether it -- can be configured for edge- or level-triggered interrupts. pinHasEdge :: (ThrowSysfsM m) => Pin -> m Bool pinHasEdge p = do exported <- pinIsExported p if exported then doesFileExist (pinEdgeFileName p) else throwM $ NotExported p -- | Read the pin's @edge@ attribute. -- -- It is an error to call this action when the pin has no @edge@ -- attribute. readPinEdge :: (ThrowCatchSysfsM m) => Pin -> m SysfsEdge readPinEdge p = catchIOError (readFile (pinEdgeFileName p) >>= \case "none\n" -> return None "rising\n" -> return Rising "falling\n" -> return Falling "both\n" -> return Both x -> throwM $ UnexpectedEdge p (decodeUtf8 x)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m SysfsEdge mapIOError e | isDoesNotExistError e = do exported <- pinIsExported p if exported then throwM $ NoEdgeAttribute p else throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Write the pin's @edge@ attribute. -- -- It is an error to call this action when the pin has no @edge@ -- attribute, or when the pin is configured for output. writePinEdge :: (CatchSysfsM m) => Pin -> SysfsEdge -> m () writePinEdge p v = catchIOError (writeFile (pinEdgeFileName p) (sysfsEdgeToBS v)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m () mapIOError e | isDoesNotExistError e = do exported <- pinIsExported p if exported then throwM $ NoEdgeAttribute p else throwM $ NotExported p | isInvalidArgumentError e = throwM $ InvalidOperation p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Read the pin's @active_low@ attribute. readPinActiveLow :: (ThrowCatchSysfsM m) => Pin -> m Bool readPinActiveLow p = catchIOError (readFile (pinActiveLowFileName p) >>= \case "0\n" -> return False "1\n" -> return True x -> throwM $ UnexpectedActiveLow p (decodeUtf8 x)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m Bool mapIOError e | isDoesNotExistError e = throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Write the pin's @active_low@ attribute. writePinActiveLow :: (CatchSysfsM m) => Pin -> Bool -> m () writePinActiveLow p v = catchIOError (writeFile (pinActiveLowFileName p) (activeLowToBS v)) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m () mapIOError e | isDoesNotExistError e = throwM $ NotExported p | isPermissionError e = throwM $ PermissionDenied p | otherwise = throwM e -- | Return a list of all pins that are exposed via the @sysfs@ GPIO -- filesystem. Note that the returned list may omit some pins that -- are available on the host but which, for various reasons, are not -- exposed via the @sysfs@ GPIO filesystem. availablePins :: (ThrowCatchSysfsM m) => m [Pin] availablePins = catchIOError (do sysfsEntries <- getDirectoryContents sysfsPath let sysfsContents = fmap (sysfsPath ) sysfsEntries sysfsDirectories <- filterM doesDirectoryExist sysfsContents let chipDirs = filter (isPrefixOf "gpiochip" . takeFileName) sysfsDirectories gpioPins <- mapM pinRange chipDirs return $ sort $ concat gpioPins) mapIOError where mapIOError :: (ThrowSysfsM m) => IOError -> m [Pin] mapIOError e | isDoesNotExistError e = throwM SysfsError | isPermissionError e = throwM SysfsPermissionDenied | otherwise = throwM e -- Helper actions that aren't exported. -- readIntFromFile :: (ThrowSysfsM m) => FilePath -> m Int readIntFromFile f = do contents <- readFile f case C8.readInt contents of Just (n, _) -> return n Nothing -> throwM $ UnexpectedContents f (decodeUtf8 contents) pinRange :: (ThrowSysfsM m) => FilePath -> m [Pin] pinRange chipDir = do base <- readIntFromFile (chipDir "base") ngpio <- readIntFromFile (chipDir "ngpio") if base >= 0 && ngpio > 0 then return $ fmap Pin [base .. (base + ngpio - 1)] else return [] -- IOErrorType predicates for the extended GHC.IO.Exception types -- which we use. isInvalidArgumentErrorType :: IO.IOErrorType -> Bool isInvalidArgumentErrorType IO.InvalidArgument = True isInvalidArgumentErrorType _ = False isInvalidArgumentError :: IOError -> Bool isInvalidArgumentError = isInvalidArgumentErrorType . ioeGetErrorType