packages feed

fuyu-gpio-0.0.9.0: examples/05-request-config.hs

{-# LANGUAGE OverloadedStrings #-}
-- In this example we will learn how to read a rotary encoder using a custom 'RequestConfig',
-- flattening resource allocation brackets with monadic continuation ('ContT'), and managing state
-- cleanly with 'Control.Monad.Trans.State.Strict' (StateT).
module Main where

-- High-level resource brackets & exception handling
import Fuyu.GPIO.Chip (withChip)
import qualified Fuyu.GPIO.Line as Line
import qualified Fuyu.GPIO.EdgeEvent as EdgeEvent  
import qualified Fuyu.GPIO.RequestConfig as ReqConf  
import Fuyu.GPIO.Exception (withGpioApp)

-- Base & third-party libraries
import Control.Monad.Trans.Cont (evalContT, ContT(..))
import Control.Monad.Trans.State.Strict (StateT, evalStateT, gets, modify')
import Control.Monad.IO.Class (liftIO)
import Control.Monad (forever, when)
import qualified Data.Vector.Storable as V (fromList)
import Data.List.NonEmpty (NonEmpty(..))

chipPath :: FilePath
chipPath = "/dev/gpiochip0"

-- Line offsets for the rotary encoder signals
offsetCLK :: Line.Offset
offsetCLK = Line.Offset 256

offsetDT :: Line.Offset
offsetDT = Line.Offset 271

-- Timeout for waiting on edge events (5 seconds)
fiveSecondsNs :: EdgeEvent.Timeout
fiveSecondsNs = EdgeEvent.Nanoseconds 5000000000

-- Setting user buffer capacity to 1 guarantees that 'readEvents' returns exactly 1 event at a time.
-- This simplifies pattern matching to '(ev :| _)' without losing any events in the kernel queue.
capacity :: EdgeEvent.Capacity
capacity = EdgeEvent.userBufferCapacity 1

--------------------------------------------------------------------------------
-- Encoder State Definition
--------------------------------------------------------------------------------

-- Clean pure Haskell record representing the quadrature state and step count.
data EncoderState = EncoderState
  { clkPin   :: !Int  -- Logical level of CLK line (1 = HIGH, 0 = LOW)
  , dtPin    :: !Int  -- Logical level of DT line (1 = HIGH, 0 = LOW)
  , position :: !Int  -- Accumulated rotary encoder step count
  } deriving (Eq, Show)

-- Initial state at startup (both lines idle at HIGH with 0 position count)
initialState :: EncoderState
initialState = EncoderState { clkPin = 1, dtPin = 1, position = 0 }

main :: IO ()
main = withGpioApp $ do
  putStrLn "Starting request config example..."
  runApp
  putStrLn "Request config example completed successfully."

--------------------------------------------------------------------------------
-- Helper Configurator Brackets
--------------------------------------------------------------------------------

-- Encapsulates the creation and configuration of RequestConfig (consumer label & buffer size).
withAppRequestConfig :: (ReqConf.RequestConfig -> IO r) -> IO r
withAppRequestConfig action = ReqConf.withRequestConfig $ \reqconf -> do
  ReqConf.setConsumer reqconf "encoder-app"
  ReqConf.setBufferSize reqconf 256
  action reqconf

-- Encapsulates line settings configuration (input mode, 1ms debounce, edge detection).
withAppLineSettings :: (Line.Settings -> IO r) -> IO r
withAppLineSettings action = Line.withSettings $ \settings -> do
  Line.setDirection settings Line.DirInput
  Line.setDebouncePeriodUs settings 1000 -- 1ms debounce suitable for rotary encoder hardware
  Line.setEdgeDetection settings Line.EdgeBoth 
  action settings

-- Encapsulates building line configuration for target pin offsets (CLK & DT).
withAppLineConfig :: Line.Settings -> (Line.Config -> IO r) -> IO r
withAppLineConfig settings action = Line.withConfig $ \config -> do
  Line.addSettings config (V.fromList [offsetCLK, offsetDT]) settings
  action config

--------------------------------------------------------------------------------
-- Resource Setup using ContT and Execution with StateT
--------------------------------------------------------------------------------

-- Monadic resource setup using 'ContT' flattens nested 'with...' brackets into a linear 'do' block.
-- 'evalStateT' then runs the application loop with managed pure state ('EncoderState').
runApp :: IO ()
runApp = evalContT $ do
  chip     <- ContT $ withChip chipPath
  reqconf  <- ContT withAppRequestConfig
  settings <- ContT withAppLineSettings
  config   <- ContT $ withAppLineConfig settings
  request  <- ContT $ Line.withRequest chip (Just reqconf) config
  buffer   <- ContT $ EdgeEvent.withBuffer capacity

  -- Run stateful application loop starting with 'initialState'
  liftIO $ evalStateT (appLoop request buffer) initialState

--------------------------------------------------------------------------------
-- Encoder Application Loop using MonadState (StateT)
--------------------------------------------------------------------------------

-- Application loop running in 'StateT EncoderState IO ()'.
appLoop :: Line.Request -> EdgeEvent.Buffer -> StateT EncoderState IO ()
appLoop request buffer = forever $ do
  result <- liftIO $ EdgeEvent.waitEvents request fiveSecondsNs
  case result of
    EdgeEvent.TimeoutResult -> 
      liftIO $ putStrLn "No edge event was read (timeout)."

    EdgeEvent.EventReady req -> do
      (ev :| _) <- liftIO $ EdgeEvent.readEvents req buffer
      oldPos    <- gets position
      
      -- Update pure state cleanly using strict 'modify''
      modify' (updateEncoderState ev)
      
      newPos    <- gets position
      when (newPos /= oldPos)
        $ liftIO $ putStrLn $ "Encoder Position: " ++ show newPos

-- Pure function that updates 'EncoderState' based on incoming 'EdgeEvent'.
-- When CLK transitions to LOW (Falling edge), we inspect the current state of DT:
--   - DT == 1 (HIGH) -> Clockwise rotation (+1)
--   - DT == 0 (LOW)  -> Counter-Clockwise rotation (-1)
updateEncoderState :: EdgeEvent.EdgeEvent -> EncoderState -> EncoderState
updateEncoderState (EdgeEvent.EdgeEvent offset evType _) st = case (offset, evType) of
  (Line.Offset 256, EdgeEvent.Falling) ->
    let delta  = if dtPin st == 1 then 1 else (-1)
    in st { clkPin = 0, position = position st + delta }

  (Line.Offset 256, EdgeEvent.Rising)  -> st { clkPin = 1 }
  (Line.Offset 271, EdgeEvent.Falling) -> st { dtPin = 0 }
  (Line.Offset 271, EdgeEvent.Rising)  -> st { dtPin = 1 }
  _                                    -> st