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
« 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.
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'.
8The following commands are implemented:
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"""
19import contextlib
20import datetime
21import multiprocessing.process
22import re
23import select
24import socket
25import sys
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
34from cgse_dummy.sim_data import SimulatedTemperature
36logger = logging.getLogger("egse.dummy")
38_VERSION = "0.0.2"
40device_settings = Settings.load("DUMMY DEVICE")
41cs_settings = Settings.load("DUMMY CS")
43hostname = cs_settings.get("HOSTNAME", "localhost")
44port = cs_settings.get("PORT", 5555)
46device_time = datetime.datetime.now(datetime.timezone.utc)
47reference_time = device_time
48error_msg = ""
50sensor_1 = SimulatedTemperature()
52app = typer.Typer(help=f"{device_settings.BRAND} {device_settings.MODEL} Simulator")
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)
63def nothing():
64 return None
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 )
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
83def beep(a, b):
84 print(f"BEEP {a=}, {b=}")
87def reset():
88 print("RESET")
91def clear():
92 """Clear all status data structures in the device."""
93 print("CLEAR")
96def get_value():
97 _, temperature = next(sensor_1)
98 return temperature
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}
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}
124def process_command(command_string: str) -> str:
125 global COMMAND_ACTIONS_RESPONSES
126 global COMMAND_PATTERNS_ACTIONS_RESPONSES
128 logger.debug(f"{command_string=}")
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}"
153def run_simulator():
154 """
155 Raises:
156 OSError: when the simulator is already running.
157 """
158 global error_msg
160 multiprocessing.current_process().name = "dummy_sim"
162 logger.info(f"Starting the {device_settings.MODEL} Simulator")
164 killer = SignalCatcher()
166 timeout = 2.0
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)
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)
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()
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}")
231def send_request(cmd: str, _type: str = "query") -> bytes:
232 from cgse_dummy.dummy_dev import Dummy
234 response = None
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.")
244 return response
247def send_command(cmd: str) -> None:
248 send_request(cmd, _type="write")
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)
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)
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)
277if __name__ == "__main__": 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 app()