hpio-0.9.0.3: src/System/GPIO/Linux/Sysfs/Monad.hs
{-|
Module : System.GPIO.Linux.Sysfs.Monad
Description : Monads for Linux @sysfs@ GPIO operations
Copyright : (c) 2017, Quixoftic, LLC
License : BSD3
Maintainer : Drew Hess <dhess-src@quixoftic.com>
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