Coverage for src/cgse_dummy/dummy_cs.py: 64%

1"""

2 The control server for the dummy device.

3"""

4

5 import logging

6 import multiprocessing

7 import random

8 import sys

9 import threading

10 import time

11 from functools import partial

12

13 import typer

14 import zmq

15 from egse.command import ClientServerCommand

16 from egse.confman import is_configuration_manager_active

17 from egse.control import ControlServer

18 from egse.control import is_control_server_active

19 from egse.decorators import dynamic_interface

20 from egse.listener import Event

21 from egse.listener import EventInterface

22 from egse.protocol import CommandProtocol

23 from egse.proxy import Proxy

24 from egse.settings import Settings

25 from egse.storage import TYPES

26 from egse.storage import is_storage_manager_active

27 from egse.storage import register_to_storage_manager

28 from egse.storage import store_housekeeping_information

29 from egse.storage import unregister_from_storage_manager

30 from egse.system import attrdict

31 from egse.system import format_datetime

32 from egse.zmq_ser import bind_address

33 from egse.zmq_ser import connect_address

34

35 from cgse_dummy.dummy_dev import Dummy

36

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

38

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

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

41

42 DEV_HOST = "localhost"

43"""The hostname or IP address of the Dummy Device."""

44 DEV_PORT = dev_settings.PORT

45"""The port number for the Dummy Device."""

46 DEV_NAME = f"Dummy Device {dev_settings.MODEL}"

47"""The name used for the Dummy Device, this is used in Exceptions and in the info command."""

48

49 READ_TIMEOUT = 10.0 # seconds

50"""The maximum time to wait for a socket receive command."""

51 WRITE_TIMEOUT = 1.0 # seconds

52"""The maximum time to wait for a socket send command."""

53 CONNECT_TIMEOUT = 3.0 # seconds

54"""The maximum time to wait for establishing a socket connect."""

55

56# Especially DummyCommand and DummyController need to be defined in a known module

57# because those objects are pickled and when de-pickled at the clients side the class

58# definition must be known.

59

60 commands = attrdict(

61 {

62 "info": {

63 "description": "Info on the Dummy Device.",

64 "response": "handle_device_method",

65 },

66 "get_value": {

67 "description": "Read a value from the device.",

68 },

69 "handle_event": {

70 "description": "Notification of an event",

71 "device_method": "handle_event",

72 "cmd": "{event}",

73 "response": "handle_device_method",

74 },

75 }

76)

77

78 app = typer.Typer(

79 help=f"Dummy control server for the dummy device {dev_settings.MODEL}"

80)

81

82

83 def is_dummy_cs_active():

84 return is_control_server_active(

85 endpoint=connect_address(

86 cs_settings.PROTOCOL, cs_settings.HOSTNAME, cs_settings.COMMANDING_PORT

87 )

88 )

89

90

91 class DummyCommand(ClientServerCommand):

92 pass

93

94

95 class DummyInterface:

96 @dynamic_interface

97 def info(self): ...

98

99 @dynamic_interface

100 def get_value(self, *args, **kwargs): ...

101

102

103 class DummyProxy(Proxy, DummyInterface, EventInterface):

104 def __init__(

105 self,

106 protocol=cs_settings.PROTOCOL,

107 hostname=cs_settings.HOSTNAME,

108 port=cs_settings.COMMANDING_PORT,

109 timeout=cs_settings.TIMEOUT,

110 ):

111 """

112 Args:

113 protocol: the transport protocol [default is taken from settings file]

114 hostname: location of the control server (IP address) [default is taken from settings file]

115 port: TCP port on which the control server is listening for commands [default is taken from settings file]

116 """

117 super().__init__(connect_address(protocol, hostname, port), timeout=timeout)

118

119

120 class DummyController(DummyInterface, EventInterface):

121 def __init__(self, control_server):

122 self._cs = control_server

123 self._dev = Dummy(DEV_HOST, DEV_PORT)

124 self._dev.connect()

125

126 def info(self) -> str:

127 return self._dev.trans("info").decode().strip()

128

129 def get_value(self) -> float:

130 return float(self._dev.trans("get_value").decode().strip())

131

132 def handle_event(self, event: Event) -> str:

133 _exec_in_thread = False

134

135 def _handle_event(_event):

136 logger.info(f"An event is received, {_event=}")

137 logger.info(f"CM CS active? {is_configuration_manager_active()}")

138 time.sleep(5.0)

139 logger.info(f"CM CS active? {is_configuration_manager_active()}")

140 logger.info(f"An event is processed, {_event=}")

141

142 if _exec_in_thread:

143 # We execute this function in a daemon thread so the acknowledgment is

144 # sent back immediately (the ACK means 'command received and will be

145 # executed').

146

147 retry_thread = threading.Thread(target=_handle_event, args=(event,))

148 retry_thread.daemon = True

149 retry_thread.start()

150 else:

151 # An alternative to the daemon thread is to create a scheduled task that will be executed

152 # after the event is acknowledged.

153

154 self._cs.schedule_task(partial(_handle_event, event))

155

156 return "ACK"

157

158

159 class DummyProtocol(CommandProtocol):

160 def __init__(self, control_server: ControlServer):

161 super().__init__()

162 self.control_server = control_server

