Coverage for src/cgse_dummy/dummy_sim.py: 28%

1"""

2 The dummy device is a virtual device that is developed as a demonstration of how an external package, that delivers a

3 device interface for the CGSE, can be implemented.

4

5 The simulator listens on the Ethernet socket port number 5555, unless another port number is specified in the

6 Settings file under the section 'DUMMY DEVICE'.

7

8 The following commands are implemented:

9

10- *IDN? — returns identification of the device: Manufacturer, Model, Serial Number, Firmware version

11- *RST — reset the instrument

12- *CLS — clear

13- SYSTem:TIME year, month, day, hour, minute, second — set the date/time

14- SYSTem:TIME? — returns the current date/time

15- info — returns a string containing brand name, model name, version, ...

16- get_value — returns a measurement from the simulated temperature

17"""

18

19 import contextlib

20 import datetime

21 import multiprocessing.process

22 import re

23 import select

24 import socket

25 import sys

26

27 import typer

28 from egse.log import logging

29 from egse.device import DeviceConnectionError

30 from egse.settings import Settings

31 from egse.system import SignalCatcher

32 from egse.system import type_name

33

34 from cgse_dummy.sim_data import SimulatedTemperature

35

36 logger = logging.getLogger("egse.dummy")

37

38 _VERSION = "0.0.2"

39

40 device_settings = Settings.load("DUMMY DEVICE")

41 cs_settings = Settings.load("DUMMY CS")

42

43 hostname = cs_settings.get("HOSTNAME", "localhost")

44 port = cs_settings.get("PORT", 5555)

45

46 device_time = datetime.datetime.now(datetime.timezone.utc)

47 reference_time = device_time

48 error_msg = ""

49

50 sensor_1 = SimulatedTemperature()

51

52 app = typer.Typer(help=f"{device_settings.BRAND} {device_settings.MODEL} Simulator")

53

54

55 def create_datetime(year, month, day, hour, minute, second):

56 global device_time, reference_time

57 device_time = datetime.datetime(

58 year, month, day, hour, minute, second, tzinfo=datetime.timezone.utc

59 )

60 reference_time = datetime.datetime.now(datetime.timezone.utc)

61

62

63 def nothing():

64 return None

65

66

67 def set_time(year, month, day, hour, minute, second):

68 print(f"TIME {year}, {month}, {day}, {hour}, {minute}, {second}")

69 create_datetime(

70 int(year), int(month), int(day), int(hour), int(minute), int(second)

71 )

72

73

74 def get_time():

75 current_device_time = device_time + (

76 datetime.datetime.now(datetime.timezone.utc) - reference_time

77 )

78 msg = current_device_time.strftime("%a %b %d %H:%M:%S %Y")

79 print(f":SYST:TIME? {msg = }")

80 return msg

81

82

83 def beep(a, b):

84 print(f"BEEP {a=}, {b=}")

85

86

87 def reset():

88 print("RESET")

89

90

91 def clear():

92 """Clear all status data structures in the device."""

93 print("CLEAR")

94

95

96 def get_value():

97 _, temperature = next(sensor_1)

98 return temperature

99

100

101 COMMAND_ACTIONS_RESPONSES = {

102 "*IDN?": (

103 None,

104 f"{device_settings.BRAND}, {device_settings.MODEL}, {device_settings.SERIAL_NUMBER}, {_VERSION}",

105 ),

106 "*RST": (reset, None),

107 "*CLS": (clear, None),

108 "info": (

109 None,

110 f"{device_settings.BRAND}, MODEL {device_settings.MODEL}, {_VERSION}, SIMULATOR",

111 ),

112 "get_value": (None, get_value),

113}

114

115

116 COMMAND_PATTERNS_ACTIONS_RESPONSES = {

117 r":?\*RST": (reset, None),

118 r":?SYST(?:em)*:TIME (\d+), (\d+), (\d+), (\d+), (\d+), (\d+)": (set_time, None),

119 r":?SYST(?:em)*:TIME\?": (nothing, get_time),

120 r":?SYST(?:em)*:BEEP(?:er)* (\d+), (\d+(?:\.\d+)?)": (beep, None),

121}

122

123

124 def process_command(command_string: str) -> str:

125 global COMMAND_ACTIONS_RESPONSES

126 global COMMAND_PATTERNS_ACTIONS_RESPONSES

127

128 logger.debug(f"{command_string=}")

129

130 try:

