aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Oliver <contact@pauloliver.dev>2025-07-02 15:06:35 +0200
committerPaul Oliver <contact@pauloliver.dev>2025-08-13 23:54:10 +0000
commit8fe62292f18f4577303a868a8557b0486b218bcb (patch)
treecb9a9108eea479e932f37d03cf399b680e3886b2
parent0be7f1274e0cb8406bd4262b86d5e2e9dda77d7a (diff)
Code now uses `effectful` to manage side-effects
-rw-r--r--Makefile14
-rw-r--r--README.md4
-rw-r--r--hsm-bin/Test/Drive.hs22
-rw-r--r--hsm-bin/Test/Status.hs18
-rw-r--r--hsm-bin/hsm-bin.cabal34
-rw-r--r--hsm-core/Hsm/Core/Serial.hs53
-rw-r--r--hsm-core/hsm-core.cabal15
-rw-r--r--hsm-drive/Hsm/Drive.hs177
-rw-r--r--hsm-drive/hsm-drive.cabal19
-rw-r--r--hsm-gpio/Hsm/GPIO.hs284
-rw-r--r--hsm-gpio/Hsm/GPIO/FFI.hsc116
-rw-r--r--hsm-gpio/Hsm/GPIO/Lib.hsc116
-rw-r--r--hsm-gpio/hsm-gpio.cabal19
-rw-r--r--hsm-log/Hsm/Log.hs80
-rw-r--r--hsm-log/hsm-log.cabal15
-rw-r--r--hsm-pwm/Hsm/PWM.hs210
-rw-r--r--hsm-pwm/hsm-pwm.cabal15
-rw-r--r--hsm-readline/Hsm/Readline.hs50
-rw-r--r--hsm-readline/hsm-readline.cabal18
-rw-r--r--hsm-repl/Hsm/Repl.hs104
-rw-r--r--hsm-repl/Test/Repl.hs15
-rw-r--r--hsm-repl/hsm-repl.cabal36
-rw-r--r--hsm-status/Hsm/Status.hs41
-rw-r--r--hsm-status/hsm-status.cabal16
-rw-r--r--stack.yaml11
-rw-r--r--stack.yaml.lock24
-rw-r--r--sysconf/98-gpiod.rules1
-rw-r--r--sysconf/99-pwm.rules14
28 files changed, 734 insertions, 807 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..9dbf632
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+resolver = $(shell curl -s https://www.stackage.org/download/snapshots.json | yq .nightly)
+packages = $(shell ls -dQm hsm-*)
+
+build:
+ stack build
+
+clean:
+ stack clean --full
+
+exec:
+ stack exec $(exec)
+
+update:
+ yq -iSY '.resolver=$(resolver) | .packages=[$(packages)]' stack.yaml
diff --git a/README.md b/README.md
index c2c0385..d74a71e 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ without needing root access.
1. Install [`stack`](https://docs.haskellstack.org/en/stable/). It’s
recommended to use [`ghcup`](https://www.haskell.org/ghcup/) for
installation.
-2. Run `stack build` to compile the libraries and executables
+2. Run `make` to compile the libraries and executables
> Note: You may need to install system dependencies on your host first (e.g.,
-> `libgpiod`, etc.)
+> `libgpiod`, `libcamera`, etc.)
diff --git a/hsm-bin/Test/Drive.hs b/hsm-bin/Test/Drive.hs
deleted file mode 100644
index 7a58c11..0000000
--- a/hsm-bin/Test/Drive.hs
+++ /dev/null
@@ -1,22 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-import Control.IO.Region (region)
-import Control.Monad.Loops (whileJust_)
-import Hsm.Drive (DriveEnv, drive, driveEnvDefault)
-import Hsm.GPIO (allocateGPIO)
-import Hsm.PWM (allocatePWM)
-import Hsm.Readline (allocateReadline, readline)
-
-newtype Env = Env
- { driveEnv :: DriveEnv
- }
-
-main :: IO ()
-main =
- region $ \ioRegion -> do
- lineRequest <- allocateGPIO ioRegion "test-status"
- pwmHandle <- allocatePWM ioRegion $ const 0
- handle <- allocateReadline ioRegion
- whileJust_ (readline handle)
- $ drive lineRequest pwmHandle
- $ Env driveEnvDefault
diff --git a/hsm-bin/Test/Status.hs b/hsm-bin/Test/Status.hs
deleted file mode 100644
index 62ba4fa..0000000
--- a/hsm-bin/Test/Status.hs
+++ /dev/null
@@ -1,18 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-import Control.IO.Region (region)
-import Control.Monad.Loops (whileJust_)
-import Hsm.GPIO (allocateGPIO)
-import Hsm.Readline (allocateReadline, readline)
-import Hsm.Status (StatusEnv, status, statusEnvDefault)
-
-newtype Env = Env
- { statusEnv :: StatusEnv
- }
-
-main :: IO ()
-main =
- region $ \ioRegion -> do
- lineRequest <- allocateGPIO ioRegion "test-status"
- handle <- allocateReadline ioRegion
- whileJust_ (readline handle) $ status lineRequest $ Env statusEnvDefault
diff --git a/hsm-bin/hsm-bin.cabal b/hsm-bin/hsm-bin.cabal
deleted file mode 100644
index de6e1a5..0000000
--- a/hsm-bin/hsm-bin.cabal
+++ /dev/null
@@ -1,34 +0,0 @@
-cabal-version: 3.4
-author: Paul Oliver
-build-type: Simple
-maintainer: contact@pauloliver.dev
-name: hsm-bin
-version: 0.1.0.0
-
-common test-executable
- build-depends:
- , base
- , hsm-gpio
- , hsm-readline
- , io-region
- , monad-loops
-
- ghc-options: -Wall -Wunused-packages
-
- if !arch(x86_64)
- ghc-options: -optl=-mno-fix-cortex-a53-835769
-
- default-language: GHC2021
-
-executable test-status
- import: test-executable
- build-depends: hsm-status
- main-is: Test/Status.hs
-
-executable test-drive
- import: test-executable
- build-depends:
- , hsm-drive
- , hsm-pwm
-
- main-is: Test/Drive.hs
diff --git a/hsm-core/Hsm/Core/Serial.hs b/hsm-core/Hsm/Core/Serial.hs
new file mode 100644
index 0000000..0fc89e8
--- /dev/null
+++ b/hsm-core/Hsm/Core/Serial.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE TemplateHaskellQuotes #-}
+
+module Hsm.Core.Serial
+ ( makeSerial
+ )
+where
+
+import GHC.Num (integerFromInt)
+import Language.Haskell.TH
+ ( Body (NormalB)
+ , Clause (Clause)
+ , Con (NormalC)
+ , Dec (DataD, FunD, SigD)
+ , DerivClause (DerivClause)
+ , Exp (LitE)
+ , Lit (IntegerL)
+ , Name
+ , Pat (ConP)
+ , Q
+ , Type (AppT, ArrowT, ConT)
+ , mkName
+ )
+
+-- Generates a serial data type with the given name and a set of constructors,
+-- each mapped to a corresponding integer value.
+--
+-- - The data type derives `Bounded`, `Enum`, and `Show` for convenience.
+-- - A companion mapping function is also generated, converting each constructor
+-- to its associated integer.
+--
+-- For debugging purposes, use `-ddump-splices` to inspect the generated code.
+--
+-- Example:
+--
+-- $(makeSerial "GPIO" "Pin" "pinLine" ''Int [2, 3, 4])
+--
+-- Generates a data type `GPIOPin` with constructors `GPIO2`, `GPIO3` `GPIO4`,
+-- and a function `pinLine :: GPIOPin -> Int`.
+makeSerial :: String -> String -> String -> Name -> [Int] -> Q [Dec]
+makeSerial name suffix mapFun mapType idxs =
+ return
+ [ DataD [] dtName [] Nothing (idxCons <$> idxs) [derivClause]
+ , SigD mapFunName . AppT (AppT ArrowT $ ConT dtName) $ ConT mapType
+ , FunD mapFunName $ mapFunClause <$> idxs
+ ]
+ where
+ dtName = mkName $ name <> suffix
+ idxName idx = mkName $ name <> show idx
+ idxCons idx = NormalC (idxName idx) []
+ derivClause = DerivClause Nothing [ConT ''Bounded, ConT ''Enum, ConT ''Show]
+ mapFunName = mkName mapFun
+ mapFunBody = NormalB . LitE . IntegerL . integerFromInt
+ mapFunClause idx = Clause [ConP (idxName idx) [] []] (mapFunBody idx) []
diff --git a/hsm-core/hsm-core.cabal b/hsm-core/hsm-core.cabal
new file mode 100644
index 0000000..3242ff6
--- /dev/null
+++ b/hsm-core/hsm-core.cabal
@@ -0,0 +1,15 @@
+cabal-version: 3.8
+author: Paul Oliver
+build-type: Simple
+maintainer: contact@pauloliver.dev
+name: hsm-core
+version: 0.1.0.0
+
+library
+ build-depends:
+ , base
+ , template-haskell
+
+ default-language: GHC2024
+ exposed-modules: Hsm.Core.Serial
+ ghc-options: -O2 -Wall -Werror -Wno-star-is-type -Wunused-packages
diff --git a/hsm-drive/Hsm/Drive.hs b/hsm-drive/Hsm/Drive.hs
deleted file mode 100644
index 3580b5a..0000000
--- a/hsm-drive/Hsm/Drive.hs
+++ /dev/null
@@ -1,177 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE OverloadedRecordDot #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Hsm.Drive
- ( Speed(..)
- , Direction(..)
- , Duration
- , DriveAction(..)
- , DriveEnv(..)
- , driveEnvDefault
- , drive
- ) where
-
-import Control.Concurrent (threadDelay)
-import Control.Exception (AsyncException, handle)
-import Control.Monad (forM_)
-import Data.Text (pack)
-import Data.Typeable (Typeable)
-import Foreign.Ptr (Ptr)
-import GHC.Records (HasField)
-import Hsm.GPIO (GPIO(..), LineRequest, active, inactive, setPins)
-import Hsm.Log qualified as L
-import Hsm.PWM qualified as P
-import System.IO.Echo (withoutInputEcho)
-
-data Direction
- = N
- | NE
- | E
- | SE
- | S
- | SW
- | W
- | NW
- | RL
- | RR
- deriving (Read, Show, Typeable)
-
-data Speed
- = SlowXXX
- | SlowXX
- | SlowX
- | Slow
- | Fast
- | Top
- deriving (Read, Show, Typeable)
-
-type Duration = Float
-
-data DriveAction
- = Move Direction Speed Duration
- | Stop Duration
- deriving (Read, Show, Typeable)
-
--- This function maps a `Speed` value to a corresponding PWM cycle duration.
--- It assumes a stepper motor with 200 steps per revolution, using a 1/16
--- microstep setting (so actually, 3200 steps per revolution). The returned
--- values represent the duration of a whole PWM cycle in nanoseconds.
-cycleDuration :: Speed -> Int
-cycleDuration SlowXXX = 8000000000 `div` 3200 -- 1/8 revs/s
-cycleDuration SlowXX = 4000000000 `div` 3200 -- 1/4 revs/s
-cycleDuration SlowX = 2000000000 `div` 3200 -- 1/2 revs/s
-cycleDuration Slow = 1000000000 `div` 3200 -- 1 revs/s
-cycleDuration Fast = 500000000 `div` 3200 -- 2 revs/s
-cycleDuration Top = 250000000 `div` 3200 -- 4 revs/s
-
-data DriveEnv = DriveEnv
- { pinEnable :: GPIO
- , pinDiag1 :: GPIO
- , pinDiag2 :: GPIO
- , pinDir1 :: GPIO
- , pinDir2 :: GPIO
- , pinDir3 :: GPIO
- , pinDir4 :: GPIO
- , pwmChannel :: P.PWMChannel
- }
-
-driveEnvDefault :: DriveEnv
-driveEnvDefault =
- DriveEnv
- { pinEnable = GPIO21
- , pinDiag1 = GPIO20
- , pinDiag2 = GPIO16
- , pinDir1 = GPIO12
- , pinDir2 = GPIO7
- , pinDir3 = GPIO8
- , pinDir4 = GPIO25
- , pwmChannel = P.PWM3
- }
-
-drive ::
- HasField "driveEnv" env DriveEnv
- => Ptr LineRequest
- -> P.PWMHandle
- -> env
- -> [DriveAction]
- -> IO Bool
-drive lineRequest pwmHandle env actions =
- withoutInputEcho $ handle handler runActions
- where
- logMsg = L.logMsg ["drive"]
- -- Sets GPIO pins to a desired state
- setCycleDuration = P.setCycleDuration pwmHandle env.driveEnv.pwmChannel
- setActive pins = setPins lineRequest (pins <*> [env.driveEnv]) active
- setInactive pins = setPins lineRequest (pins <*> [env.driveEnv]) inactive
- -- Pin assignments for various movement directions, each direction
- -- corresponds to a specific set of GPIO pins.
- pinsDiag = [pinDiag1, pinDiag2]
- pinsDir = [pinDir1, pinDir2, pinDir3, pinDir4]
- pinDiagNE = [pinDiag1]
- pinDiagSE = [pinDiag2]
- pinDiagSW = [pinDiag1]
- pinDiagNW = [pinDiag2]
- pinsN = [pinDir2, pinDir4]
- pinsE = [pinDir1, pinDir2]
- pinsS = [pinDir1, pinDir3]
- pinsW = [pinDir3, pinDir4]
- pinsRL = [pinDir1, pinDir2, pinDir3, pinDir4]
- pinsRR = []
- -- Introduces a delay with the duration converted from seconds to
- -- microseconds.
- runDelay = threadDelay . round . (* 1000000)
- -- Ensures that the system is reset to a safe state by setting the PWM
- -- cycle duration to zero and deactivating all used motor control pins.
- runRelease = do
- setCycleDuration 0
- setInactive $ pinsDir <> pinsDiag
- -- Handles each movement command and activates the appropriate pins for
- -- the requested direction. It also sets the cycle duration for PWM and
- -- holds this state for the specified duration.
- runAction (Move direction speed duration) = do
- case direction of
- N -> setActive $ pinsN <> pinsDiag
- NE -> setActive $ pinsN <> pinDiagNE
- E -> setActive $ pinsE <> pinsDiag
- SE -> setActive $ pinsS <> pinDiagSE
- S -> setActive $ pinsS <> pinsDiag
- SW -> setActive $ pinsS <> pinDiagSW
- W -> setActive $ pinsW <> pinsDiag
- NW -> setActive $ pinsN <> pinDiagNW
- RL -> setActive $ pinsRL <> pinsDiag
- RR -> setActive $ pinsRR <> pinsDiag
- setCycleDuration $ cycleDuration speed
- runDelay duration
- runRelease
- -- A Stop command causes the system to wait for the specified duration
- -- without performing any movement. During this period, the motor drivers
- -- remain enabled, effectively applying a brake to the motor by holding it
- -- in its current position.
- runAction (Stop duration) = runDelay duration
- -- Catches any asynchronous exceptions during the execution of commands.
- -- If an exception occurs, the system will log the error and ensure that
- -- the motors are safely released by calling `runRelease`.
- handler exception = do
- logMsg
- $ "Async exception caught while command was running: "
- <> pack (show @AsyncException exception)
- runRelease
- return False
- -- Executes a series of drive actions
- runActions = do
- setCycleDuration 0
- -- The A4988 motor driver is placed in sleep mode between commands to
- -- save power. To wake it up, a 1-microsecond delay is required before
- -- sending step commands. For added safety, we wait 1 millisecond to
- -- ensure the driver is fully awake.
- logMsg "Enabling drivers"
- setActive [pinEnable]
- logMsg "Allowing drivers to come out of sleep mode"
- threadDelay 1000
- forM_ actions $ \action -> do
- logMsg $ "Running action " <> pack (show action)
- runAction action
- logMsg "Disabling drivers"
- setInactive [pinEnable]
- return True
diff --git a/hsm-drive/hsm-drive.cabal b/hsm-drive/hsm-drive.cabal
deleted file mode 100644
index a9dbe69..0000000
--- a/hsm-drive/hsm-drive.cabal
+++ /dev/null
@@ -1,19 +0,0 @@
-cabal-version: 3.4
-author: Paul Oliver
-build-type: Simple
-maintainer: contact@pauloliver.dev
-name: hsm-drive
-version: 0.1.0.0
-
-library
- build-depends:
- , base
- , echo
- , hsm-gpio
- , hsm-log
- , hsm-pwm
- , text
-
- exposed-modules: Hsm.Drive
- ghc-options: -Wall -Wunused-packages
- default-language: GHC2021
diff --git a/hsm-gpio/Hsm/GPIO.hs b/hsm-gpio/Hsm/GPIO.hs
index dd69122..2bcf3ed 100644
--- a/hsm-gpio/Hsm/GPIO.hs
+++ b/hsm-gpio/Hsm/GPIO.hs
@@ -1,163 +1,161 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
module Hsm.GPIO
- ( G.active
- , G.inactive
- , G.LineRequest
- , GPIO(..)
+ ( GPIOPin (..)
+ , GPIO
, setPins
, setAllPins
- , allocateGPIO
- ) where
+ , runGPIO
+ )
+where
-import Control.IO.Region (Region, alloc, alloc_, defer, free)
import Control.Monad (forM_, void)
-import Data.ByteString (useAsCString)
-import Data.Text (Text, pack)
-import Data.Text.Encoding (encodeUtf8)
-import Data.Vector.Storable qualified as V
-import Foreign.C.Types (CSize(CSize), CUInt(CUInt))
+import Data.Vector.Storable (fromList, replicate, unsafeWith)
+import Effectful (Dispatch (Static), DispatchOf, Eff, IOE, liftIO, (:>))
+import Effectful.Dispatch.Static
+ ( SideEffects (WithSideEffects)
+ , StaticRep
+ , evalStaticRep
+ , getStaticRep
+ , unsafeEff_
+ )
+import Effectful.Resource (Resource, allocateEff, releaseEff)
+import Foreign.C.String (withCString)
+import Foreign.C.Types (CSize (CSize), CUInt)
import Foreign.Ptr (Ptr)
-import Hsm.GPIO.Lib qualified as G
-import Hsm.Log qualified as L
+import Hsm.Core.Serial (makeSerial)
+import Hsm.GPIO.FFI
+ ( LineRequest
+ , LineValue
+ , chipClose
+ , chipOpen
+ , chipRequestLines
+ , inactive
+ , lineConfigAddLineSettings
+ , lineConfigFree
+ , lineConfigNew
+ , lineRequestRelease
+ , lineRequestSetValue
+ , lineRequestSetValues
+ , lineSettingsFree
+ , lineSettingsNew
+ , lineSettingsSetDirection
+ , lineSettingsSetOutputValue
+ , output
+ , requestConfigFree
+ , requestConfigNew
+ , requestConfigSetConsumer
+ )
+import Hsm.Log (Log, Severity (Info, Trace), logMsg)
+import Prelude hiding (replicate)
-logMsg :: Text -> IO ()
-logMsg = L.logMsg ["gpio"]
+$(makeSerial "GPIO" "Pin" "pinLine" ''CUInt $ [2 .. 17] <> [20 .. 27])
-data GPIO
- = GPIO2
- | GPIO3
- | GPIO4
- | GPIO5
- | GPIO6
- | GPIO7
- | GPIO8
- | GPIO9
- | GPIO10
- | GPIO11
- | GPIO12
- | GPIO13
- | GPIO14
- | GPIO15
- | GPIO16
- | GPIO17
- -- | GPIO18 -- reserved for PWM
- -- | GPIO19 -- reserved for PWM
- | GPIO20
- | GPIO21
- | GPIO22
- | GPIO23
- | GPIO24
- | GPIO25
- | GPIO26
- | GPIO27
- deriving (Bounded, Enum, Show)
-
-pinLine :: GPIO -> CUInt
-pinLine = CUInt . read . drop 4 . show
-
-allPins :: [GPIO]
+allPins :: [GPIOPin]
allPins = [minBound .. maxBound]
allLines :: [CUInt]
allLines = pinLine <$> allPins
-setPins :: Ptr G.LineRequest -> [GPIO] -> G.LineValue -> IO ()
-setPins lineRequest pins lineValue = do
- logMsg
- $ "Setting pin(s) "
- <> pack (show pins)
- <> " to state "
- <> pack (show lineValue)
- forM_ pins $ \pin -> G.lineRequestSetValue lineRequest (pinLine pin) lineValue
+pinCount :: Int
+pinCount = length allPins
+
+data GPIO (a :: * -> *) (b :: *)
+
+type instance DispatchOf GPIO = Static WithSideEffects
+
+newtype instance StaticRep GPIO
+ = GPIO (Ptr LineRequest)
+
+setPins :: (GPIO :> es, Log "gpio" :> es) => [GPIOPin] -> LineValue -> Eff es ()
+setPins pins lineValue = do
+ GPIO lineRequest <- getStaticRep
+ logMsg Trace $ "Setting pin(s) " <> show pins <> " to " <> show lineValue
+ forM_ pins $ \pin ->
+ unsafeEff_ $ lineRequestSetValue lineRequest (pinLine pin) lineValue
-setAllPins :: Ptr G.LineRequest -> G.LineValue -> IO ()
-setAllPins lineRequest lineValue = do
- logMsg
- $ "Setting all pins "
- <> pack (show allPins)
- <> " to state "
- <> pack (show lineValue)
- void
- $ V.unsafeWith (V.replicate (length allPins) lineValue)
- $ G.lineRequestSetValues lineRequest
+setAllPins :: (GPIO :> es, Log "gpio" :> es) => LineValue -> Eff es ()
+setAllPins lineValue = do
+ GPIO lineRequest <- getStaticRep
+ logMsg Trace $ "Setting all pins " <> show allPins <> " to " <> show lineValue
+ unsafeEff_ . unsafeWith (replicate pinCount lineValue) $
+ void . lineRequestSetValues lineRequest
-allocateGPIO :: Region -> Text -> IO (Ptr G.LineRequest)
-allocateGPIO region consumer = do
- (chip, chipKey) <- allocateChip
- (lineSettings, lineSettingsKey) <- allocateLineSettings
- (lineConfig, lineConfigKey) <- allocateLineConfig lineSettings
- (requestConfig, requestConfigKey) <- allocateRequestConfig
- lineRequest <- allocateLineRequest chip requestConfig lineConfig
- free requestConfigKey
- free lineConfigKey
- free lineSettingsKey
- free chipKey
- defer region $ setAllPins lineRequest G.inactive
- return lineRequest
+runGPIO
+ :: (IOE :> es, Log "gpio" :> es, Resource :> es)
+ => String
+ -> Eff (GPIO : es) a
+ -> Eff es a
+runGPIO consumer action = do
+ (chipKey, chip) <- chipBracket
+ (lineSettingsKey, lineSettings) <- lineSettingsBracket
+ (lineConfigKey, lineConfig) <- lineConfigBracket lineSettings
+ (requestConfigKey, requestConfig) <- requestConfigBracket
+ (_, lineRequest) <- lineRequestBracket chip requestConfig lineConfig
+ releaseEff requestConfigKey
+ releaseEff lineConfigKey
+ releaseEff lineSettingsKey
+ releaseEff chipKey
+ evalStaticRep (GPIO lineRequest) action
where
- chipPath = "/dev/gpiochip0"
- -- GPIO chip
- chipOpen = do
- logMsg $ "Opening GPIO chip " <> chipPath
- useAsCString (encodeUtf8 chipPath) G.chipOpen
- chipClose chip = do
- logMsg $ "Closing GPIO chip " <> chipPath
- G.chipClose chip
- allocateChip = alloc region chipOpen chipClose
- -- Line settings
- lineSettingsNew = do
- logMsg "Allocating line settings"
- lineSettings <- G.lineSettingsNew
- logMsg $ "With direction set to " <> pack (show G.output)
- void $ G.lineSettingsSetDirection lineSettings G.output
- logMsg $ "With output set to " <> pack (show G.inactive)
- void $ G.lineSettingsSetOutputValue lineSettings G.inactive
- return lineSettings
- lineSettingsFree lineSettings = do
- logMsg "Freeing line settings"
- G.lineSettingsFree lineSettings
- allocateLineSettings = alloc region lineSettingsNew lineSettingsFree
- -- Line config
- lineConfigNew lineSettings = do
- logMsg "Allocating line config"
- logMsg $ "With GPIO pins " <> pack (show allPins)
- lineConfig <- G.lineConfigNew
- void
- $ V.unsafeWith (V.fromList allLines)
- $ \pinsVector ->
- G.lineConfigAddLineSettings
+ chipBracket = allocateEff chipAlloc chipDealloc
+ where
+ chipPath = "/dev/gpiochip0"
+ chipAlloc = do
+ logMsg Info $ "Opening GPIO chip " <> chipPath
+ liftIO $ withCString chipPath chipOpen
+ chipDealloc chip = do
+ logMsg Info $ "Closing GPIO chip " <> chipPath
+ liftIO $ chipClose chip
+ lineSettingsBracket = allocateEff lineSettingsAlloc lineSettingsDealloc
+ where
+ lineSettingsAlloc = do
+ logMsg Info "Allocating line settings"
+ lineSettings <- liftIO lineSettingsNew
+ logMsg Info $ "With direction set to " <> show output
+ liftIO . void $ lineSettingsSetDirection lineSettings output
+ logMsg Info $ "With output set to " <> show inactive
+ liftIO . void $ lineSettingsSetOutputValue lineSettings inactive
+ return lineSettings
+ lineSettingsDealloc lineSettings = do
+ logMsg Info "Freeing line settings"
+ liftIO $ lineSettingsFree lineSettings
+ lineConfigBracket lineSettings =
+ allocateEff lineConfigAlloc lineConfigDealloc
+ where
+ lineConfigAlloc = do
+ logMsg Info "Allocating line config"
+ logMsg Info $ "With GPIO pins " <> show allPins
+ lineConfig <- liftIO lineConfigNew
+ liftIO . void . unsafeWith (fromList allLines) $ \pinsVector ->
+ lineConfigAddLineSettings
lineConfig
pinsVector
- (CSize $ fromIntegral $ length allPins)
+ (CSize $ fromIntegral pinCount)
lineSettings
- return lineConfig
- lineConfigFree lineConfig = do
- logMsg "Freeing line config"
- G.lineConfigFree lineConfig
- allocateLineConfig lineSettings =
- alloc region (lineConfigNew lineSettings) lineConfigFree
- -- Request config
- requestConfigNew = do
- logMsg "Allocating request config"
- logMsg $ "With consumer " <> consumer
- requestConfig <- G.requestConfigNew
- useAsCString (encodeUtf8 consumer)
- $ G.requestConfigSetConsumer requestConfig
- return requestConfig
- requestConfigFree requestConfig = do
- logMsg "Freeing request config"
- G.requestConfigFree requestConfig
- allocateRequestConfig = alloc region requestConfigNew requestConfigFree
- -- Line request
- requestLines chip requestConfig lineConfig = do
- logMsg "Allocating line request"
- G.requestLines chip requestConfig lineConfig
- lineRequestRelease lineRequest = do
- logMsg "Releasing line request"
- G.lineRequestRelease lineRequest
- allocateLineRequest chip requestConfig lineConfig =
- alloc_
- region
- (requestLines chip requestConfig lineConfig)
- lineRequestRelease
+ return lineConfig
+ lineConfigDealloc lineConfig = do
+ logMsg Info "Freeing line config"
+ liftIO $ lineConfigFree lineConfig
+ requestConfigBracket = allocateEff requestConfigAlloc requestConfigDealloc
+ where
+ requestConfigAlloc = do
+ logMsg Info "Allocating request config"
+ logMsg Info $ "With consumer " <> consumer
+ requestConfig <- liftIO requestConfigNew
+ liftIO . withCString consumer $ requestConfigSetConsumer requestConfig
+ return requestConfig
+ requestConfigDealloc requestConfig = do
+ logMsg Info "Freeing request config"
+ liftIO $ requestConfigFree requestConfig
+ lineRequestBracket chip requestConfig lineConfig =
+ allocateEff lineRequestAlloc lineRequestDealloc
+ where
+ lineRequestAlloc = do
+ logMsg Info "Allocating line request"
+ liftIO $ chipRequestLines chip requestConfig lineConfig
+ lineRequestDealloc lineRequest = do
+ logMsg Info "Releasing line request"
+ liftIO $ lineRequestRelease lineRequest
diff --git a/hsm-gpio/Hsm/GPIO/FFI.hsc b/hsm-gpio/Hsm/GPIO/FFI.hsc
new file mode 100644
index 0000000..f0f5737
--- /dev/null
+++ b/hsm-gpio/Hsm/GPIO/FFI.hsc
@@ -0,0 +1,116 @@
+{-# LANGUAGE CApiFFI #-}
+
+-- FFI bindings to `libgpiod` for direct GPIO hardware access.
+--
+-- Exposes only the minimal required subset of `libgpiod` functionality used by
+-- this project. The bindings are suitable for low-level hardware control.
+--
+-- Future work could expand this into a comprehensive `gpiod` binding package.
+module Hsm.GPIO.FFI
+ ( chipOpen
+ , chipClose
+ , input
+ , output
+ , LineValue
+ , active
+ , inactive
+ , lineSettingsNew
+ , lineSettingsFree
+ , lineSettingsSetDirection
+ , lineSettingsSetOutputValue
+ , lineConfigNew
+ , lineConfigFree
+ , lineConfigAddLineSettings
+ , requestConfigNew
+ , requestConfigFree
+ , requestConfigSetConsumer
+ , LineRequest
+ , chipRequestLines
+ , lineRequestRelease
+ , lineRequestSetValue
+ , lineRequestSetValues
+ )
+where
+
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (CInt), CSize (CSize), CUInt (CUInt))
+import Foreign.Ptr (Ptr)
+import Foreign.Storable (Storable)
+
+data Chip
+
+foreign import capi unsafe "gpiod.h gpiod_chip_open"
+ chipOpen :: CString -> IO (Ptr Chip)
+
+foreign import capi unsafe "gpiod.h gpiod_chip_close"
+ chipClose :: Ptr Chip -> IO ()
+
+data LineSettings
+
+newtype LineDirection
+ = LineDirection CInt
+ deriving Show
+
+foreign import capi "gpiod.h value GPIOD_LINE_DIRECTION_INPUT"
+ input :: LineDirection
+
+foreign import capi "gpiod.h value GPIOD_LINE_DIRECTION_OUTPUT"
+ output :: LineDirection
+
+newtype LineValue
+ = LineValue CInt
+ deriving (Show, Storable)
+
+foreign import capi "gpiod.h value GPIOD_LINE_VALUE_ACTIVE"
+ active :: LineValue
+
+foreign import capi "gpiod.h value GPIOD_LINE_VALUE_INACTIVE"
+ inactive :: LineValue
+
+foreign import capi unsafe "gpiod.h gpiod_line_settings_new"
+ lineSettingsNew :: IO (Ptr LineSettings)
+
+foreign import capi unsafe "gpiod.h gpiod_line_settings_free"
+ lineSettingsFree :: Ptr LineSettings -> IO ()
+
+foreign import capi unsafe "gpiod.h gpiod_line_settings_set_direction"
+ lineSettingsSetDirection :: Ptr LineSettings -> LineDirection -> IO CInt
+
+foreign import capi unsafe "gpiod.h gpiod_line_settings_set_output_value"
+ lineSettingsSetOutputValue :: Ptr LineSettings -> LineValue -> IO CInt
+
+data LineConfig
+
+foreign import capi unsafe "gpiod.h gpiod_line_config_new"
+ lineConfigNew :: IO (Ptr LineConfig)
+
+foreign import capi unsafe "gpiod.h gpiod_line_config_free"
+ lineConfigFree :: Ptr LineConfig -> IO ()
+
+foreign import capi unsafe "gpiod.h gpiod_line_config_add_line_settings"
+ lineConfigAddLineSettings :: Ptr LineConfig -> Ptr CUInt -> CSize -> Ptr LineSettings -> IO CInt
+
+data RequestConfig
+
+foreign import capi unsafe "gpiod.h gpiod_request_config_new"
+ requestConfigNew :: IO (Ptr RequestConfig)
+
+foreign import capi unsafe "gpiod.h gpiod_request_config_free"
+ requestConfigFree :: Ptr RequestConfig -> IO ()
+
+foreign import capi unsafe "gpiod.h gpiod_request_config_set_consumer"
+ requestConfigSetConsumer :: Ptr RequestConfig -> CString -> IO ()
+
+data LineRequest
+
+foreign import capi unsafe "gpiod.h gpiod_chip_request_lines"
+ chipRequestLines :: Ptr Chip -> Ptr RequestConfig -> Ptr LineConfig -> IO (Ptr LineRequest)
+
+foreign import capi unsafe "gpiod.h gpiod_line_request_release"
+ lineRequestRelease :: Ptr LineRequest -> IO ()
+
+foreign import capi unsafe "gpiod.h gpiod_line_request_set_value"
+ lineRequestSetValue :: Ptr LineRequest -> CUInt -> LineValue -> IO CInt
+
+foreign import capi unsafe "gpiod.h gpiod_line_request_set_values"
+ lineRequestSetValues :: Ptr LineRequest -> Ptr LineValue -> IO CInt
diff --git a/hsm-gpio/Hsm/GPIO/Lib.hsc b/hsm-gpio/Hsm/GPIO/Lib.hsc
deleted file mode 100644
index 6716f3a..0000000
--- a/hsm-gpio/Hsm/GPIO/Lib.hsc
+++ /dev/null
@@ -1,116 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE ForeignFunctionInterface #-}
-
--- This module provides C bindings to the `gpiod` library for direct GPIO pin
--- control. It includes bindings only for the C functions that are currently
--- used. In the future, creating a complete set of bindings for the entire
--- `gpiod` library as an external package would be a valuable contribution to
--- Hackage.
-
-module Hsm.GPIO.Lib
- ( chipOpen
- , chipClose
- , input
- , output
- , LineValue
- , active
- , inactive
- , lineSettingsNew
- , lineSettingsFree
- , lineSettingsSetDirection
- , lineSettingsSetOutputValue
- , lineConfigNew
- , lineConfigFree
- , lineConfigAddLineSettings
- , requestConfigNew
- , requestConfigFree
- , requestConfigSetConsumer
- , requestLines
- , LineRequest
- , lineRequestRelease
- , lineRequestSetValue
- , lineRequestSetValues
- ) where
-
-#include <gpiod.h>
-
-import Foreign.C.String (CString)
-import Foreign.C.Types (CInt(CInt), CSize(CSize), CUInt(CUInt))
-import Foreign.Ptr (Ptr)
-import Foreign.Storable (Storable)
-
-data Chip
-
-foreign import ccall unsafe "gpiod.h gpiod_chip_open"
- chipOpen :: CString -> IO (Ptr Chip)
-
-foreign import ccall unsafe "gpiod.h gpiod_chip_close"
- chipClose :: Ptr Chip -> IO ()
-
-data LineSettings
-
-newtype LineDirection =
- LineDirection CInt
- deriving (Show)
-
-#{enum LineDirection, LineDirection
- , input = GPIOD_LINE_DIRECTION_INPUT
- , output = GPIOD_LINE_DIRECTION_OUTPUT
-}
-
-newtype LineValue =
- LineValue CInt
- deriving (Show, Storable)
-
-#{enum LineValue, LineValue
- , active = GPIOD_LINE_VALUE_ACTIVE
- , inactive = GPIOD_LINE_VALUE_INACTIVE
-}
-
-foreign import ccall unsafe "gpiod.h gpiod_line_settings_new"
- lineSettingsNew :: IO (Ptr LineSettings)
-
-foreign import ccall unsafe "gpiod.h gpiod_line_settings_free"
- lineSettingsFree :: Ptr LineSettings -> IO ()
-
-foreign import ccall unsafe "gpiod.h gpiod_line_settings_set_direction"
- lineSettingsSetDirection :: Ptr LineSettings -> LineDirection -> IO CInt
-
-foreign import ccall unsafe "gpiod.h gpiod_line_settings_set_output_value"
- lineSettingsSetOutputValue :: Ptr LineSettings -> LineValue -> IO CInt
-
-data LineConfig
-
-foreign import ccall unsafe "gpiod.h gpiod_line_config_new"
- lineConfigNew :: IO (Ptr LineConfig)
-
-foreign import ccall unsafe "gpiod.h gpiod_line_config_free"
- lineConfigFree :: Ptr LineConfig -> IO ()
-
-foreign import ccall unsafe "gpiod.d gpiod_line_config_add_line_settings"
- lineConfigAddLineSettings :: Ptr LineConfig -> Ptr CUInt -> CSize -> Ptr LineSettings -> IO CInt
-
-data RequestConfig
-
-foreign import ccall unsafe "gpiod.h gpiod_request_config_new"
- requestConfigNew :: IO (Ptr RequestConfig)
-
-foreign import ccall unsafe "gpiod.h gpiod_request_config_free"
- requestConfigFree :: Ptr RequestConfig -> IO ()
-
-foreign import ccall unsafe "gpiod.h gpiod_request_config_set_consumer"
- requestConfigSetConsumer :: Ptr RequestConfig -> CString -> IO ()
-
-data LineRequest
-
-foreign import ccall unsafe "gpiod.h gpiod_chip_request_lines"
- requestLines :: Ptr Chip -> Ptr RequestConfig -> Ptr LineConfig -> IO (Ptr LineRequest)
-
-foreign import ccall unsafe "gpiod.h gpiod_line_request_release"
- lineRequestRelease :: Ptr LineRequest -> IO ()
-
-foreign import ccall unsafe "gpiod.h gpiod_line_request_set_value"
- lineRequestSetValue :: Ptr LineRequest -> CUInt -> LineValue -> IO CInt
-
-foreign import ccall unsafe "gpiod.h gpiod_line_request_set_values"
- lineRequestSetValues :: Ptr LineRequest -> Ptr LineValue -> IO CInt
diff --git a/hsm-gpio/hsm-gpio.cabal b/hsm-gpio/hsm-gpio.cabal
index 786977a..b38a777 100644
--- a/hsm-gpio/hsm-gpio.cabal
+++ b/hsm-gpio/hsm-gpio.cabal
@@ -1,4 +1,4 @@
-cabal-version: 3.4
+cabal-version: 3.8
author: Paul Oliver
build-type: Simple
maintainer: contact@pauloliver.dev
@@ -8,14 +8,17 @@ version: 0.1.0.0
library
build-depends:
, base
- , bytestring
+ , effectful-core
+ , effectful-plugin
+ , hsm-core
, hsm-log
- , io-region
- , text
+ , resourcet-effectful
, vector
+ default-language: GHC2024
exposed-modules: Hsm.GPIO
- other-modules: Hsm.GPIO.Lib
- ghc-options: -Wall -Wunused-packages
- extra-libraries: gpiod
- default-language: GHC2021
+ ghc-options:
+ -O2 -Wall -Werror -Wno-star-is-type -Wunused-packages
+ -ddump-splices -fplugin=Effectful.Plugin
+
+ other-modules: Hsm.GPIO.FFI
diff --git a/hsm-log/Hsm/Log.hs b/hsm-log/Hsm/Log.hs
index 0f388be..5321910 100644
--- a/hsm-log/Hsm/Log.hs
+++ b/hsm-log/Hsm/Log.hs
@@ -1,15 +1,77 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
module Hsm.Log
- ( logMsg
- ) where
+ ( Severity (Attention, Info, Trace)
+ , Log
+ , logMsg
+ , runLog
+ )
+where
-import Data.Text qualified as T
-import Data.Text.IO qualified as T
+import Control.Monad (when)
+import Data.Function (applyWhen)
+import Data.Proxy (Proxy (Proxy))
import Data.Time.Clock (getCurrentTime)
import Data.Time.ISO8601 (formatISO8601Millis)
+import Effectful (Dispatch (Static), DispatchOf, Eff, Effect, IOE, (:>))
+import Effectful.Dispatch.Static
+ ( SideEffects (WithSideEffects)
+ , StaticRep
+ , evalStaticRep
+ , getStaticRep
+ , unsafeEff_
+ )
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+import GHC.TypeLits.Printf (printf)
+import String.ANSI (red)
-logMsg :: [T.Text] -> T.Text -> IO ()
-logMsg domain msg = do
- time <- T.pack . formatISO8601Millis <$> getCurrentTime
- T.putStrLn $ T.unwords [time, "[" <> T.intercalate "/" domain <> "]", msg]
+data Severity
+ = Attention
+ | Info
+ | Trace
+ deriving (Eq, Ord, Show)
+
+data Log (d :: Symbol) (a :: * -> *) (b :: *)
+
+type instance DispatchOf (Log d) = Static WithSideEffects
+
+newtype instance StaticRep (Log d)
+ = Log Severity
+
+logMsg
+ :: forall d es
+ . (KnownSymbol d, Log d :> es)
+ => Severity
+ -> String
+ -> Eff es ()
+logMsg severity message = do
+ Log level <- getStaticRep
+ unsafeEff_ . when (severity <= level) $ do
+ time <- formatISO8601Millis <$> getCurrentTime
+ putStrLn . applyWhen (severity == Attention) red $
+ printf "%s %s [%s] %s" time (symbolVal $ Proxy @d) (show level) message
+
+runLog
+ :: forall d es a
+ . IOE :> es
+ => Severity
+ -> Eff (Log d : es) a
+ -> Eff es a
+runLog = evalStaticRep . Log
+
+class Logs (ds :: [Symbol]) (es :: [Effect]) where
+ type Insert ds es :: [Effect]
+ runLogs :: Severity -> Eff (Insert ds es) a -> Eff es a
+
+instance Logs ('[] :: [Symbol]) (es :: [Effect]) where
+ type Insert '[] es = es
+ runLogs _ = id
+
+instance
+ (IOE :> Insert ds es, KnownSymbol d, Logs ds es)
+ => Logs (d : ds :: [Symbol]) (es :: [Effect])
+ where
+ type Insert (d : ds) es = Log d : Insert ds es
+ runLogs level = runLogs @ds level . runLog @d level
diff --git a/hsm-log/hsm-log.cabal b/hsm-log/hsm-log.cabal
index 65279db..24995da 100644
--- a/hsm-log/hsm-log.cabal
+++ b/hsm-log/hsm-log.cabal
@@ -1,4 +1,4 @@
-cabal-version: 3.4
+cabal-version: 3.8
author: Paul Oliver
build-type: Simple
maintainer: contact@pauloliver.dev
@@ -8,10 +8,15 @@ version: 0.1.0.0
library
build-depends:
, base
+ , effectful-core
+ , effectful-plugin
, iso8601-time
- , text
+ , text-ansi
, time
+ , typelits-printf
- exposed-modules: Hsm.Log
- ghc-options: -Wall -Wunused-packages
- default-language: GHC2021
+ default-language: GHC2024
+ exposed-modules: Hsm.Log
+ ghc-options:
+ -O2 -Wall -Werror -Wno-star-is-type -Wunused-packages
+ -fplugin=Effectful.Plugin
diff --git a/hsm-pwm/Hsm/PWM.hs b/hsm-pwm/Hsm/PWM.hs
index 6b2a882..aa16d5c 100644
--- a/hsm-pwm/Hsm/PWM.hs
+++ b/hsm-pwm/Hsm/PWM.hs
@@ -1,143 +1,141 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+-- Raspberry Pi 5 PWM control (sysfs interface)
+--
+-- Requires:
+-- - `dtoverlay=pwm-2chan` in `/boot/config.txt`
+-- - UDEV rules for non-root access
+--
+-- Supports 2 active PWM channels. For details see:
+-- - PWM Configuration: https://www.pi4j.com/blog/2024/20240423_pwm_rpi5/#modify-configtxt
+-- - SysFS Reference: https://forums.raspberrypi.com/viewtopic.php?t=359251
+-- - UDEV Setup: https://forums.raspberrypi.com/viewtopic.php?t=316514
module Hsm.PWM
- ( PWMHandle
- , PWMChannel(PWM2, PWM3)
+ ( PWMChannel (..)
+ , PWM
, setCycleDuration
- , allocatePWM
- ) where
+ , runPWM
+ )
+where
import Control.Concurrent (threadDelay)
-import Control.IO.Region (Region, alloc_)
+import Control.Monad (forM_, void)
import Control.Monad.Loops (untilM_)
-import Data.Text (Text, pack)
-import Hsm.Log qualified as L
+import Effectful (Dispatch (Static), DispatchOf, Eff, IOE, liftIO, (:>))
+import Effectful.Dispatch.Static
+ ( SideEffects (WithSideEffects)
+ , StaticRep
+ , evalStaticRep
+ , unsafeEff_
+ )
+import Effectful.Resource (Resource, allocateEff_)
+import Hsm.Core.Serial (makeSerial)
+import Hsm.Log (Log, Severity (Info, Trace), logMsg)
import System.FilePath ((</>))
import System.Posix.Files (fileAccess)
--- This data type defines a placeholder `PWMHandle` to ensure that PWM actions
--- occur only after the `allocatePWM` function has been called. The empty
--- handle acts as a flag to enforce the correct order of operations.
-data PWMHandle =
- PWMHandle
+$(makeSerial "PWM" "Channel" "channelIndex" ''Int [1, 2])
--- This PWM controller assumes `dtoverlay=pwm-2chan` is set in
--- `/boot/config.txt`, enabling PWM on GPIO 18 (channel 2) and GPIO 19
--- (channel 3) for the Pi 5. Alternative configurations with additional PWM
--- channels are possible. For more information, consult the following links:
---
--- - Modifications to `config.txt`:
--- https://www.pi4j.com/blog/2024/20240423_pwm_rpi5/#modify-configtxt
---
--- - SysFS PWM interface:
--- https://forums.raspberrypi.com/viewtopic.php?t=359251
---
--- - UDEV setup for non-root access:
--- https://forums.raspberrypi.com/viewtopic.php?t=316514
-data PWMChannel
- = PWM2
- | PWM3
- deriving (Bounded, Enum, Show)
+data PWM (a :: * -> *) (b :: *)
+
+type instance DispatchOf PWM = Static WithSideEffects
-logMsg :: Text -> IO ()
-logMsg = L.logMsg ["pwm"]
+newtype instance StaticRep PWM
+ = PWM ()
chipPath :: FilePath
chipPath = "/sys/class/pwm/pwmchip0"
-channelIndex :: PWMChannel -> Int
-channelIndex = read . drop 3 . show
+channelPath :: PWMChannel -> FilePath
+channelPath channel = chipPath </> "pwm" <> show (channelIndex channel)
-channelPaths :: PWMChannel -> (FilePath, FilePath, FilePath)
-channelPaths channel = (enablePath, periodPath, dutyCyclePath)
- where
- channelPath = chipPath </> ("pwm" <> show (channelIndex channel))
- enablePath = channelPath </> "enable"
- periodPath = channelPath </> "period"
- dutyCyclePath = channelPath </> "duty_cycle"
+enablePath :: PWMChannel -> FilePath
+enablePath channel = channelPath channel </> "enable"
-setEnable :: PWMChannel -> Bool -> IO ()
+periodPath :: PWMChannel -> FilePath
+periodPath channel = channelPath channel </> "period"
+
+dutyCyclePath :: PWMChannel -> FilePath
+dutyCyclePath channel = channelPath channel </> "duty_cycle"
+
+setEnable :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Bool -> Eff es ()
setEnable channel enable = do
- logMsg $ "Setting " <> pack enablePath <> " to " <> pack (show enable)
- writeFile enablePath enableString
- where
- (enablePath, _, _) = channelPaths channel
- enableString = show $ fromEnum enable
+ logMsg Trace $ "Setting " <> enablePath channel <> " to " <> show enable
+ unsafeEff_ . writeFile (enablePath channel) . show $ fromEnum enable
-setPeriod :: PWMChannel -> Int -> IO ()
+setPeriod :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Int -> Eff es ()
setPeriod channel period = do
- logMsg $ "Setting " <> pack periodPath <> " to " <> pack (show period)
- writeFile periodPath $ show period
- where
- (_, periodPath, _) = channelPaths channel
+ logMsg Trace $ "Setting " <> periodPath channel <> " to " <> show period
+ unsafeEff_ . writeFile (periodPath channel) $ show period
-setDutyCycle :: PWMChannel -> Int -> IO ()
+setDutyCycle :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Int -> Eff es ()
setDutyCycle channel dutyCycle = do
- logMsg $ "Setting " <> pack dutyCyclePath <> " to " <> pack (show dutyCycle)
- writeFile dutyCyclePath $ show dutyCycle
- where
- (_, _, dutyCyclePath) = channelPaths channel
+ logMsg Trace $ "Setting " <> dutyCyclePath channel <> " to " <> show dutyCycle
+ unsafeEff_ . writeFile (dutyCyclePath channel) $ show dutyCycle
-setCycleDuration :: PWMHandle -> PWMChannel -> Int -> IO ()
-setCycleDuration _ channel 0 = do
- logMsg $ "Halting PWM signals on channel " <> pack (show channel)
+-- Sets the PWM cycle duration (period) for a channel
+--
+-- - Special case: A duration of 0 halts PWM output
+-- - Normal operation:
+-- 1. Zero the duty cycle first to avoid 'Invalid argument' errors
+-- (period cannot be smaller than current duty cycle)
+-- 2. Update period
+-- 3. Set default 50% duty cycle
+-- 4. Re-enable output
+setCycleDuration
+ :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Int -> Eff es ()
+setCycleDuration channel 0 = do
+ logMsg Trace $ "Halting PWM signals on channel " <> show channel
setEnable channel False
-setCycleDuration _ channel cycleDuration = do
- logMsg
- $ "Setting cycle duration on channel "
- <> pack (show channel)
- <> " to "
- <> pack (show cycleDuration)
+setCycleDuration channel cycleDuration = do
+ logMsg Trace $
+ "Setting cycle duration on channel "
+ <> show channel
+ <> " to "
+ <> show cycleDuration
setEnable channel False
- -- Sets the duty cycle to zero before updating the period. This prevents
- -- `Invalid argument` errors, as the period must never be set to a value
- -- smaller than the duty cycle.
setDutyCycle channel 0
setPeriod channel cycleDuration
setDutyCycle channel $ cycleDuration `div` 2
setEnable channel True
-allocatePWM :: Region -> (PWMChannel -> Int) -> IO PWMHandle
-allocatePWM region mapper = alloc_ region acquire $ const release
+runPWM
+ :: (IOE :> es, Log "pwm" :> es, Resource :> es)
+ => Eff (PWM : es) a
+ -> Eff es a
+runPWM action =
+ evalStaticRep (PWM ()) $ do
+ void $ allocateEff_ pwmAlloc pwmDealloc
+ action
where
exportPath = chipPath </> "export"
unexportPath = chipPath </> "unexport"
- -- This function waits for a file at the given `path` to become writable
- -- by the `pwm` user group. A UDEV rule ensures that files in
- -- `/sys/class/pwm/pwmchip*` are made writable through a `chown` call.
- -- However, because UDEV rules are applied asynchronously, there may be a
- -- brief delay before the rule takes effect. This function blocks and
- -- repeatedly checks the file's write permissions by calling `fileAccess`.
- -- It continues checking until write access is confirmed.
+ -- Blocks until the PWM sysfs file becomes writable
+ --
+ -- Handles the race condition caused by:
+ -- 1. Sysfs file creation
+ -- 2. UDEV rules (async `chown` to `pwm` group)
+ --
+ -- Polls permissions with 1ms delay between checks.
waitWritable path = do
- logMsg $ "Waiting for " <> pack path <> " to become writable"
- untilM_ (threadDelay 1000) $ fileAccess path False True False
+ logMsg Info $ "Waiting for " <> path <> " to become writable"
+ liftIO . untilM_ (threadDelay 1000) $ fileAccess path False True False
allChannels = [minBound .. maxBound]
- -- Acquire PWM channels
- acquireChannel channel = do
- logMsg
- $ "Exporting channel "
- <> pack (show channel)
- <> " on chip "
- <> pack chipPath
- writeFile exportPath $ show (channelIndex channel)
- let (enablePath, periodPath, dutyCyclePath) = channelPaths channel
- waitWritable enablePath
- waitWritable periodPath
- waitWritable dutyCyclePath
- setCycleDuration PWMHandle channel $ mapper channel
- acquire = do
+ pwmAlloc = do
waitWritable exportPath
waitWritable unexportPath
- mapM_ acquireChannel allChannels
- return PWMHandle
- -- Release PWM channels
- releaseChannel channel = do
- setEnable channel False
- logMsg
- $ "Unexporting channel "
- <> pack (show channel)
- <> " on chip "
- <> pack chipPath
- writeFile unexportPath $ show (channelIndex channel)
- release = mapM_ releaseChannel allChannels
+ forM_ allChannels $ \channel -> do
+ logMsg Info $
+ "Exporting channel " <> show channel <> " on chip " <> chipPath
+ liftIO . writeFile exportPath $ show (channelIndex channel)
+ waitWritable $ enablePath channel
+ waitWritable $ periodPath channel
+ waitWritable $ dutyCyclePath channel
+ setCycleDuration channel 0
+ pwmDealloc =
+ forM_ allChannels $ \channel -> do
+ setEnable channel False
+ logMsg Info $
+ "Unexporting channel " <> show channel <> " on chip " <> chipPath
+ liftIO . writeFile unexportPath $ show (channelIndex channel)
diff --git a/hsm-pwm/hsm-pwm.cabal b/hsm-pwm/hsm-pwm.cabal
index 8a6c44d..a4453ba 100644
--- a/hsm-pwm/hsm-pwm.cabal
+++ b/hsm-pwm/hsm-pwm.cabal
@@ -1,4 +1,4 @@
-cabal-version: 3.4
+cabal-version: 3.8
author: Paul Oliver
build-type: Simple
maintainer: contact@pauloliver.dev
@@ -8,14 +8,17 @@ version: 0.1.0.0
library
build-depends:
, base
+ , effectful-core
+ , effectful-plugin
, filepath
+ , hsm-core
, hsm-log
- , io-region
, monad-loops
- , text
+ , resourcet-effectful
, unix
+ default-language: GHC2024
exposed-modules: Hsm.PWM
- ghc-options: -Wall -Wunused-packages
- extra-libraries: gpiod
- default-language: GHC2021
+ ghc-options:
+ -O2 -Wall -Werror -Wno-star-is-type -Wunused-packages
+ -ddump-splices -fplugin=Effectful.Plugin
diff --git a/hsm-readline/Hsm/Readline.hs b/hsm-readline/Hsm/Readline.hs
deleted file mode 100644
index 8a0c232..0000000
--- a/hsm-readline/Hsm/Readline.hs
+++ /dev/null
@@ -1,50 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Hsm.Readline
- ( readline
- , allocateReadline
- ) where
-
-import Control.IO.Region (Region, alloc_)
-import Data.Text (Text, pack)
-import Data.Typeable (Proxy(Proxy), Typeable, typeRep)
-import Hsm.Log qualified as L
-import System.Console.Haskeline qualified as H
-import System.Console.Haskeline.IO qualified as H
-import Text.Read (readEither)
-
-logMsg :: Text -> IO ()
-logMsg = L.logMsg ["readline"]
-
-readline ::
- forall a. (Read a, Show a, Typeable a)
- => H.InputState
- -> IO (Maybe a)
-readline handle = do
- logMsg $ "Expecting value of type " <> pack (show $ typeRep $ Proxy @a)
- valueMaybe <- queryInput
- maybe (return Nothing) parseValueStr valueMaybe
- where
- queryInput =
- H.queryInput handle
- $ H.handleInterrupt (return Nothing)
- $ H.withInterrupt
- $ H.getInputLine "% "
- parseValueStr valueStr =
- case readEither @a valueStr of
- Right commandValue -> do
- logMsg $ "Parsed value " <> pack (show commandValue)
- return $ Just commandValue
- Left err -> do
- logMsg $ pack err
- readline handle
-
-allocateReadline :: Region -> IO H.InputState
-allocateReadline region = alloc_ region initializeInput cancelInput
- where
- initializeInput = do
- logMsg "Initializing input with default settings"
- H.initializeInput H.defaultSettings
- cancelInput handle = do
- logMsg "Cancelling input"
- H.cancelInput handle
diff --git a/hsm-readline/hsm-readline.cabal b/hsm-readline/hsm-readline.cabal
deleted file mode 100644
index 4532219..0000000
--- a/hsm-readline/hsm-readline.cabal
+++ /dev/null
@@ -1,18 +0,0 @@
-cabal-version: 3.4
-author: Paul Oliver
-build-type: Simple
-maintainer: contact@pauloliver.dev
-name: hsm-readline
-version: 0.1.0.0
-
-library
- build-depends:
- , base
- , haskeline
- , hsm-log
- , io-region
- , text
-
- exposed-modules: Hsm.Readline
- ghc-options: -Wall -Wunused-packages
- default-language: GHC2021
diff --git a/hsm-repl/Hsm/Repl.hs b/hsm-repl/Hsm/Repl.hs
new file mode 100644
index 0000000..5265e59
--- /dev/null
+++ b/hsm-repl/Hsm/Repl.hs
@@ -0,0 +1,104 @@
+{-# LANGUAGE TypeFamilies #-}
+
+module Hsm.Repl
+ ( Repl
+ , repl
+ , runRepl
+ )
+where
+
+import Control.Monad (forM_)
+import Data.Typeable (Proxy (Proxy), Typeable, typeRep)
+import Effectful (Dispatch (Static), DispatchOf, Eff, IOE, liftIO, (:>))
+import Effectful.Dispatch.Static
+ ( SideEffects (WithSideEffects)
+ , StaticRep
+ , evalStaticRep
+ , getStaticRep
+ , unsafeEff_
+ )
+import Effectful.Resource (Resource, allocateEff)
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+import Generic.Data.Function.Common.Generic.Meta (KnownSymbols, symbolVals)
+import Hsm.Log (Log, Severity (Attention, Info, Trace), logMsg)
+import Language.Haskell.Interpreter
+ ( GhcError (errMsg)
+ , InterpreterError (WontCompile)
+ , as
+ , interpret
+ , runInterpreter
+ , setImports
+ )
+import System.Console.Haskeline
+ ( defaultSettings
+ , getInputLine
+ , handleInterrupt
+ , withInterrupt
+ )
+import System.Console.Haskeline.IO
+ ( InputState
+ , cancelInput
+ , initializeInput
+ , queryInput
+ )
+
+data Repl (p :: Symbol) (ms :: [Symbol]) (t :: *) (a :: * -> *) (b :: *)
+
+type instance DispatchOf (Repl p ms t) = Static WithSideEffects
+
+newtype instance StaticRep (Repl p ms t)
+ = Repl InputState
+
+repl
+ :: forall p ms t es
+ . ( KnownSymbol p
+ , KnownSymbols ms
+ , Log "repl" :> es
+ , Repl p ms t :> es
+ , Show t
+ , Typeable t
+ )
+ => Eff es (Maybe t)
+repl = query >>= maybe (return Nothing) parse
+ where
+ query = do
+ Repl inputState <- getStaticRep
+ logMsg Trace $ "Expecting a value of type: " <> show (typeRep $ Proxy @t)
+ unsafeEff_
+ . queryInput inputState
+ . handleInterrupt (return Nothing)
+ . withInterrupt
+ . getInputLine
+ $ symbolVal (Proxy @p)
+ parse string = do
+ logMsg Trace $ "Parsing string: " <> string
+ eitherValue <-
+ unsafeEff_ . runInterpreter $ do
+ setImports $ symbolVals @ms
+ interpret string as
+ case eitherValue of
+ Right value -> do
+ logMsg Trace $ "Parsed value: " <> show value
+ return $ Just value
+ Left (WontCompile errors) -> do
+ forM_ errors $ logMsg Attention . errMsg
+ repl
+ Left err -> do
+ logMsg Attention $ show err
+ repl
+
+runRepl
+ :: forall p ms t es a
+ . (IOE :> es, Log "repl" :> es, Resource :> es)
+ => Eff (Repl p ms t : es) a
+ -> Eff es a
+runRepl action = do
+ inputState <- snd <$> allocateEff inputStateAlloc inputStateDealloc
+ evalStaticRep (Repl inputState) action
+ where
+ inputStateAlloc = do
+ logMsg Info "Initializing input"
+ liftIO $ initializeInput defaultSettings
+ inputStateDealloc inputState = do
+ logMsg Info "Cancelling input"
+ liftIO $ cancelInput inputState
diff --git a/hsm-repl/Test/Repl.hs b/hsm-repl/Test/Repl.hs
new file mode 100644
index 0000000..9052ef1
--- /dev/null
+++ b/hsm-repl/Test/Repl.hs
@@ -0,0 +1,15 @@
+import Control.Monad (void)
+import Control.Monad.Loops (whileJust_)
+import Data.Function ((&))
+import Effectful (runEff)
+import Effectful.Resource (runResource)
+import Hsm.Log (Severity (Trace), runLog)
+import Hsm.Repl (repl, runRepl)
+
+main :: IO ()
+main =
+ void (whileJust_ repl return)
+ & runRepl @"exec-repl λ " @'["Prelude"] @[Bool]
+ & runLog @"repl" Trace
+ & runResource
+ & runEff
diff --git a/hsm-repl/hsm-repl.cabal b/hsm-repl/hsm-repl.cabal
new file mode 100644
index 0000000..fd346f9
--- /dev/null
+++ b/hsm-repl/hsm-repl.cabal
@@ -0,0 +1,36 @@
+cabal-version: 3.8
+author: Paul Oliver
+build-type: Simple
+maintainer: contact@pauloliver.dev
+name: hsm-repl
+version: 0.1.0.0
+
+common common
+ build-depends:
+ , base
+ , effectful-core
+ , effectful-plugin
+ , generic-data-functions
+ , haskeline
+ , hint
+ , hsm-log
+ , resourcet-effectful
+
+ default-language: GHC2024
+ ghc-options:
+ -O2 -Wall -Werror -Wno-star-is-type -Wunused-packages
+ -fplugin=Effectful.Plugin
+
+library
+ import: common
+ exposed-modules: Hsm.Repl
+
+executable test-repl
+ import: common
+ build-depends: monad-loops
+
+ if !arch(x86_64)
+ ghc-options: -optl=-mno-fix-cortex-a53-835769
+
+ main-is: Test/Repl.hs
+ other-modules: Hsm.Repl
diff --git a/hsm-status/Hsm/Status.hs b/hsm-status/Hsm/Status.hs
deleted file mode 100644
index 8154611..0000000
--- a/hsm-status/Hsm/Status.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE OverloadedRecordDot #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Hsm.Status
- ( StatusEnv(..)
- , statusEnvDefault
- , status
- ) where
-
-import Foreign.Ptr (Ptr)
-import GHC.Records (HasField)
-import Hsm.GPIO (GPIO(..), LineRequest, active, inactive, setPins)
-import Hsm.Log qualified as L
-
-data StatusEnv = StatusEnv
- { gpioOk :: GPIO
- , gpioError :: GPIO
- }
-
-statusEnvDefault :: StatusEnv
-statusEnvDefault = StatusEnv {gpioOk = GPIO17, gpioError = GPIO27}
-
-status ::
- HasField "statusEnv" env StatusEnv
- => Ptr LineRequest
- -> env
- -> [Bool]
- -> IO ()
-status lineRequest env signals = do
- if and signals
- then do
- logMsg "All signals OK"
- setPins lineRequest [env.statusEnv.gpioError] inactive
- setPins lineRequest [env.statusEnv.gpioOk] active
- else do
- logMsg "Error signal received"
- setPins lineRequest [env.statusEnv.gpioError] active
- setPins lineRequest [env.statusEnv.gpioOk] inactive
- where
- logMsg = L.logMsg ["status"]
diff --git a/hsm-status/hsm-status.cabal b/hsm-status/hsm-status.cabal
deleted file mode 100644
index 66560e8..0000000
--- a/hsm-status/hsm-status.cabal
+++ /dev/null
@@ -1,16 +0,0 @@
-cabal-version: 3.4
-author: Paul Oliver
-build-type: Simple
-maintainer: contact@pauloliver.dev
-name: hsm-status
-version: 0.1.0.0
-
-library
- build-depends:
- , base
- , hsm-gpio
- , hsm-log
-
- exposed-modules: Hsm.Status
- ghc-options: -Wall -Wunused-packages
- default-language: GHC2021
diff --git a/stack.yaml b/stack.yaml
index 0fda386..640de3f 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,9 +1,10 @@
+extra-deps:
+ - resourcet-effectful-1.0.1.0
+ - typelits-printf-0.3.0.0
packages:
- - hsm-bin
- - hsm-drive
+ - hsm-core
- hsm-gpio
- hsm-log
- hsm-pwm
- - hsm-readline
- - hsm-status
-snapshot: lts-23.7
+ - hsm-repl
+resolver: nightly-2025-07-02
diff --git a/stack.yaml.lock b/stack.yaml.lock
index 6ce598a..a6533d3 100644
--- a/stack.yaml.lock
+++ b/stack.yaml.lock
@@ -3,10 +3,24 @@
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/topics/lock_files
-packages: []
+packages:
+- completed:
+ hackage: resourcet-effectful-1.0.1.0@sha256:13f94c9832d0d1573abbabcddc5c3aa3c341973d1d442445795593e355e7803e,2115
+ pantry-tree:
+ sha256: ef0db7bdeca5df1e722958cf5c8f3205ed5bf92b111e0fbc5d1a3c592d1c210e
+ size: 283
+ original:
+ hackage: resourcet-effectful-1.0.1.0
+- completed:
+ hackage: typelits-printf-0.3.0.0@sha256:47f3d044056546f5c027db53deb6412dbf455c9dcfb8cbb0637a83692906fc6e,2111
+ pantry-tree:
+ sha256: bdda47a6cdfc18b1ad74f9c16432a411909fa65147fe0f8a91b09a02a442751d
+ size: 702
+ original:
+ hackage: typelits-printf-0.3.0.0
snapshots:
- completed:
- sha256: 4ef79c30b9efcf07335cb3de532983a7ac4c5a4180bc17f6212a86b09ce2ff75
- size: 680777
- url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/7.yaml
- original: lts-23.7
+ sha256: 141ac77307711e7dde0c4a5ff5e0738969f77529d6a88b71fdbc317d2310142e
+ size: 722188
+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2025/7/2.yaml
+ original: nightly-2025-07-02
diff --git a/sysconf/98-gpiod.rules b/sysconf/98-gpiod.rules
index 01a05ed..a7275d9 100644
--- a/sysconf/98-gpiod.rules
+++ b/sysconf/98-gpiod.rules
@@ -1,2 +1 @@
-# This rule grants the `gpio` group access to GPIO devices.
SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpiod", MODE="0660"
diff --git a/sysconf/99-pwm.rules b/sysconf/99-pwm.rules
index 8407ebe..59ba681 100644
--- a/sysconf/99-pwm.rules
+++ b/sysconf/99-pwm.rules
@@ -1,9 +1,11 @@
-# This UDEV rule provides the `pwm` user group with access to PWM devices.
-# Note that UDEV operates asynchronously, so there may be a slight delay
-# between changes to the directory structure (e.g., when a new PWM channel is
-# added) and the corresponding permission updates. To ensure the rule has been
-# fully applied, you can use the command `udevadm settle` to wait for the UDEV
-# process to complete.
+# Grants 'pwm' group RW access to Raspberry Pi PWM sysfs interfaces
+#
+# Handles both:
+# - Standard /sys/class/pwm paths
+# - RPi5-specific PCIe PWM controllers (/sys/devices/platform/axi/...)
+#
+# Note: For race-free operation, check file writability (not just existence)
+# using `access(2)` before attempting operations.
SUBSYSTEM=="pwm*", PROGRAM="/bin/sh -c ' \
chown -R root:pwm /sys/class/pwm ; \
chmod -R 770 /sys/class/pwm ; \