packages feed

fuyu-gpio-0.1.0.0: src/Fuyu/GPIO.hs

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Fuyu.GPIO
-- Description : High-level, type-safe Haskell interface for Linux GPIO (libgpiod v2).
-- Maintainer  : BassGT
-- Stability   : experimental
-- Portability : POSIX (Linux GPIO character device interface)
--
-- This module groups functions related to GPIO chip and line handling from the
-- <https://libgpiod.readthedocs.io/en/master/core_api.html libgpiod Core API>.
-- Specifically, these functions cover GPIO Chip, GPIO line settings,
-- GPIO line configuration, GPIO line requests, and libgpiod miscellaneous interfaces.
--
module Fuyu.GPIO
  ( -- * GPIO Chip
    Chip
  , withChip
  , chipPath
  , lineOffsetFromName
  , chipFd

    -- * GPIO line definitions
  , Offset
  , pattern Offset
  , Direction
  , pattern DirAsIs
  , pattern DirInput
  , pattern DirOutput
  , Edge
  , pattern EdgeNone
  , pattern EdgeRising
  , pattern EdgeFalling
  , pattern EdgeBoth
  , Bias
  , pattern BiasAsIs
  , pattern BiasUnknown
  , pattern BiasDisabled
  , pattern BiasPullUp
  , pattern BiasPullDown
  , Drive
  , pattern PushPull
  , pattern OpenDrain
  , pattern OpenSource
  , Clock
  , pattern Monotonic
  , pattern Realtime
  , pattern Hardware
  , Value
  , pattern Active
  , pattern Inactive
  , pattern ValueError

    -- * GPIO Line Settings
  , Settings
  , withSettings
  , setDirection
  , direction
  , setEdgeDetection
  , edgeDetection
  , setBias
  , bias
  , setDrive
  , drive
  , setEventClock
  , eventClock
  , setActiveLow
  , activeLow
  , setDebouncePeriodUs
  , debouncePeriodUs
  , setOutputValue
  , outputValue
  , resetSettings

    -- * GPIO Line Config
  , Config
  , withConfig
  , addSettings
  , lineSettings
  , setOutputValues
  , numOffsets
  , configuredOffsets
  , resetConfig

    -- * GPIO Line Request
  , Request
  , RequestConfig
  , withRequest
  , lineValue
  , lineValues
  , lineValuesSubset
  , setLineValue
  , setLineValues
  , setLineValuesSubset
  , chipName
  , numLines
  , requestedOffsets
  , lineFd
  , reconfigureLines

    -- * General Utilities
  , isGPIOChip
  , gpiodAPIVersion
  ) where

import Control.Exception (bracket, throwIO)
import System.OsPath.Posix (encodeFS, decodeFS)
import qualified Data.ByteString.Char8 as C8
import qualified Data.Vector.Storable as V
import System.Posix.Types (Fd)
import qualified Fuyu.GPIO.Direct as D
import Fuyu.GPIO.Unsafe
  ( openChip
  , closeChip
  , newLineSettings
  , freeLineSettings
  , newLineConfig
  , freeLineConfig
  , requestLines
  , releaseLineRequest
  )
import Fuyu.GPIO.Exception
import Fuyu.GPIO.Types

--------------------------------------------------------------------------------
-- GPIO Chip
--------------------------------------------------------------------------------