131 action, response = COMMAND_ACTIONS_RESPONSES[command_string]

132 action and action()

133 if error_msg:

134 return error_msg

135 else:

136 return response if isinstance(response, str) else response()

137 except KeyError:

138 # try to match with a value

139 for key, value in COMMAND_PATTERNS_ACTIONS_RESPONSES.items():

140 if match := re.match(key, command_string):

141 logger.debug(f"{match=}, {match.groups()}")

142 action, response = value

143 logger.debug(f"{action=}, {response=}")

144 action and action(*match.groups())

145 return error_msg or (

146 response

147 if isinstance(response, str) or response is None

148 else response()

149 )

150 return f"ERROR: unknown command string: {command_string}"

151

152

153 def run_simulator():

154 """

155 Raises:

156 OSError: when the simulator is already running.

157 """

158 global error_msg

159

160 multiprocessing.current_process().name = "dummy_sim"

161

162 logger.info(f"Starting the {device_settings.MODEL} Simulator")

163

164 killer = SignalCatcher()

165

166 timeout = 2.0

167

168 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

169 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

170 sock.bind((hostname, port))

171 sock.listen()

172 sock.settimeout(timeout)

173 while True:

174 while True:

175 with contextlib.suppress(socket.timeout):

176 conn, addr = sock.accept()

177 break

178 if killer.term_signal_received:

179 return

180 with conn:

181 logger.info(f"Accepted connection from {addr}")

182 conn.sendall(

183 f"This is {device_settings.BRAND} {device_settings.MODEL} {_VERSION}.sim".encode()

184 )

185 try:

186 error_msg = ""

187 while True:

188 read_sockets, _, _ = select.select([conn], [], [], timeout)

189

190 if conn in read_sockets:

191 data = conn.recv(4096).decode().rstrip()

192 logger.debug(f"{data = }")

193 # Now that we use `select` I don't think the following will ever be true

194 # if not data:

195 # logger.info("Client closed connection, accepting new connection...")

196 # break

197 if data.strip() == "STOP":

198 logger.info("Client requested to terminate...")

199 sock.close()

200 return

201 for cmd in data.split(";"):

202 logger.debug(f"{cmd=}")

203 response = process_command(cmd.strip())

204 logger.debug(f"{response=}")

205 if response is not None:

206 response_b = f"{response}\n".encode()

207 logger.debug(f"write: {response_b=}")

208 conn.sendall(response_b)

209

210 if killer.term_signal_received:

211 logger.info("Terminating...")

212 sock.close()

213 return

214 if killer.user_signal_received:

215 if killer.signal_name == "SIGUSR1":

216 logger.info(

217 "SIGUSR1 is not supported by this simulator"

218 )

219 if killer.signal_name == "SIGUSR2":

220 logger.info(

221 "SIGUSR2 is not supported by this simulator"

222 )

223 killer.clear()

224

225 except ConnectionResetError as exc:

226 logger.info(f"ConnectionResetError: {exc}")

227 except Exception as exc:

228 logger.info(f"{exc.__class__.__name__} caught: {exc.args}")

229

230

231 def send_request(cmd: str, _type: str = "query") -> bytes:

232 from cgse_dummy.dummy_dev import Dummy

233

234 response = None

235

236 with Dummy(hostname=hostname, port=port) as daq_dev:

237 if _type == "query":

238 response = daq_dev.query(cmd)

239 elif _type == "write":

240 daq_dev.write(cmd)

241 else:

242 logger.info(f"Unknown type {_type} for send_request.")

243

244 return response

245

246

247 def send_command(cmd: str) -> None:

248 send_request(cmd, _type="write")

249

250

251@app.command()

252 def start():

253 try:

254 run_simulator()

255 except OSError as exc:

256 print(f"ERROR: Caught {type_name(exc)}: {exc}", file=sys.stderr)

257

258

259@app.command()

260 def status():

261 try:

262 response = send_request("*IDN?")

263 print(f"{response.decode().rstrip()}")

264 except DeviceConnectionError as exc:

265 print(f"ERROR: Caught {type_name(exc)}: {exc}", file=sys.stderr)

266

267

268@app.command()

269 def stop():

270 try:

271 response = send_request("STOP")

272 print(f"{response.decode().rstrip()}")

273 except DeviceConnectionError as exc:

274 print(f"ERROR: Caught {type_name(exc)}: {exc}", file=sys.stderr)

275

276

277 if __name__ == "__main__": 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true

278 app()