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

153 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-15 11:57 +0200

1""" 

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

3device interface for the CGSE, can be implemented. 

4 

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

6Settings file under the section 'DUMMY DEVICE'. 

7 

8The 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 

19import contextlib 

20import datetime 

21import multiprocessing.process 

22import re 

23import select 

24import socket 

25import sys 

26 

27import typer 

28from egse.log import logging 

29from egse.device import DeviceConnectionError 

30from egse.settings import Settings 

31from egse.system import SignalCatcher 

32from egse.system import type_name 

33 

34from cgse_dummy.sim_data import SimulatedTemperature 

35 

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

37 

38_VERSION = "0.0.2" 

39 

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

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

42 

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

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

45 

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

47reference_time = device_time 

48error_msg = "" 

49 

50sensor_1 = SimulatedTemperature() 

51 

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

53 

54 

55def 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 

63def nothing(): 

64 return None 

65 

66 

67def 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 

74def 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 

83def beep(a, b): 

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

85 

86 

87def reset(): 

88 print("RESET") 

89 

90 

91def clear(): 

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

93 print("CLEAR") 

94 

95 

96def get_value(): 

97 _, temperature = next(sensor_1) 

98 return temperature 

99 

100 

101COMMAND_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 

116COMMAND_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 

124def 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 

153def 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 

231def 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 

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

248 send_request(cmd, _type="write") 

249 

250 

251@app.command() 

252def 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() 

260def 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() 

269def 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 

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

278 app()