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