packages feed

fuyu-gpio-0.1.0.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 
import qualified Fuyu.GPIO.EdgeEvent as Edge 
import qualified Fuyu.GPIO.RequestConfig as ReqConf  

-- Base & core libraries ('transformers' is a core GHC boot package with zero external dependencies).
import Control.Monad.Trans.Cont (evalContT, ContT(..))
-- We import 'Control.Monad.Trans.State.Strict' directly from 'transformers' rather than
-- 'Control.Monad.State' from 'mtl' to keep external dependencies minimal.
-- We explicitly choose the strict variant ('.Strict') and use 'modify'' to force evaluation of state
-- updates eagerly. This prevents space leaks (accumulation of unevaluated thunks in memory)
-- as encoder tick counts change continuously.
import Control.Monad.Trans.State.Strict (StateT, evalStateT, gets, modify')
import Control.Monad.IO.Class (liftIO)
import Control.Monad (forever, when)
-- To have a clean exit when Ctrl+C is pressed.
import Control.Exception (catch, throwIO, AsyncException(UserInterrupt))
import Data.List.NonEmpty (NonEmpty(..))

myChipPath :: FilePath
myChipPath = "/dev/gpiochip0"

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

offsetDT :: Offset
offsetDT = Offset 271

-- Timeout for waiting on edge events (5 seconds)
fiveSecondsNs :: Edge.Timeout
fiveSecondsNs = Edge.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 :: Edge.Capacity
capacity = Edge.userBufferCapacity 1

-- 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 = do 
  putStrLn "Starting request config example (Press Ctrl+C to exit)..."
  runApp `catch` \exc -> case exc of
    UserInterrupt -> putStrLn "\nLoop terminated successfully!"
    other         -> throwIO other

-- We need some helpers configurator brackets. 
-- This one 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

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

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

-- Monadic resource setup using 'ContT' (from 'transformers') flattens nested 'with...' brackets
-- into a linear 'do' block. This achieves the same goal as 'runManaged' in example 04, but using
-- standard core transformers without external dependencies.
-- 'evalStateT' then runs the stateful application loop initialized with 'initialState'.
runApp :: IO ()
runApp = evalContT $ do
  chip     <- ContT $ withChip myChipPath
  reqconf  <- ContT withAppRequestConfig
  settings <- ContT withAppLineSettings
  config   <- ContT $ withAppLineConfig settings
  request  <- ContT $ withRequest chip (Just reqconf) config
  buffer   <- ContT $ Edge.withBuffer capacity

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

-- Application loop running in 'StateT EncoderState IO ()'.
-- Unlike example 04 which used an 'MVar' to share mutable state across concurrent threads ('forkIO'),
-- this example is a single-threaded sequential event loop. 'StateT' provides pure, structured state
-- transitions without the synchronization overhead or locking primitives of MVars.
appLoop :: Request -> Edge.Buffer -> StateT EncoderState IO ()
appLoop request buffer = forever $ do
  result <- liftIO $ Edge.waitEvents request fiveSecondsNs
  case result of
    Edge.TimeoutResult -> 
      liftIO $ putStrLn "No edge event was read (timeout)."

    Edge.EventReady req -> do
      (ev :| _) <- liftIO $ Edge.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 :: Edge.Event -> EncoderState -> EncoderState
updateEncoderState (Edge.EdgeEvent offset evType _) st = case (offset, evType) of
  (Offset 256, Edge.Falling) ->
    let delta  = if dtPin st == 1 then 1 else (-1)
    in st { clkPin = 0, position = position st + delta }

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