-- | Open a GPIO chip by filesystem path (e.g. @"\/dev\/gpiochip0"@) and automatically close it when finished.
--
-- @
-- main :: IO ()
-- main = do
--   withChip "\/dev\/gpiochip0" $ \chip -> do
--     someFunc chip
-- @
withChip :: FilePath -> (Chip -> IO a) -> IO a
withChip str f = do
  path' <- encodeFS str
  bracket (openChip path') closeChip f

-- | Retrieve chip filesystem path as a 'FilePath'.
chipPath :: Chip -> IO FilePath
chipPath chip = do
  path' <- unwrapOrThrow ChipInfoFailed (D.chipPath chip)
  decodeFS path'

-- | Map a GPIO line name (e.g. "GPIO17") to its numeric 'Offset' on the chip.
lineOffsetFromName :: Chip -> String -> IO Offset
lineOffsetFromName chip name =
  unwrapOrThrow LineInfoFailed (D.chipLineOffsetFromName chip (C8.pack name))

-- | Get the underlying Linux file descriptor associated with the GPIO chip handle.
chipFd :: Chip -> IO Fd
chipFd = D.chipFd

--------------------------------------------------------------------------------
-- GPIO Line Settings
--------------------------------------------------------------------------------

-- | Allocate a new line settings object and free it automatically afterwards.
withSettings :: (Settings -> IO a) -> IO a
withSettings = bracket newLineSettings freeLineSettings

-- | Set the line direction in the settings.
setDirection :: Settings -> Direction -> IO ()
setDirection set dir = unwrapOrThrow LineSettingsSetFailed (D.lineSettingsSetDirection set dir)

-- | Get the line direction from the settings.
direction :: Settings -> IO Direction
direction = D.lineSettingsDirection

-- | Set edge detection in the settings.
setEdgeDetection :: Settings -> Edge -> IO ()
setEdgeDetection set edge = unwrapOrThrow LineSettingsSetFailed (D.lineSettingsSetEdgeDetection set edge)

-- | Get edge detection from the settings.
edgeDetection :: Settings -> IO Edge
edgeDetection = D.lineSettingsEdgeDetection

-- | Set electrical bias in the settings.
setBias :: Settings -> Bias -> IO ()
setBias _ BiasUnknown = throwIO $ InvalidArgument "setBias: BiasUnknown is a read-only state and cannot be set as a bias configuration."
setBias set biasVal   = unwrapOrThrow LineSettingsSetFailed (D.lineSettingsSetBias set biasVal)

-- | Get electrical bias from the settings.
bias :: Settings -> IO Bias
bias = D.lineSettingsBias

-- | Set drive mode in the settings.
setDrive :: Settings -> Drive -> IO ()
setDrive set driveMode = unwrapOrThrow LineSettingsSetFailed (D.lineSettingsSetDrive set driveMode)

-- | Get drive mode from the settings.
drive :: Settings -> IO Drive
drive = D.lineSettingsDrive

-- | Set event clock source in the settings.
setEventClock :: Settings -> Clock -> IO ()
setEventClock set clk = unwrapOrThrow LineSettingsSetFailed (D.lineSettingsSetEventClock set clk)

-- | Get event clock source from the settings.
eventClock :: Settings -> IO Clock
eventClock = D.lineSettingsEventClock

-- | Set active-low in the settings.
setActiveLow :: Settings -> Bool -> IO ()
setActiveLow = D.lineSettingsSetActiveLow

-- | Get active-low setting.
activeLow :: Settings -> IO Bool
activeLow = D.lineSettingsActiveLow

-- | Set debounce period in microseconds.
setDebouncePeriodUs :: Settings -> Word -> IO ()
setDebouncePeriodUs = D.lineSettingsSetDebouncePeriodUs

-- | Get debounce period in microseconds.
debouncePeriodUs :: Settings -> IO Word
debouncePeriodUs = D.lineSettingsDebouncePeriodUs

-- | Set default output value in the settings.
setOutputValue :: Settings -> Value -> IO ()
setOutputValue _ ValueError = throwIO $ InvalidArgument "setOutputValue: ValueError pattern is a read-only error state and cannot be set as an output value."
setOutputValue set val     = unwrapOrThrow LineSettingsSetFailed (D.lineSettingsSetOutputValue set val)

-- | Get default output value from the settings.
outputValue :: Settings -> IO Value
outputValue = D.lineSettingsOutputValue

-- | Reset line settings object to default values.
resetSettings :: Settings -> IO ()
resetSettings = D.lineSettingsReset

--------------------------------------------------------------------------------
-- GPIO Line Config
--------------------------------------------------------------------------------

-- | Allocate a new line configuration object and free it automatically afterwards.
-- 
withConfig :: (Config -> IO a) -> IO a
withConfig = bracket newLineConfig freeLineConfig

-- | Add settings for a list of line offsets in the configuration.
addSettings :: Config -> [Offset] -> Settings -> IO ()
addSettings config offsets stgs =
  unwrapOrThrow LineConfigNewFailed (D.lineConfigAddLineSettings config (V.fromList offsets) stgs)

-- | Get settings for a specific line offset from configuration.
lineSettings :: Config -> Offset -> IO Settings
lineSettings config offset' = unwrapOrThrow LineConfigNewFailed (D.lineConfigLineSettings config offset')

-- | Set output values for lines in configuration.
setOutputValues :: Config -> [Value] -> IO ()
setOutputValues config vals
  | V.elem ValueError (V.fromList vals) =
    throwIO $ InvalidArgument "setOutputValues: Vector contains ValueError pattern, which cannot be set as an output value."
  | otherwise                          =
    unwrapOrThrow LineConfigNewFailed (D.lineConfigSetOutputValues config (V.fromList vals))

-- | Get the number of configured offsets in the line configuration.
numOffsets :: Config -> IO Word
numOffsets = D.lineConfigNumOffsets

-- | Get all configured line offsets in the configuration as a 'Offset' list.
configuredOffsets :: Config -> IO [Offset]
configuredOffsets config = do
   vec <- D.lineConfigConfiguredOffsets config 
   return (V.toList vec)
  

