aboutsummaryrefslogtreecommitdiff
path: root/hsm-drive/Hsm/Drive.hs
blob: cf8e4f1a05ed7412e311d1a6770c6304b8841723 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
{-# LANGUAGE TypeFamilies #-}

module Hsm.Drive
  ( Drive
  , Direction (..)
  , Angle (..)
  , Speed (..)
  , Duration
  , Action (..)
  , drive
  , runDrive
  )
where

import Control.Concurrent (threadDelay)
import Control.Monad (forM_)
import Effectful (Dispatch (Static), DispatchOf, Eff, IOE, (:>))
import Effectful.Dispatch.Static (SideEffects (WithSideEffects), StaticRep, evalStaticRep, unsafeEff_)
import Effectful.Exception (AsyncException, bracket_, handle)
import Hsm.GPIO (GPIO, GPIOPin (..), active, inactive, setPins)
import Hsm.Log (Logs, Severity (Attention, Info, Trace), logMsg)
import Hsm.PWM (PWM, PWMChannel (..), setCycleDuration)

data Drive (a :: * -> *) (b :: *)

type instance DispatchOf Drive = Static WithSideEffects

newtype instance StaticRep Drive
  = Drive ()

-- Defines the complete action space for omnidirectional robots:
-- - 8-directional movement (cardinal + intercardinal)
-- - Bidirectional rotation (clockwise/counter-clockwise)
data Direction
  = N
  | NE
  | E
  | SE
  | S
  | SW
  | W
  | NW
  deriving Show

data Angle
  = CCW
  | CW
  deriving Show

data Speed
  = Slow8
  | Slow4
  | Slow2
  | Slow
  | Fast
  | Top
  deriving Show

type Duration = Float

data Action
  = Move Direction Speed Duration
  | Tilt Angle Speed Duration
  | Stop Duration
  deriving Show

-- 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). Returned
-- values represent the duration of a whole PWM cycle in nanoseconds.
cycleDuration :: Speed -> Int
cycleDuration Slow8 = 8000000000 `div` 3200 -- 1/8 revs/s
cycleDuration Slow4 = 4000000000 `div` 3200 -- 1/4 revs/s
cycleDuration Slow2 = 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

ms1 :: GPIOPin
ms1 = GPIO2

ms2 :: GPIOPin
ms2 = GPIO3

ms3 :: GPIOPin
ms3 = GPIO4

notReset :: GPIOPin
notReset = GPIO27

notSleep :: GPIOPin
notSleep = GPIO22

dir1 :: GPIOPin
dir1 = GPIO10

dir2 :: GPIOPin
dir2 = GPIO9

dir3 :: GPIOPin
dir3 = GPIO11

dir4 :: GPIOPin
dir4 = GPIO5

diag2 :: GPIOPin
diag2 = GPIO6

diag1 :: GPIOPin
diag1 = GPIO13

step :: PWMChannel
step = PWM3

-- Executes a sequence of drive actions with interruption support
-- Wakes motors from SLEEP mode during execution and guarantees return to SLEEP
-- mode upon completion or interruption.
drive :: (GPIO :> es, IOE :> es, Logs '["drive", "gpio", "pwm"] es, PWM :> es) => [Action] -> Eff es ()
drive actions =
  bracket_ awaken sleep . handle handler . forM_ actions $ \action -> do
    logMsg @"drive" Trace $ "Running action: " <> show action
    runAction action
  where
    pinsDir = [dir1, dir2, dir3, dir4]
    pinsDiag = [diag1, diag2]
    pinDiagNE = [diag1]
    pinDiagSE = [diag2]
    pinDiagSW = [diag1]
    pinDiagNW = [diag2]
    pinsN = [dir2, dir4]
    pinsE = [dir1, dir2]
    pinsS = [dir1, dir3]
    pinsW = [dir3, dir4]
    pinsCCW = [dir1, dir2, dir3, dir4]
    pinsCW = []
    -- 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.
    awaken = do
      logMsg @"drive" Trace "Enabling drivers"
      setPins [notSleep] active
      logMsg @"drive" Trace "Allowing drivers to come out of sleep mode"
      unsafeEff_ $ threadDelay 1000
    sleep = do
      logMsg @"drive" Trace "Disabling drivers"
      setPins [notSleep] inactive
    -- Catches any asynchronous exceptions during the execution of commands.
    -- If an exception occurs, the system will log the error and ensure that
    -- all pins are deactivated.
    endAction = do
      setCycleDuration step 0
      setPins (pinsDir <> pinsDiag) inactive
    handler exception = do
      logMsg @"drive" Attention $ "Async exception caught while action was running: " <> show @AsyncException exception
      endAction
    -- 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.
    delay seconds = unsafeEff_ . threadDelay . round $ seconds * 1000000
    togglePins speed duration pins = do
      setCycleDuration step $ cycleDuration speed
      setPins pins active
      delay duration
      endAction
    runAction (Move direction speed duration) =
      togglePins speed duration $
        case direction of
          N -> pinsN <> pinsDiag
          NE -> pinsN <> pinDiagNE
          E -> pinsE <> pinsDiag
          SE -> pinsS <> pinDiagSE
          S -> pinsS <> pinsDiag
          SW -> pinsS <> pinDiagSW
          W -> pinsW <> pinsDiag
          NW -> pinsN <> pinDiagNW
    runAction (Tilt angle speed duration) =
      togglePins speed duration $
        case angle of
          CCW -> pinsCCW <> pinsDiag
          CW -> pinsCW <> pinsDiag
    -- 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) = delay duration

runDrive :: (GPIO :> es, IOE :> es, Logs '["drive", "gpio"] es) => Eff (Drive : es) a -> Eff es a
runDrive = evalStaticRep (Drive ()) . bracket_ enterActive exitActive
  where
    alwaysActive = [ms1, ms2, ms3, notReset]
    enterActive = do
      logMsg @"drive" Info "Entering active drive state"
      setPins alwaysActive active
    exitActive = do
      logMsg @"drive" Info "Exiting active drive state"
      setPins alwaysActive inactive