src-lib/Therac25.hs
TPhase CollimatorPosition CollimatorPositionInt cpMap BeamTypeInt BeamType btMap ExtCallType ExtCallTypeInt ectMap ExternalCall BeamEnergy MEOS newMEOS makeMEOSFromCParams TheracState prescribedDose newTherac WrappedComms schedulerTick housekeeperTick numMagnets hysteresisDelay pTimePoll turntableTravelPasses maxPauses spuriousPausePercent modifyTherac setFieldInStruct setTPhase readFieldFromStruct resetTherac resetRequested requestedTurntable keyboardHandler editMEOS cursorToCommandLine beamOn proceedTreatment fieldLight handleExternalCalls treat setupDone waitForProceedOrReset waitForReset Delivery beamPhysics zapTheSpecimen spuriousMessage externalCallWrap startMachine StateInfoRequest SIRInt siriMap stateInfo requestStateInfo setupTest lmtchk chkcol hand housekeeper setBendingMagnetFlag unsetBendingMagnetFlag pTime magnet outputParameters datent
1{-# LANGUAGE CPP #-}2{-# LANGUAGE FlexibleInstances #-}3{-# LANGUAGE ForeignFunctionInterface #-}4{-# LANGUAGE FunctionalDependencies #-}5{-# LANGUAGE MultiWayIf #-}6{-# LANGUAGE StrictData #-}7{-# LANGUAGE TemplateHaskell #-}89-- A simulation of the Therac-25 treatment software, following10-- N. G. Leveson and C. S. Turner, "An Investigation of the Therac-25 Accidents",11-- IEEE Computer 26(7), July 1993.12-- Text in "double quotes" in the comments below is quoted from that paper.13--14-- The two software races the paper blames for the Tyler (Malfunction 54) and15-- Yakima (Class3 overflow) overdoses are reproduced ON PURPOSE. Everything else16-- (the plumbing between this module and the UIs) is meant to be boringly correct.17-- README.md lists which details come from the paper and which are assumptions.1819module Therac25 (externalCallWrap, startMachine, requestStateInfo, theracState, externalCalls, TheracState (..), WrappedComms (..)) where2021import Control.Concurrent (forkIO, threadDelay)22import Control.Concurrent.STM23 ( STM,24 TChan,25 TMVar,26 atomically,27 newTChan,28 newTMVarIO,29 putTMVar,30 readTChan,31 readTMVar,32 retry,33 takeTMVar,34 writeTChan,35 )36import Control.Exception (SomeException, catch)37import Control.Lens (ASetter, Getting, makeFields, (%~), (&), (.~), (^.))38import Control.Lens.Tuple (Field1 (_1), Field2 (_2))39import Control.Monad (forever, unless, when)40import Data.Bits (clearBit, setBit)41import Data.Map.Strict qualified as M42import Data.Word (Word16, Word8)43import Foreign.C.String (CString, newCString)44import Foreign.C.Types ()45import Foreign.StablePtr46 ( StablePtr,47 deRefStablePtr,48 newStablePtr,49 )50import System.Random (randomRIO)5152data TPhase = TP_Reset | TP_Datent | TP_SetupDone | TP_SetupTest | TP_PatientTreatment | TP_PauseTreatment | TP_TerminateTreatment | TP_Date_Time_IDChanges53 deriving (Eq, Show)5455-- "three cardinal turntable positions: electron beam, X ray, and field light".56-- CollimatorPositionUndefined only appears on the console side and means "nothing requested yet".57-- CollimatorPositionFieldLight is only reachable through the hand control (ExtCallFieldLight).58data CollimatorPosition = CollimatorPositionXRay | CollimatorPositionElectronBeam | CollimatorPositionUndefined | CollimatorPositionFieldLight59 deriving (Eq, Show)6061type CollimatorPositionInt = Int6263cpMap :: M.Map CollimatorPositionInt CollimatorPosition64cpMap = M.fromList [(1, CollimatorPositionXRay), (2, CollimatorPositionElectronBeam), (3, CollimatorPositionUndefined)]6566type BeamTypeInt = Int6768data BeamType = BeamTypeXRay | BeamTypeElectron | BeamTypeUndefined69 deriving (Eq, Show)7071btMap :: M.Map BeamTypeInt BeamType72btMap = M.fromList [(1, BeamTypeXRay), (2, BeamTypeElectron), (3, BeamTypeUndefined)]7374-- ExtCallToggleDatentComplete and ExtCallToggleEditingTakingPlace keep their old names and numbers75-- for the UIs, but they SET their flag now (see `keyboardHandler`)76data ExtCallType = ExtCallSendMEOS | ExtCallToggleDatentComplete | ExtCallToggleEditingTakingPlace | ExtCallReset | ExtCallProceed | ExtCallHardReset | ExtCallSet | ExtCallFieldLight | ExtCallBeamOn | ExtCallUseBeamOnKey | ExtCallPrescribeDose7778type ExtCallTypeInt = Int7980ectMap :: M.Map ExtCallTypeInt ExtCallType81ectMap = M.fromList [(1, ExtCallSendMEOS), (2, ExtCallToggleDatentComplete), (3, ExtCallToggleEditingTakingPlace), (4, ExtCallReset), (5, ExtCallProceed), (6, ExtCallHardReset), (7, ExtCallSet), (8, ExtCallFieldLight), (9, ExtCallBeamOn), (10, ExtCallUseBeamOnKey), (11, ExtCallPrescribeDose)]8283data ExternalCall = ExternalCall84 { _ecType :: ExtCallType,85 _ecMEOS :: MEOS,86 _ecValue :: Int -- ExtCallPrescribeDose: monitor units87 }8889type BeamEnergy = Int9091-- Mode/Energy Offset. In the real machine a 2-byte variable: one byte used by Datent to set the92-- beam parameters, the other used by Hand to position the turntable.93data MEOS = MEOS94 { _mEOSDatentParams :: (BeamType, BeamEnergy),95 _mEOSHandParams :: CollimatorPosition96 }97 deriving (Eq, Show)9899$(makeFields ''MEOS)100101newMEOS :: MEOS102newMEOS = MEOS (BeamTypeUndefined, 1477) CollimatorPositionUndefined103104-- Nothing for values the C header doesn't define (including its own *CheekyPadding = 0).105makeMEOSFromCParams :: BeamTypeInt -> CollimatorPositionInt -> BeamEnergy -> Maybe MEOS106makeMEOSFromCParams bti cpi be = do107 bt <- M.lookup bti btMap108 cp <- M.lookup cpi cpMap109 pure $ MEOS (bt, be) cp110111data TheracState = TheracState112 { _theracStateClass3 :: Word8, -- one byte, incremented by every pass through Set-Up Test, so it is 0 on every 256th pass113 _theracStateFMal :: Word16, -- F$mal, the interlock/malfunction word. Bit 9 = upper collimator inconsistent (written by `chkcol`), read by Set-Up Test114 _theracStateConsoleMeos :: MEOS, -- set by the keyboard handler; read by Datent (beam) and by Hand (turntable)115 _theracStateTPhase :: TPhase, -- used by `treat` - treatment phase116 _theracStateDataEntryComplete :: Bool, -- set by the keyboard handler when the cursor reaches the command line. Only a reset clears it117 _theracStateBendingMagnetFlag :: Bool, -- set by `magnet`, cleared by `pTime` (at the end of the FIRST pTime - that's the bug)118 _theracStateEditingTakingPlace :: Bool, -- set by the keyboard handler when the prescription is edited; only a reset clears it119 _theracStateHardwareMeos :: MEOS, -- what the machine is really doing: datentParams = beam parameters Datent output, handParams = where the turntable physically is120 _theracStateTreatmentOutcome :: String, -- message for the last beam-on attempt121 _theracStateResetPending :: Bool, -- set by the keyboard handler (R)122 _theracStateMalfunctionCount :: Int, -- pauses during this treatment; the 5th one suspends123 _theracStateTreatmentSuspended :: Bool, -- "treatment suspend, which required a complete machine reset to restart"124 _theracStateAwaitingSet :: Bool, -- the hand control put the turntable in field-light position; console says "PRESS SET BUTTON"125 _theracStateTurntableTarget :: CollimatorPosition, -- where the turntable motor is driving to126 _theracStateTurntableEta :: Int, -- housekeeper passes until it gets there127 _theracStateDisplayedDose :: Int, -- monitor units the dose monitor SHOWED for the last attempt128 _theracStatePatientDose :: Int, -- rads the patient ACTUALLY received since the last reset. The real console had no way to show this129 _theracStatePrescribedMu :: Int, -- monitor units the console prescribed; a normal treatment delivers and shows this130 _theracStateBeamOnKey :: Bool, -- the UI has a "B" command (ExtCallUseBeamOnKey): Set-Up Done waits for it instead of firing by itself131 _theracStateMagnetBeingSet :: Int -- which bending magnet `magnet` is setting, 0 when none; only for the UIs to show132 }133 deriving (Eq, Show)134135$(makeFields ''TheracState)136137-- "Typical single therapeutic doses are in the 200-rad range". Used until the UI sends138-- ExtCallPrescribeDose.139prescribedDose :: Int140prescribedDose = 200141142newTherac :: TheracState143newTherac =144 TheracState145 { _theracStateClass3 = 1,146 _theracStateFMal = 0,147 _theracStateConsoleMeos = newMEOS,148 _theracStateTPhase = TP_Datent,149 _theracStateDataEntryComplete = False,150 _theracStateBendingMagnetFlag = False,151 _theracStateEditingTakingPlace = False,152 _theracStateHardwareMeos = newMEOS & handParams .~ CollimatorPositionFieldLight, -- power up with no beam expected153 _theracStateTreatmentOutcome = "",154 _theracStateResetPending = False,155 _theracStateMalfunctionCount = 0,156 _theracStateTreatmentSuspended = False,157 _theracStateAwaitingSet = False,158 _theracStateTurntableTarget = CollimatorPositionFieldLight,159 _theracStateTurntableEta = 0,160 _theracStateDisplayedDose = 0,161 _theracStatePatientDose = 0,162 _theracStatePrescribedMu = prescribedDose,163 _theracStateBeamOnKey = False,164 _theracStateMagnetBeingSet = 0165 }166167data WrappedComms = WrappedComms168 { _wrappedCommsTheracState :: TMVar TheracState,169 _wrappedCommsExternalCalls :: TChan ExternalCall170 }171172$(makeFields ''WrappedComms)173174-- timing175176-- "Tasks are initiated every 0.1 second"177schedulerTick :: Int178schedulerTick = 100000179180-- The housekeeper runs twice per Treat tick so it always gets a look at Class3 between two181-- Set-Up Test passes. (The paper only says tasks run every 0.1 s; see README.)182housekeeperTick :: Int183housekeeperTick = schedulerTick `div` 2184185-- "Setting the bending magnets takes about 8 seconds." The paper only says "several magnets".186numMagnets :: Int187numMagnets = 4188189hysteresisDelay :: Int190hysteresisDelay = 2000000191192pTimePoll :: Int193pTimePoll = 20000194195-- Not in the paper. This is how long the turntable is still in the wrong place after it is196-- told to move, i.e. how wide the Yakima window is. See README.197turntableTravelPasses :: Int198turntableTravelPasses = 2000000 `div` housekeeperTick199200-- "This convenient and simple feature could be invoked a maximum of five times before the201-- machine automatically suspended treatment"202maxPauses :: Int203maxPauses = 5204205-- Not in the paper; "Malfunction messages were commonplace". High enough that P becomes a206-- habit, low enough that normal treatments finish.207spuriousPausePercent :: Int208spuriousPausePercent = 30209210-- all of the helper functions that aren't part of the therac software are mercifully doing most things atomically211212modifyTherac :: TMVar TheracState -> (TheracState -> TheracState) -> STM ()213modifyTherac ts f = takeTMVar ts >>= \l -> putTMVar ts $! f l214215setFieldInStruct :: ASetter a1 a1 a2 b -> b -> TMVar a1 -> STM ()216setFieldInStruct ff fv ts = takeTMVar ts >>= \l -> putTMVar ts $! (ff .~ fv $ l)217218setTPhase :: (HasTPhase a1 b) => b -> TMVar a1 -> STM ()219setTPhase = setFieldInStruct tPhase220221readFieldFromStruct :: Getting b s b -> TMVar s -> STM b222readFieldFromStruct ff ts = readTMVar ts >>= \l -> return $ l ^. ff223224-- soft reset (R), when Treat gets to it: a new treatment. Class3 is never re-initialised, and225-- the turntable physically stays where it is. Whether the UI has a "B" command is not the226-- treatment's business. What the console holds was already cleared when R was typed227-- (`resetRequested`), so anything in it now was entered since, for the next prescription.228resetTherac :: TMVar TheracState -> STM ()229resetTherac ts = modifyTherac ts $ \s ->230 newTherac231 & class3 .~ (s ^. class3)232 & hardwareMeos . handParams .~ (s ^. hardwareMeos . handParams)233 & turntableTarget .~ (s ^. hardwareMeos . handParams)234 & beamOnKey .~ (s ^. beamOnKey)235 & consoleMeos .~ (s ^. consoleMeos)236 & dataEntryComplete .~ (s ^. dataEntryComplete)237 & editingTakingPlace .~ (s ^. editingTakingPlace)238 & prescribedMu .~ (s ^. prescribedMu)239240-- R typed: "an 'R' reset command must be used and the whole prescription reentered". The241-- keyboard handler drops the prescription at once; Treat resets the machine when it gets there,242-- which is only after the magnets if they are being set (ASSUMPTION, see README).243resetRequested :: TheracState -> TheracState244resetRequested s =245 s246 & resetPending .~ True247 & consoleMeos .~ newMEOS248 & dataEntryComplete .~ False249 & editingTakingPlace .~ False250 & prescribedMu .~ prescribedDose251252-- the turntable position the console is asking for; the UIs usually send it, otherwise it follows the mode253requestedTurntable :: MEOS -> CollimatorPosition254requestedTurntable m = case m ^. handParams of255 CollimatorPositionUndefined -> case m ^. datentParams . _1 of256 BeamTypeXRay -> CollimatorPositionXRay257 BeamTypeElectron -> CollimatorPositionElectronBeam258 BeamTypeUndefined -> CollimatorPositionUndefined259 p -> p260261-- BEGIN virtual task keyboardhandler262263keyboardHandler :: TMVar TheracState -> ExternalCall -> STM ()264keyboardHandler ts (ExternalCall ect m v) = case ect of265 ExtCallSendMEOS -> editMEOS m ts266 ExtCallToggleDatentComplete -> cursorToCommandLine ts267 ExtCallToggleEditingTakingPlace -> setFieldInStruct editingTakingPlace True ts268 ExtCallReset -> modifyTherac ts resetRequested269 ExtCallProceed -> proceedTreatment ts270 ExtCallHardReset -> modifyTherac ts $ \s -> newTherac & beamOnKey .~ (s ^. beamOnKey)271 ExtCallSet -> setFieldInStruct awaitingSet False ts272 ExtCallFieldLight -> fieldLight ts273 ExtCallBeamOn -> beamOn ts274 ExtCallUseBeamOnKey -> setFieldInStruct beamOnKey True ts275 ExtCallPrescribeDose -> when (v > 0) $ setFieldInStruct prescribedMu v ts276277-- An edit of the prescription. While Datent is running it just lands in MEOS - that's the Tyler278-- race. Once Datent has exited, the edit sends the machine back through Datent. The paper doesn't279-- say what caught slow edits, only that "data-entry speed during editing was the key factor"280-- (ASSUMPTION, see README).281editMEOS :: MEOS -> TMVar TheracState -> STM ()282editMEOS m ts = modifyTherac ts $ \s ->283 let s' = s & consoleMeos .~ m & editingTakingPlace .~ True284 in if m /= s ^. consoleMeos && s ^. tPhase `elem` [TP_SetupTest, TP_SetupDone, TP_PauseTreatment]285 then s' & tPhase .~ TP_Datent286 else s'287288-- "the data-entry completion variable only indicates that the cursor has been down to the289-- command line, not that it is still there. A potential race condition is set up."290-- So: set it, and nothing but a reset clears it.291-- (editingTakingPlace is left alone: on the PDP-11 an edit took keystrokes, long enough for Ptime to292-- see the flag, but a UI can send "edit" and "back to the command line" in the same millisecond.)293cursorToCommandLine :: TMVar TheracState -> STM ()294cursorToCommandLine = setFieldInStruct dataEntryComplete True295296-- "She hit the one-key command 'B' (for 'beam on') to begin the treatment." Only means297-- anything once the console says BEAM READY.298beamOn :: TMVar TheracState -> STM ()299beamOn ts = modifyTherac ts $ \s ->300 if s ^. tPhase == TP_SetupDone then s & tPhase .~ TP_PatientTreatment else s301302proceedTreatment :: TMVar TheracState -> STM ()303proceedTreatment ts = do304 tp <- readFieldFromStruct tPhase ts305 case tp of306 TP_PauseTreatment -> setTPhase TP_PatientTreatment ts307 _ -> return ()308309-- The hand control in the treatment room rotates the turntable to the field-light position to310-- check the patient's position; "The console displays the message 'Press set button' while311-- the turntable is in the field-light position." At Yakima this happened during a pause312-- between exposures; going back to Set-Up Test from a pause is an ASSUMPTION.313fieldLight :: TMVar TheracState -> STM ()314fieldLight ts = modifyTherac ts $ \s -> case s ^. tPhase of315 TP_Datent -> s & awaitingSet .~ True316 TP_SetupTest -> s & awaitingSet .~ True317 TP_SetupDone -> s & awaitingSet .~ True & tPhase .~ TP_SetupTest318 TP_PauseTreatment -> s & awaitingSet .~ True & tPhase .~ TP_SetupTest319 _ -> s320321-- END virtual task keyboardhandler322323handleExternalCalls :: TMVar TheracState -> TChan ExternalCall -> IO ()324handleExternalCalls ts ecc = forever $ atomically $ readTChan ecc >>= keyboardHandler ts325326-- task - treatment monitor - the supervisor task basically327-- "Treat ... directs and monitors patient setup and treatment via eight operating phases.328-- These are called as subroutines, depending on the value of the Tphase control variable.329-- Following the execution of a particular subroutine, Treat reschedules itself."330treat :: TMVar TheracState -> IO ()331treat ts = forever $ do332 threadDelay schedulerTick333 curTPhase <- atomically $ readFieldFromStruct tPhase ts334 case curTPhase of335 TP_Reset -> atomically $ resetTherac ts336 TP_Datent -> datent ts337 TP_SetupDone -> atomically $ setupDone ts338 TP_SetupTest -> atomically $ setupTest ts339 TP_PatientTreatment -> zapTheSpecimen ts340 TP_PauseTreatment -> waitForProceedOrReset ts341 TP_TerminateTreatment -> waitForReset ts342 TP_Date_Time_IDChanges -> return () -- this + a bunch of other purely cosmetic things will be implemented elsewhere (the c++ class or the UI in unreal engine probably)343344-- The console says "BEAM READY". A UI with a "B" command (ExtCallUseBeamOnKey) fires the beam345-- with ExtCallBeamOn; for the others Begin doubles as the "B" key.346setupDone :: TMVar TheracState -> STM ()347setupDone ts = do348 s <- readTMVar ts349 if350 | s ^. resetPending -> setTPhase TP_Reset ts351 | not (s ^. beamOnKey) -> setTPhase TP_PatientTreatment ts352 | otherwise -> return ()353354waitForProceedOrReset :: TMVar TheracState -> IO ()355waitForProceedOrReset ts = atomically $ do356 s <- readTMVar ts357 if s ^. resetPending358 then setTPhase TP_Reset ts359 else when (s ^. tPhase == TP_PauseTreatment) retry360361waitForReset :: TMVar TheracState -> IO ()362waitForReset ts = atomically $ do363 tsrp <- readFieldFromStruct resetPending ts364 if tsrp then setTPhase TP_Reset ts else retry365366-- BEGIN zapping367368-- What the beam physically does, given the beam parameters Datent output and where the turntable369-- really is. Note that the software never compares these two: "The software appears to include370-- no checks to detect such an incompatibility."371data Delivery372 = Treated -- correct setup373 | TylerOverdose -- X-ray current, turntable in electron position: no target, no flattener374 | YakimaOverdose -- X-ray current, turntable in field-light position: no target, no scanning, a mirror in the beam375 | NuisancePause -- electron current with the wrong accessories: not documented, modelled as harmless376377beamPhysics :: BeamType -> CollimatorPosition -> Delivery378beamPhysics BeamTypeXRay CollimatorPositionXRay = Treated379beamPhysics BeamTypeElectron CollimatorPositionElectronBeam = Treated380-- "Much greater electron-beam current is required for photon mode (some 100 times greater than381-- that for electron therapy)" because the flattener is "a very efficient attenuator"382beamPhysics BeamTypeXRay CollimatorPositionElectronBeam = TylerOverdose383beamPhysics BeamTypeXRay _ = YakimaOverdose384beamPhysics _ _ = NuisancePause385386zapTheSpecimen :: TMVar TheracState -> IO ()387zapTheSpecimen ts = do388 spuriousRoll <- randomRIO (1, 100) :: IO Int389 spuriousKind <- randomRIO (0, 3) :: IO Int390 channel <- randomRIO (1, 63) :: IO Int391 -- "After-the-fact simulations of the accident revealed possible doses of 16,500 to 25,000 rads"392 tylerRads <- randomRIO (16500, 25000) :: IO Int393 -- "the dose delivered under these conditions - that is, when the turntable was in the394 -- field-light position - was on the order of 4,000 to 5,000 rads"395 yakimaRads <- randomRIO (4000, 5000) :: IO Int396 atomically $ modifyTherac ts $ \s ->397 let hw = s ^. hardwareMeos398 pause msg shown rads =399 let mc = s ^. malfunctionCount + 1400 suspend = mc >= maxPauses401 in s402 & treatmentOutcome .~ msg403 & displayedDose .~ shown404 & patientDose %~ (+ rads)405 & malfunctionCount .~ mc406 & treatmentSuspended .~ suspend407 & tPhase .~ (if suspend then TP_TerminateTreatment else TP_PauseTreatment)408 in case beamPhysics (hw ^. datentParams . _1) (hw ^. handParams) of409 -- "Malfunction 54 ... a 'dose input 2' error ... a dose had been delivered that was either too410 -- high or too low." The saturated ion chamber read low: "6 monitor units delivered, whereas the411 -- operator had requested 202 monitor units", every time P was pressed.412 TylerOverdose -> pause "MALFUNCTION 54" 6 tylerRads413 -- "the console displayed no dose or dose rate. After 5 or 6 seconds, the unit shut down with a414 -- pause" ... "The machine paused again, this time displaying 'flatness' on the reason line."415 -- (there's no ion chamber in the field-light position)416 YakimaOverdose -> pause "FLATNESS" 0 yakimaRads417 NuisancePause -> pause "LOW DOSE RATE" 0 0418 Treated419 -- simulate shitty fucking computer doodad breaking all the time to prime people to P(roceed) repeatedly and carelessly420 | spuriousRoll <= spuriousPausePercent -> pause (spuriousMessage spuriousKind channel) 0 0421 | otherwise ->422 s423 & treatmentOutcome .~ "TREATMENT OK"424 & displayedDose .~ (s ^. prescribedMu)425 & patientDose %~ (+ (s ^. prescribedMu))426 & tPhase .~ TP_TerminateTreatment427428-- "They would give messages of low dose rate, V-tilt, H-tilt, and other things", and "some429-- merely consisted of the word 'malfunction' followed by a number from 1 to 64 denoting an430-- analog/digital channel number". 54 is kept for the real thing.431spuriousMessage :: Int -> Int -> String432spuriousMessage kind channel = case kind of433 0 -> "LOW DOSE RATE"434 1 -> "H-TILT"435 2 -> "V-TILT"436 _ -> "MALFUNCTION " ++ show (if channel >= 54 then channel + 1 else channel)437438-- END zapping439440#ifdef mingw32_HOST_OS441foreign export stdcall externalCallWrap :: StablePtr WrappedComms -> ExtCallTypeInt -> BeamTypeInt -> CollimatorPositionInt -> BeamEnergy -> IO ()442#else443foreign export ccall externalCallWrap :: StablePtr WrappedComms -> ExtCallTypeInt -> BeamTypeInt -> CollimatorPositionInt -> BeamEnergy -> IO ()444#endif445-- Unknown values are dropped here instead of being stored for some other thread to trip over446-- (an uncaught exception in a foreign export takes the host process down with it).447externalCallWrap :: StablePtr WrappedComms -> ExtCallTypeInt -> BeamTypeInt -> CollimatorPositionInt -> BeamEnergy -> IO ()448externalCallWrap mywc ecti bti cpi be =449 ( do450 mywc' <- deRefStablePtr mywc451 let send = atomically . writeTChan (_wrappedCommsExternalCalls mywc')452 case M.lookup ecti ectMap of453 Nothing -> return ()454 Just ExtCallSendMEOS -> mapM_ (\m -> send (ExternalCall ExtCallSendMEOS m 0)) (makeMEOSFromCParams bti cpi be)455 Just ect -> send (ExternalCall ect newMEOS be)456 )457 `catch` \(_ :: SomeException) -> return ()458459-- external start machine460-- hs_exit() will probably kill children threads ?? not sure how else to keep this alive and return from the call on c++ caller's side. need to test461#ifdef mingw32_HOST_OS462foreign export stdcall startMachine :: IO (StablePtr WrappedComms)463#else464foreign export ccall startMachine :: IO (StablePtr WrappedComms)465#endif466startMachine :: IO (StablePtr WrappedComms)467startMachine = do468 ts <- newTMVarIO newTherac469 ecc <- atomically newTChan470 _ <- forkIO $ handleExternalCalls ts ecc471 _ <- forkIO $ treat ts472 _ <- forkIO $ housekeeper ts473 newStablePtr $ WrappedComms ts ecc474475data StateInfoRequest = RequestTreatmentOutcome | RequestActiveSubsystem | RequestTreatmentState | RequestReason | RequestBeamMode | RequestBeamEnergy | RequestDumpFullState | RequestClass3 | RequestTurntablePosition | RequestDisplayedDose | RequestPatientDose | RequestSetButtonPrompt | RequestMagnetBeingSet | RequestBendingMagnetFlag476477type SIRInt = Int478479siriMap :: M.Map Int StateInfoRequest480siriMap = M.fromList [(1, RequestTreatmentOutcome), (2, RequestActiveSubsystem), (3, RequestTreatmentState), (4, RequestReason), (5, RequestBeamMode), (6, RequestBeamEnergy), (7, RequestDumpFullState), (8, RequestClass3), (9, RequestTurntablePosition), (10, RequestDisplayedDose), (11, RequestPatientDose), (12, RequestSetButtonPrompt), (13, RequestMagnetBeingSet), (14, RequestBendingMagnetFlag)]481482stateInfo :: TheracState -> SIRInt -> String483stateInfo ts' siri = case M.lookup siri siriMap of484 Just RequestTreatmentOutcome -> ts' ^. treatmentOutcome485 Just RequestActiveSubsystem -> if ts' ^. dataEntryComplete then "TREAT" else "DATA ENTRY"486 Just RequestTreatmentState -> show $ ts' ^. tPhase487 Just RequestReason -> let to = ts' ^. treatmentOutcome in if to == "TREATMENT OK" || to == "" then "OPERATOR" else to488 Just RequestBeamMode -> show $ ts' ^. hardwareMeos . datentParams . _1489 Just RequestBeamEnergy -> show $ ts' ^. hardwareMeos . datentParams . _2490 Just RequestDumpFullState -> show ts'491 Just RequestClass3 -> show $ ts' ^. class3492 Just RequestTurntablePosition -> show $ ts' ^. hardwareMeos . handParams493 Just RequestDisplayedDose -> show $ ts' ^. displayedDose494 Just RequestPatientDose -> show $ ts' ^. patientDose495 Just RequestSetButtonPrompt -> if ts' ^. awaitingSet then "PRESS SET BUTTON" else ""496 Just RequestMagnetBeingSet -> show $ ts' ^. magnetBeingSet497 Just RequestBendingMagnetFlag -> if ts' ^. bendingMagnetFlag then "1" else "0"498 Nothing -> ""499500-- external return requested state info. The string is malloc'd; free it with free_state_info.501#ifdef mingw32_HOST_OS502foreign export stdcall requestStateInfo :: StablePtr WrappedComms -> SIRInt -> IO CString503#else504foreign export ccall requestStateInfo :: StablePtr WrappedComms -> SIRInt -> IO CString505#endif506requestStateInfo :: StablePtr WrappedComms -> SIRInt -> IO CString507requestStateInfo mywc siri =508 ( do509 mywc' <- deRefStablePtr mywc510 ts' <- atomically $ readTMVar (_wrappedCommsTheracState mywc')511 newCString $ stateInfo ts' siri512 )513 `catch` \(_ :: SomeException) -> newCString ""514515-- BEGIN TP_SetupTest phase516517-- `treat` Set-Up Test subroutine. "Every pass through the Set-Up Test routine increments the upper518-- collimator position check, a shared variable called Class3. If Class3 is nonzero, there is an519-- inconsistency and treatment should not proceed." ... "After setting the Class3 variable, Set-Up520-- Test next checks for any malfunctions in the system by checking another shared variable ...521-- called F$mal ... When F$mal is zero ... the Set-Up Test subroutine sets the Tphase variable522-- equal to 2". It also waits for the set button while the field light is on.523-- The AECL fix: "the Class3 variable is set to some fixed nonzero value each time through Set-Up524-- Test instead of being incremented."525setupTest :: TMVar TheracState -> STM ()526setupTest ts = modifyTherac ts $ \s ->527 let s' = s & class3 %~ (+ 1) -- a Word8, so 255 + 1 == 0, just like the PDP-11 byte528 in if529 | s ^. resetPending -> s' & tPhase .~ TP_Reset530 | not (s ^. awaitingSet) && s ^. fMal == 0 -> s' & tPhase .~ TP_SetupDone531 | otherwise -> s'532533-- END TP_SetupTest phase534535-- BEGIN `housekeeper` stuff536537-- `housekeeper` subroutine - "analog/digital limit checking". "Lmtchk first checks the Class3538-- variable. If Class3 contains a nonzero value, Lmtchk calls the Check Collimator (Chkcol)539-- subroutine. If Class3 contains zero, Chkcol is bypassed and the upper collimator position check540-- is not performed." F$mal is rebuilt on every pass, so a bypassed check leaves bit 9 clear541-- (ASSUMPTION, see README).542lmtchk :: TMVar TheracState -> STM ()543lmtchk ts = modifyTherac ts $ \s ->544 let s' = s & fMal %~ (`clearBit` 9)545 in if s ^. class3 /= 0 then chkcol s' else s'546547-- "If upper collimator position inconsistent with treatment then set bit 9 of F$mal"548chkcol :: TheracState -> TheracState549chkcol s550 | want /= CollimatorPositionUndefined && s ^. hardwareMeos . handParams /= want = s & fMal %~ (`setBit` 9)551 | otherwise = s552 where553 want = requestedTurntable (s ^. consoleMeos)554555-- Hand: "used by another task (Hand) to set the collimator/turntable to the proper position for556-- the selected mode/energy". Drives the turntable toward what the console asks for (or to the557-- field-light position while the hand control holds it there). The motor takes a while.558hand :: TheracState -> TheracState559hand s560 | want == CollimatorPositionUndefined || want == cur = s & turntableTarget .~ cur & turntableEta .~ 0561 | s ^. turntableTarget /= want || s ^. turntableEta <= 0 = s & turntableTarget .~ want & turntableEta .~ turntableTravelPasses562 | s ^. turntableEta == 1 = s & hardwareMeos . handParams .~ want & turntableEta .~ 0563 | otherwise = s & turntableEta %~ subtract 1564 where565 cur = s ^. hardwareMeos . handParams566 want = if s ^. awaitingSet then CollimatorPositionFieldLight else requestedTurntable (s ^. consoleMeos)567568-- task - runs concurrently to other stuff - "takes care of system-status interlocks and limit569-- checks" - moves the turntable, then checks it. The turntable only moves during data entry and570-- set-up, so after a Yakima overdose it is still in the field-light position when P is pressed:571-- "The machine paused again, this time displaying 'flatness'" (ASSUMPTION, see README).572housekeeper :: TMVar TheracState -> IO ()573housekeeper ts = forever $ do574 threadDelay housekeeperTick575 atomically $ modifyTherac ts $ \s ->576 if s ^. tPhase `elem` [TP_Reset, TP_Datent, TP_SetupTest] then hand s else s577 atomically $ lmtchk ts578579-- END `housekeeper` stuff580581-- BEGIN TP_Datent stuff582583setBendingMagnetFlag :: TMVar TheracState -> STM ()584setBendingMagnetFlag = setFieldInStruct bendingMagnetFlag True585586unsetBendingMagnetFlag :: TMVar TheracState -> STM ()587unsetBendingMagnetFlag = setFieldInStruct bendingMagnetFlag False588589-- subroutine - part of `treat` TP_Datent - spin until hysteresis delay expired590-- Ptime (Figure 3):591-- repeat592-- if bending magnet flag is set then593-- if editing taking place then594-- if mode/energy has changed then exit595-- until hysteresis delay has expired596-- Clear bending magnet flag597-- return598-- "Since Ptime clears it during its first execution, any edits performed during each succeeding599-- pass through Ptime will not be recognized."600-- Returns True if it noticed an edit.601pTime :: TMVar TheracState -> (BeamType, BeamEnergy) -> IO Bool602pTime ts wanted = go (hysteresisDelay `div` pTimePoll)603 where604 go :: Int -> IO Bool605 go 0 = do606 atomically $ unsetBendingMagnetFlag ts -- THE Tyler bug. AECL's fix moved this to the end of `magnet`607 return False608 go n = do609 -- it's intentional that we read each shared variable separately, like the PDP-11 task did610 bmf <- atomically $ readFieldFromStruct bendingMagnetFlag ts611 editing <- atomically $ readFieldFromStruct editingTakingPlace ts612 current <- atomically $ readFieldFromStruct (consoleMeos . datentParams) ts613 if bmf && editing && current /= wanted614 then do615 atomically $ unsetBendingMagnetFlag ts -- "Ptime clears the bending magnet variable and exits to Magnet"616 return True617 else do618 threadDelay pTimePoll619 go (n - 1)620621-- subroutine `magnet` - set bending magnets - part of `treat` TP_Datent622-- Magnet (Figure 3):623-- Set bending magnet flag624-- repeat625-- Set next magnet626-- Call Ptime627-- if mode/energy has changed, then exit628-- until all magnets are set629-- return630magnet :: TMVar TheracState -> (BeamType, BeamEnergy) -> IO Bool631magnet ts wanted = do632 atomically $ setBendingMagnetFlag ts633 let setMagnets :: Int -> IO Bool634 setMagnets n635 | n > numMagnets = return False636 | otherwise = do637 atomically $ setFieldInStruct magnetBeingSet n ts638 edited <- pTime ts wanted639 if edited then return True else setMagnets (n + 1)640 edited <- setMagnets 1641 atomically $ setFieldInStruct magnetBeingSet 0 ts642 return edited643644-- "it uses the high-order byte to index into a table of preset operating parameters and places645-- them in the digital-to-analog output table"646outputParameters :: TMVar TheracState -> (BeamType, BeamEnergy) -> STM ()647outputParameters ts wanted = setFieldInStruct (hardwareMeos . datentParams) wanted ts648649-- subroutine `TP_Datent` - part of `treat`650-- Datent (Figure 3):651-- if mode/energy specified then652-- begin653-- calculate table index654-- repeat fetch parameter, output parameter, point to next parameter until all parameters set655-- call Magnet656-- if mode/energy changed then return657-- end658-- if data entry is complete then set Tphase to 3659-- if data entry is not complete then660-- if reset command entered then set Tphase to 0661-- return662-- We only redo the parameters (and the 8 seconds of magnets) when the console asks for something663-- other than what was last output. Once Tphase is 3, "Datent is not entered again": an edit made664-- while `magnet` was past its first pTime is never looked at.665datent :: TMVar TheracState -> IO ()666datent ts = do667 wanted <- atomically $ readFieldFromStruct (consoleMeos . datentParams) ts668 current <- atomically $ readFieldFromStruct (hardwareMeos . datentParams) ts669 edited <-670 if fst wanted /= BeamTypeUndefined && wanted /= current671 then do672 atomically $ outputParameters ts wanted673 magnet ts wanted674 else return False675 unless edited $ atomically $ do676 s <- readTMVar ts677 -- "Initially, the data-entry process forces the operator to enter the mode and energy"678 if s ^. dataEntryComplete && s ^. consoleMeos . datentParams . _1 /= BeamTypeUndefined679 then setTPhase TP_SetupTest ts680 else when (s ^. resetPending) $ setTPhase TP_Reset ts681682-- END TP_Datent stuff