-- | Reset line configuration object to empty state.
resetConfig :: Config -> IO ()
resetConfig = D.lineConfigReset

--------------------------------------------------------------------------------
-- GPIO Line Request
--------------------------------------------------------------------------------

-- | Request GPIO lines from a chip and automatically release them afterwards.
--
-- Since kernel configuration options ('RequestConfig') are optional,
-- passing 'Nothing' is equivalent to passing a NULL pointer in C.
--
-- @
-- someFunc :: IO ()
-- someFunc = do
--   withRequest chip Nothing config $ \request -> do
--     setLineValue request (Offset 271) Active 
-- @
--
-- Assuming offset 271 was configured as output ('DirOutput') with 'setDirection'
-- and added to the configuration with 'addSettings', this drives the physical pin active.
withRequest :: Chip -> Maybe RequestConfig -> Config -> (Request -> IO a) -> IO a
withRequest chip maybeReqConf lineConf = bracket (requestLines chip maybeReqConf lineConf) releaseLineRequest

-- | Get the logical value of a requested GPIO line at the given offset.
lineValue :: Request -> Offset -> IO Value
lineValue req offset' = unwrapOrThrow LineValueReadFailed (D.lineRequestValue req offset')

-- | Get the logical values of all requested lines as a 'Value' list.
lineValues :: Request -> IO [Value]
lineValues req = do
  vec <- unwrapOrThrow LineValueReadFailed (D.lineRequestValues req)
  return (V.toList vec)

-- | Get the logical values of a subset of requested lines specified by offsets.
lineValuesSubset :: Request -> [Offset] -> IO (V.Vector Value)
lineValuesSubset req offsets =
  unwrapOrThrow LineValueReadFailed (D.lineRequestSubsetValues req (V.fromList offsets))

-- | Set the logical value of a requested GPIO line at the given offset.
setLineValue :: Request -> Offset -> Value -> IO ()
setLineValue _ _ ValueError  = throwIO $ InvalidArgument "setValue: ValueError pattern is a read-only error state and cannot be written to a GPIO line."
setLineValue req offset' val = unwrapOrThrow LineValueWriteFailed (D.lineRequestSetValue req offset' val)

-- | Set the logical values of all requested lines from a 'Value' list.
setLineValues :: Request -> [Value] -> IO ()
setLineValues req vals
  | V.elem ValueError (V.fromList vals) =
    throwIO $ InvalidArgument "setValues: Vector contains ValueError pattern, which cannot be written to GPIO lines."
  | otherwise                           =
    unwrapOrThrow LineValueWriteFailed (D.lineRequestSetValues req (V.fromList vals))

-- | Set the logical values of a subset of requested lines from lists of 'Offset' and 'Value'.
setLineValuesSubset :: Request -> [Offset] -> [Value] -> IO ()
setLineValuesSubset req offsets vals
  | V.elem ValueError (V.fromList vals) =
    throwIO $ InvalidArgument "setValuesSubset: Vector contains ValueError pattern, which cannot be written to GPIO lines."
  | otherwise                           =
    unwrapOrThrow LineValueWriteFailed $ D.lineRequestSetValuesSubset req (V.fromList offsets) (V.fromList vals)

-- | Get the name as a 'String' of the chip this request was made on.
chipName :: Request -> IO String
chipName request = do
  name <- D.lineRequestChipName request  
  return (C8.unpack name)

-- | Get the number of lines in the request.
numLines :: Request -> IO Word
numLines = D.lineRequestNumLines

-- | Get all requested line offsets as a 'Offset' list.
requestedOffsets :: Request -> IO [Offset]
requestedOffsets request = do
  vec <- D.lineRequestRequestedOffsets request
  return (V.toList vec)

-- | Get the file descriptor associated with the line request handle.
lineFd :: Request -> IO Fd
lineFd = D.lineRequestFd

-- | Update the configuration of lines associated with an active line request.
reconfigureLines :: Request -> Config -> IO ()
reconfigureLines req config = unwrapOrThrow LineReconfigureFailed (D.lineRequestReconfigure req config)

--------------------------------------------------------------------------------
-- General Utilities
--------------------------------------------------------------------------------

-- | Check if the given filesystem path is a valid GPIO chip character device.
isGPIOChip :: FilePath -> IO Bool
isGPIOChip str = do
  path' <- encodeFS str   
  D.isGPIOChip path' 

-- | Retrieve the underlying libgpiod C API version string (e.g. "2.1").
gpiodAPIVersion :: IO String 
gpiodAPIVersion = C8.unpack <$> D.gpiodAPIVersion