aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--hsm-dummy-gradient/Main.hs48
-rw-r--r--hsm-dummy-gradient/hsm-dummy-gradient.cabal24
-rw-r--r--hsm-pwm/Hsm/PWM.hs120
-rw-r--r--hsm-pwm/hsm-pwm.cabal19
-rw-r--r--servconf.yaml4
-rw-r--r--stack.yaml2
6 files changed, 217 insertions, 0 deletions
diff --git a/hsm-dummy-gradient/Main.hs b/hsm-dummy-gradient/Main.hs
new file mode 100644
index 0000000..05ead49
--- /dev/null
+++ b/hsm-dummy-gradient/Main.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+import Control.Monad (forM_, forever)
+import Data.Function ((&))
+import Data.Text (Text)
+import Effectful (Eff, (:>), runEff)
+import Effectful.Concurrent (Concurrent, runConcurrent, threadDelay)
+import Effectful.Log (Log, runLog)
+import Effectful.Reader.Static (Reader, ask, runReader)
+import Hsm.Core.App (launch)
+import Hsm.Core.Env (deriveFromYaml)
+import Hsm.PWM (PWMEffect, dutyCycle, runPWM, withPWM)
+import System.IO.Echo (withoutInputEcho)
+
+data Env = Env
+ { name :: Text
+ , pwmPeriod :: Word
+ , stepDelay :: Word
+ }
+
+$(deriveFromYaml ''Env)
+
+pwmLoop ::
+ (Concurrent :> es, Log :> es, PWMEffect :> es, Reader Env :> es)
+ => Eff es ()
+pwmLoop = do
+ env <- ask @Env
+ withPWM
+ $ forever
+ $ forM_ [0,env.pwmPeriod `div` 10 .. env.pwmPeriod]
+ $ \dc -> do
+ threadDelay $ fromIntegral env.stepDelay
+ dutyCycle dc
+
+-- Dummy gradient service:
+-- Simple test for PWM control. Increases duty-cycle gradually on default PWM
+-- channel.
+main :: IO ()
+main =
+ launch @Env "dummy-gradient" withoutInputEcho $ \env logger level ->
+ pwmLoop
+ & runPWM @Env
+ & runConcurrent
+ & runLog env.name logger level
+ & runReader env
+ & runEff
diff --git a/hsm-dummy-gradient/hsm-dummy-gradient.cabal b/hsm-dummy-gradient/hsm-dummy-gradient.cabal
new file mode 100644
index 0000000..1f895a0
--- /dev/null
+++ b/hsm-dummy-gradient/hsm-dummy-gradient.cabal
@@ -0,0 +1,24 @@
+cabal-version: 3.4
+author: Paul Oliver
+build-type: Simple
+maintainer: contact@pauloliver.dev
+name: hsm-dummy-gradient
+version: 0.1.0.0
+
+executable dummy-gradient
+ build-depends:
+ , base
+ , echo
+ , effectful
+ , hsm-core
+ , hsm-pwm
+ , log-effectful
+ , text
+
+ main-is: Main.hs
+ ghc-options: -Wall -Wunused-packages
+
+ if !arch(x86_64)
+ ghc-options: -optl=-mno-fix-cortex-a53-835769
+
+ default-language: GHC2021
diff --git a/hsm-pwm/Hsm/PWM.hs b/hsm-pwm/Hsm/PWM.hs
new file mode 100644
index 0000000..a5e1d27
--- /dev/null
+++ b/hsm-pwm/Hsm/PWM.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Hsm.PWM
+ ( PWMEffect
+ , dutyCycle
+ , withPWM
+ , runPWM
+ ) where
+
+import Data.Text (Text, pack)
+import Effectful (Dispatch(Static), DispatchOf, Eff, IOE, (:>))
+import Effectful.Dispatch.Static qualified as E
+import Effectful.Exception (bracket_)
+import Effectful.Log (Log, localDomain, logTrace_)
+import Effectful.Reader.Static (Reader, ask)
+import GHC.Records (HasField)
+import System.FilePath ((</>))
+import System.Process (callCommand)
+
+-- The following PWMEffect implementation assumes `dtoverlay=pwm` to be set on
+-- `/boot/config.txt`. On the Pi 5, this enables one active PWM on GPIO 18.
+-- This is channel 2, so the address attribute will be 2. Alternative
+-- configurations with more PWM channels are possible. Consult the following
+-- links for more info:
+--
+-- 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 PWMEffect a b
+
+type instance DispatchOf PWMEffect = Static E.WithSideEffects
+
+newtype instance E.StaticRep PWMEffect =
+ PWMEffect ()
+
+domain :: Text
+domain = "pwm"
+
+pwmchip :: FilePath
+pwmchip = "/sys/class/pwm/pwmchip0"
+
+chanIdx :: Word
+chanIdx = 2
+
+chanPath :: FilePath
+chanPath = pwmchip </> ("pwm" <> show chanIdx)
+
+enablePath :: FilePath
+enablePath = chanPath </> "enable"
+
+dutyCycle :: (Log :> es, PWMEffect :> es) => Word -> Eff es ()
+dutyCycle dc = do
+ localDomain domain $ logTrace_ $ "Setting duty cycle to " <> pack (show dc)
+ E.unsafeEff_ $ writeFile dutyCyclePath $ show dc
+ where
+ dutyCyclePath = chanPath </> "duty_cycle"
+
+disable :: (Log :> es, PWMEffect :> es) => Eff es ()
+disable = do
+ dutyCycle 0
+ localDomain domain
+ $ logTrace_
+ $ "Disabling PWM channel " <> pack (show chanIdx)
+ E.unsafeEff_ $ writeFile enablePath "0"
+
+withPWM :: (Log :> es, PWMEffect :> es) => Eff es a -> Eff es a
+withPWM = bracket_ activate disable
+ where
+ activate = do
+ localDomain domain
+ $ logTrace_
+ $ "Enabling PWM channel " <> pack (show chanIdx)
+ E.unsafeEff_ $ writeFile enablePath "1"
+
+runPWM ::
+ forall env es a.
+ ( HasField "pwmPeriod" env Word
+ , IOE :> es
+ , Log :> es
+ , Reader env :> es
+ )
+ => Eff (PWMEffect : es) a
+ -> Eff es a
+runPWM action = E.evalStaticRep (PWMEffect ()) $ bracket_ acquire release action
+ where
+ export = pwmchip </> "export"
+ period = chanPath </> "period"
+ unexport = pwmchip </> "unexport"
+ acquire = do
+ localDomain domain $ do
+ logTrace_
+ $ "Exporting channel "
+ <> pack (show chanIdx)
+ <> " on chip "
+ <> pack pwmchip
+ E.unsafeEff_ $ writeFile export $ show chanIdx
+ logTrace_ "Giving UDEV a chance to settle"
+ E.unsafeEff_ $ callCommand "udevadm settle"
+ env <- ask @env
+ logTrace_ $ "Fixing period to " <> pack (show env.pwmPeriod)
+ E.unsafeEff_ $ writeFile period $ show env.pwmPeriod
+ disable
+ release = do
+ disable
+ localDomain domain
+ $ logTrace_
+ $ "Unexporting channel "
+ <> pack (show chanIdx)
+ <> " on chip "
+ <> pack pwmchip
+ E.unsafeEff_ $ writeFile unexport $ show chanIdx
diff --git a/hsm-pwm/hsm-pwm.cabal b/hsm-pwm/hsm-pwm.cabal
new file mode 100644
index 0000000..4190975
--- /dev/null
+++ b/hsm-pwm/hsm-pwm.cabal
@@ -0,0 +1,19 @@
+cabal-version: 3.4
+author: Paul Oliver
+build-type: Simple
+maintainer: contact@pauloliver.dev
+name: hsm-pwm
+version: 0.1.0.0
+
+library
+ build-depends:
+ , base
+ , effectful-core
+ , filepath
+ , log-effectful
+ , process
+ , text
+
+ exposed-modules: Hsm.PWM
+ ghc-options: -Wall -Wunused-packages
+ default-language: GHC2021
diff --git a/servconf.yaml b/servconf.yaml
index a014ade..b2b01cd 100644
--- a/servconf.yaml
+++ b/servconf.yaml
@@ -12,6 +12,10 @@ dummy-fail:
alive: 1000000
name: fail
pubEp: tcp://0.0.0.0:10002
+dummy-gradient:
+ name: gradient
+ pwmPeriod: 1000000
+ stepDelay: 100000
dummy-poller:
name: poller
period: 3000000
diff --git a/stack.yaml b/stack.yaml
index 28543fc..fbb6592 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -7,9 +7,11 @@ packages:
- hsm-core
- hsm-dummy-blinker
- hsm-dummy-fail
+ - hsm-dummy-gradient
- hsm-dummy-poller
- hsm-dummy-pulser
- hsm-dummy-receiver
- hsm-gpio
+ - hsm-pwm
- hsm-status
snapshot: lts-23.3