This repository was archived by the owner on Jun 28, 2025. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathLive.hs
More file actions
162 lines (140 loc) · 5.55 KB
/
Copy pathLive.hs
File metadata and controls
162 lines (140 loc) · 5.55 KB
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
{-# 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')