{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Copyright : (c) NASA, 2024-2025
-- License : BSD-style (see the LICENSE file in the distribution)
--
-- Run a Copilot simulation live and allow interacting with it using a
-- websocket.
--
-- This visualizer enables adding new streams to a visualization. To do so, the
-- visualizer needs access to the original spec, and needs to be able to
-- interpret new expressions in the same context as the prior expressions.
--
-- An example of a spec that can be passed as argument to the visualizer
-- follows:
--
-- @
-- spec :: String
-- spec = unlines
-- [ "let temperature :: Stream Word8"
-- , " temperature = extern \"temperature\" (Just [0, 15, 20, 25, 30])"
-- , ""
-- , " ctemp :: Stream Float"
-- , " ctemp = (unsafeCast temperature) * (150.0 / 255.0) - 50.0"
-- , ""
-- , " trueFalse :: Stream Bool"
-- , " trueFalse = [True] ++ not trueFalse"
-- , ""
-- , "in do trigger \"heaton\" (temperature < 18) [arg ctemp, arg (constI16 1), arg trueFalse]"
-- , " trigger \"heatoff\" (temperature > 21) [arg (constI16 1), arg ctemp]"
-- , " observer \"temperature\" temperature"
-- , " observer \"temperature2\" (temperature + 1)"
-- ]
-- @
--
-- The imports are predefined.
module Copilot.Visualize.Live
( visualize
, visualizeWith
, VisualSettings(..)
, mkDefaultVisualSettings
, SimulationSettings(..)
, mkDefaultSimulationSettings
)
where
-- External imports
import Control.Exception (SomeException (..), handle)
import Data.Aeson (ToJSON (..), encode)
import qualified Data.Text as T
import qualified Network.WebSockets as WS
import Prelude hiding (div, not, (++), (<), (>))
import qualified Prelude
import Text.Read (readMaybe)
-- External imports: Copilot
import qualified Copilot.Core as Core
import Copilot.Interpret.Eval (ShowType (Haskell), eval)
import Copilot.Language hiding (interpret, typeOf)
import Copilot.Visualize.UntypedTrace (AppData, makeTraceEval)
-- Internal imports
import Copilot.Visualize.Dynamic
-- | Start a simulation for an input spec, listening for commands and
-- communicating the status via a websocket.
visualize :: String -> IO ()
visualize = visualizeWith mkDefaultVisualSettings
-- | Start a simulation for an input spec, listening for commands and
-- communicating the status via a websocket.
visualizeWith :: VisualSettings -> String -> IO ()
visualizeWith settings spec = do
putStrLn $ concat
[ "WebSocket server starting on port "
, show (visualSettingsPort settings)
, "..."
]
WS.runServer
(visualSettingsHost settings)
(visualSettingsPort settings)
(app settings spec)
-- | Settings used to customize the simulation and interaction.
data VisualSettings = VisualSettings
{ visualSettingsHost :: String
-- ^ Host interface to listen to. Use
-- "127.0.0.1" to listen at localhost.
, visualSettingsPort :: Int
-- ^ Port to listen to.
, visualSettingsSimulation :: SimulationSettings
-- ^ Settings for the simulation.
}
-- | Default settings that simulates 3 steps and listens on localhost at port
-- 9160.
mkDefaultVisualSettings :: VisualSettings
mkDefaultVisualSettings = VisualSettings
{ visualSettingsHost = "127.0.0.1"
, visualSettingsPort = 9160
, visualSettingsSimulation = mkDefaultSimulationSettings
}
-- * Server
-- | Server application using web sockets.
app :: VisualSettings -> String -> WS.ServerApp
app settings spec pending = do
conn <- WS.acceptRequest pending
WS.withPingThread conn 30 (return ()) $ appInit settings spec conn
-- | Initialize the backend.
appInit :: VisualSettings -> String -> WS.Connection -> IO ()
appInit settings spec conn = handle appException $ do
-- Initialize the simulation.
simData <- simInit (visualSettingsSimulation settings) spec
-- Communicate the current values of the trace, in JSON, via the web
-- socket.
let appData = mkAppData (simSteps simData) (simSpec simData)
let samples = encode $ toJSON appData
WS.sendTextData conn samples
-- Start the application loop.
appMainLoop settings conn simData
where
appException :: SomeException -> IO ()
appException e = do
putStrLn $ "Error:" Prelude.++ show e
error $ show e
-- | Run the main application, repeatedly reading commands from a web socket
-- and returning results via the same web socket.
appMainLoop :: VisualSettings
-> WS.Connection
-> SimData
-> IO ()
appMainLoop settings conn simData = do
-- Read a command from the web socket.
cmdM <- readMaybe . T.unpack <$> WS.receiveData conn
-- Run a simulation step, if a command has been received.
let simulationSettings = visualSettingsSimulation settings
simData' <- maybe (pure simData) (simStep simulationSettings simData) cmdM
-- Communicate the current values of the trace, in JSON, via the web socket.
let appData = mkAppData (simSteps simData') (simSpec simData')
samples = encode $ toJSON appData
WS.sendTextData conn samples
appMainLoop settings conn simData'
-- * Auxiliary functions
-- | Obtain the trace data from a Copilot spec for a number of steps.
mkAppData :: Int -> Core.Spec -> AppData
mkAppData numSteps spec' =
makeTraceEval numSteps spec' (eval Haskell numSteps spec')