163

164 self.device_controller = DummyController(control_server)

165

166 self.load_commands(commands, DummyCommand, DummyController)

167

168 self.build_device_method_lookup_table(self.device_controller)

169

170 self._count = 0

171

172 def get_bind_address(self):

173 return bind_address(

174 self.control_server.get_communication_protocol(),

175 self.control_server.get_commanding_port(),

176 )

177

178 def get_status(self):

179 return super().get_status()

180

181 def get_housekeeping(self) -> dict:

182 # logger.debug(f"Executing get_housekeeping function for {self.__class__.__name__}.")

183

184 self._count += 1

185

186 # use the sleep to test the responsiveness of the control server when even this get_housekeeping function takes

187 # a lot of time, i.e. up to several minutes in the case of data acquisition devices

188 # import time

189 # time.sleep(2.0)

190

191 return {

192 "timestamp": format_datetime(),

193 "COUNT": self._count,

194 "PI": 3.14159, # just to have a constant parameter

195 "Random": random.randint(0, 100), # just to have a variable parameter

196 "T (ºC)": self.device_controller.get_value(),

197 }

198

199 def quit(self):

200 logger.info("Executing 'quit()' on DummyProtocol.")

201

202

203 class DummyControlServer(ControlServer):

204 """

205 DummyControlServer - Command and monitor dummy device controllers.

206

207 The sever binds to the following ZeroMQ sockets:

208

209 * a REQ-REP socket that can be used as a command server. Any client can connect and

210 send a command to the dummy device controller.

211

212 * a PUB-SUP socket that serves as a monitoring server. It will send out status

213 information to all the connected clients every HK_DELAY seconds.

214

215 """

216

217 def __init__(self):

218 multiprocessing.current_process().name = "dummy_cs"

219

220 super().__init__()

221

222 self.device_protocol = DummyProtocol(self)

223

224 logger.info(

225 f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()} for {self.__class__.__name__}"

226 )

227

228 self.device_protocol.bind(self.dev_ctrl_cmd_sock)

229

230 self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)

231

232 self.set_hk_delay(cs_settings.HK_DELAY)

233

234 from egse.confman import ConfigurationManagerProxy

235 from egse.listener import EVENT_ID

236

237 # The following import is needed because without this import, DummyProxy would be <class '__main__.DummyProxy'>

238 # instead of `egse.dummy.DummyProxy` and the ConfigurationManager control server will not be able to de-pickle

239 # the register message.

240 from egse.dummy import DummyProxy # noqa

241

242 self.register_as_listener(

243 proxy=ConfigurationManagerProxy,

244 listener={

245 "name": "Dummy CS",

246 "proxy": DummyProxy,

247 "event_id": EVENT_ID.SETUP,

248 },

249 )

250

251 def get_communication_protocol(self):

252 return "tcp"

253

254 def get_commanding_port(self):

255 return cs_settings.COMMANDING_PORT

256

257 def get_service_port(self):

258 return cs_settings.SERVICE_PORT

259

260 def get_monitoring_port(self):

261 return cs_settings.MONITORING_PORT

262

263 def get_storage_mnemonic(self):

264 return "DUMMY-HK"

265

266 def after_serve(self):

267 logger.debug("After Serve: unregistering Dummy CS")

268

269 from egse.confman import ConfigurationManagerProxy

270

271 self.unregister_as_listener(

272 proxy=ConfigurationManagerProxy, listener={"name": "Dummy CS"}

273 )

274

275 def is_storage_manager_active(self):

276 return is_storage_manager_active()

277

278 def store_housekeeping_information(self, data):

279 """Send housekeeping information to the Storage manager."""

280

281 store_housekeeping_information(origin=cs_settings.STORAGE_MNEMONIC, data=data)

282

283 def register_to_storage_manager(self) -> None:

284 register_to_storage_manager(

285 origin=cs_settings.STORAGE_MNEMONIC,

286 persistence_class=TYPES["CSV"],

287 prep={

288 "column_names": list(self.device_protocol.get_housekeeping().keys()),

289 "mode": "a",

290 },

291 )

292

293 def unregister_from_storage_manager(self) -> None:

294 unregister_from_storage_manager(origin=cs_settings.STORAGE_MNEMONIC)

295

296

297@app.command()

298 def start():

299 """Start the dummy control server on localhost."""

300

301 # The following import is needed because without this import, the control server and Proxy will not be able to

302 # instantiate classes that are passed in ZeroMQ messages and de-pickled.

303 from cgse_dummy.dummy_cs import DummyControlServer # noqa

304

305 try:

306 control_server = DummyControlServer()

307 control_server.serve()

308 except KeyboardInterrupt:

309 print("Shutdown requested...exiting")

310 except SystemExit as exit_code:

311 print(f"System Exit with code {exit_code}.")

312 sys.exit(-1)

313 except Exception: # noqa

314 import traceback

315

316 traceback.print_exc(file=sys.stdout)

317

318

319@app.command()

320 def stop():

321 """Send a quit service command to the dummy control server."""

322 with DummyProxy() as dummy:

323 logger.info("Sending quit_server() to Dummy CS.")

324 sp = dummy.get_service_proxy()

325 sp.quit_server()

326

327

328 if __name__ == "__main__":

329 app()