cgse-common 2025.0.6__tar.gz → 2025.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/PKG-INFO +1 -1
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/pyproject.toml +2 -2
- cgse_common-2025.0.8/src/egse/control.py +448 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/device.py +4 -3
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/mixin.py +7 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/protocol.py +3 -3
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/services.py +2 -2
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/setup.py +2 -1
- cgse_common-2025.0.6/src/egse/control.py +0 -348
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/.gitignore +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/README.md +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/cgse_common/__init__.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/cgse_common/settings.yaml +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/bits.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/calibration.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/command.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/config.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/decorators.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/env.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/exceptions.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/hk.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/metrics.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/monitoring.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/observer.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/obsid.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/persistence.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/plugin.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/process.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/proxy.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/reload.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/resource.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/response.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/services.yaml +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/settings.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/settings.yaml +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/state.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/system.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/version.py +0 -0
- {cgse_common-2025.0.6 → cgse_common-2025.0.8}/src/egse/zmq_ser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cgse-common"
|
|
3
|
-
version = "2025.0.
|
|
3
|
+
version = "2025.0.8"
|
|
4
4
|
description = "Software framework to support hardware testing"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "IVS KU Leuven"}
|
|
@@ -63,7 +63,7 @@ exclude = [
|
|
|
63
63
|
]
|
|
64
64
|
|
|
65
65
|
[tool.hatch.build.targets.wheel]
|
|
66
|
-
packages = ["src/egse", "src/
|
|
66
|
+
packages = ["src/egse", "src/cgse_common"]
|
|
67
67
|
|
|
68
68
|
[tool.ruff]
|
|
69
69
|
line-length = 120
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the abstract class for any Control Server and some convenience functions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import logging
|
|
7
|
+
import pickle
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
import zmq
|
|
12
|
+
|
|
13
|
+
from egse.system import time_in_ms
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from egse.logger import close_all_zmq_handlers
|
|
17
|
+
except ImportError:
|
|
18
|
+
def close_all_zmq_handlers(): # noqa
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
from egse.process import ProcessStatus
|
|
22
|
+
from egse.settings import Settings
|
|
23
|
+
from egse.system import do_every
|
|
24
|
+
from egse.system import get_average_execution_time
|
|
25
|
+
from egse.system import get_average_execution_times
|
|
26
|
+
from egse.system import get_full_classname
|
|
27
|
+
from egse.system import get_host_ip
|
|
28
|
+
from egse.system import save_average_execution_time
|
|
29
|
+
|
|
30
|
+
MODULE_LOGGER = logging.getLogger(__name__)
|
|
31
|
+
PROCESS_SETTINGS = Settings.load("PROCESS")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_control_server_active(endpoint: str = None, timeout: float = 0.5) -> bool:
|
|
35
|
+
""" Checks if the Control Server is running.
|
|
36
|
+
|
|
37
|
+
This function sends a *Ping* message to the Control Server and expects a *Pong* answer back within the timeout
|
|
38
|
+
period.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
endpoint (str): Endpoint to connect to, i.e. <protocol>://<address>:<port>
|
|
42
|
+
timeout (float): Timeout when waiting for a reply [s, default=0.5]
|
|
43
|
+
|
|
44
|
+
Returns: True if the Control Server is running and replied with the expected answer; False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
ctx = zmq.Context.instance()
|
|
48
|
+
|
|
49
|
+
return_code = False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
socket = ctx.socket(zmq.REQ)
|
|
53
|
+
socket.connect(endpoint)
|
|
54
|
+
data = pickle.dumps("Ping")
|
|
55
|
+
socket.send(data)
|
|
56
|
+
rlist, _, _ = zmq.select([socket], [], [], timeout=timeout)
|
|
57
|
+
|
|
58
|
+
if socket in rlist:
|
|
59
|
+
data = socket.recv()
|
|
60
|
+
response = pickle.loads(data)
|
|
61
|
+
return_code = response == "Pong"
|
|
62
|
+
socket.close(linger=0)
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
MODULE_LOGGER.warning(f"Caught an exception while pinging a control server at {endpoint}: {exc}.")
|
|
65
|
+
|
|
66
|
+
return return_code
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ControlServer(metaclass=abc.ABCMeta):
|
|
70
|
+
""" Base class for all device control servers and for the Storage Manager and Configuration Manager.
|
|
71
|
+
|
|
72
|
+
A Control Server reads commands from a ZeroMQ socket and executes these commands by calling the `execute()` method
|
|
73
|
+
of the commanding protocol class.
|
|
74
|
+
|
|
75
|
+
The sub-class shall define the following:
|
|
76
|
+
|
|
77
|
+
- Define the device protocol class -> `self.device_protocol`
|
|
78
|
+
- Bind the command socket to the device protocol -> `self.dev_ctrl_cmd_sock`
|
|
79
|
+
- Register the command socket in the poll set -> `self.poller`
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self):
|
|
84
|
+
""" Initialisation of a new Control Server."""
|
|
85
|
+
|
|
86
|
+
from egse.monitoring import MonitoringProtocol
|
|
87
|
+
from egse.services import ServiceProtocol
|
|
88
|
+
|
|
89
|
+
self._process_status = ProcessStatus()
|
|
90
|
+
|
|
91
|
+
self._timer_thread = threading.Thread(
|
|
92
|
+
target=do_every, args=(PROCESS_SETTINGS.METRICS_INTERVAL, self._process_status.update))
|
|
93
|
+
self._timer_thread.daemon = True
|
|
94
|
+
self._timer_thread.start()
|
|
95
|
+
|
|
96
|
+
# The logger will be overwritten by the sub-class, if not, we use this logger with the name of the sub-class.
|
|
97
|
+
# That will help us to identify which sub-class did not overwrite the logger attribute.
|
|
98
|
+
|
|
99
|
+
self.logger = logging.getLogger(get_full_classname(self))
|
|
100
|
+
|
|
101
|
+
self.interrupted = False
|
|
102
|
+
self.mon_delay = 1000 # Delay between publish status information [ms]
|
|
103
|
+
self.hk_delay = 1000 # Delay between saving housekeeping information [ms]
|
|
104
|
+
|
|
105
|
+
self.zcontext = zmq.Context.instance()
|
|
106
|
+
self.poller = zmq.Poller()
|
|
107
|
+
|
|
108
|
+
self.device_protocol = None # This will be set in the sub-class
|
|
109
|
+
self.service_protocol = ServiceProtocol(self)
|
|
110
|
+
self.monitoring_protocol = MonitoringProtocol(self)
|
|
111
|
+
|
|
112
|
+
# Set up the Control Server waiting for service requests
|
|
113
|
+
|
|
114
|
+
self.dev_ctrl_service_sock = self.zcontext.socket(zmq.REP)
|
|
115
|
+
self.service_protocol.bind(self.dev_ctrl_service_sock)
|
|
116
|
+
|
|
117
|
+
# Set up the Control Server for sending monitoring info
|
|
118
|
+
|
|
119
|
+
self.dev_ctrl_mon_sock = self.zcontext.socket(zmq.PUB)
|
|
120
|
+
self.monitoring_protocol.bind(self.dev_ctrl_mon_sock)
|
|
121
|
+
|
|
122
|
+
# Set up the Control Server waiting for device commands.
|
|
123
|
+
# The device protocol shall bind the socket in the sub-class
|
|
124
|
+
|
|
125
|
+
self.dev_ctrl_cmd_sock = self.zcontext.socket(zmq.REP)
|
|
126
|
+
|
|
127
|
+
# Initialise the poll set
|
|
128
|
+
|
|
129
|
+
self.poller.register(self.dev_ctrl_service_sock, zmq.POLLIN)
|
|
130
|
+
self.poller.register(self.dev_ctrl_mon_sock, zmq.POLLIN)
|
|
131
|
+
|
|
132
|
+
@abc.abstractmethod
|
|
133
|
+
def get_communication_protocol(self) -> str:
|
|
134
|
+
""" Returns the communication protocol used by the Control Server.
|
|
135
|
+
|
|
136
|
+
Returns: Communication protocol used by the Control Server, as specified in the settings.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abc.abstractmethod
|
|
142
|
+
def get_commanding_port(self) -> int:
|
|
143
|
+
""" Returns the commanding port used by the Control Server.
|
|
144
|
+
|
|
145
|
+
Returns: Commanding port used by the Control Server, as specified in the settings.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@abc.abstractmethod
|
|
151
|
+
def get_service_port(self) -> int:
|
|
152
|
+
""" Returns the service port used by the Control Server.
|
|
153
|
+
|
|
154
|
+
Returns: Service port used by the Control Server, as specified in the settings.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
@abc.abstractmethod
|
|
160
|
+
def get_monitoring_port(self) -> int:
|
|
161
|
+
""" Returns the monitoring port used by the Control Server.
|
|
162
|
+
|
|
163
|
+
Returns: Monitoring port used by the Control Server, as specified in the settings.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def get_ip_address(self) -> str:
|
|
169
|
+
|
|
170
|
+
return get_host_ip()
|
|
171
|
+
|
|
172
|
+
def get_storage_mnemonic(self) -> str:
|
|
173
|
+
""" Returns the storage mnemonics used by the Control Server.
|
|
174
|
+
|
|
175
|
+
This is a string that will appear in the filename with the housekeeping information of the device, as a way of
|
|
176
|
+
identifying the device. If this is not implemented in the sub-class, then the class name will be used.
|
|
177
|
+
|
|
178
|
+
Returns: Storage mnemonics used by the Control Server, as specified in the settings.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
return self.__class__.__name__
|
|
182
|
+
|
|
183
|
+
def get_process_status(self) -> dict:
|
|
184
|
+
""" Returns the process status of the Control Server.
|
|
185
|
+
|
|
186
|
+
Returns: Dictionary with the process status of the Control Server.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
return self._process_status.as_dict()
|
|
190
|
+
|
|
191
|
+
def get_average_execution_times(self) -> dict:
|
|
192
|
+
""" Returns the average execution times of all functions that have been monitored by this process.
|
|
193
|
+
|
|
194
|
+
Returns: Dictionary with the average execution times of all functions that have been monitored by this process.
|
|
195
|
+
The dictionary keys are the function names, and the values are the average execution times in ms.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
return get_average_execution_times()
|
|
199
|
+
|
|
200
|
+
def set_mon_delay(self, seconds: float) -> float:
|
|
201
|
+
""" Sets the delay time for monitoring.
|
|
202
|
+
|
|
203
|
+
The delay time is the time between two successive executions of the `get_status()` function of the device
|
|
204
|
+
protocol.
|
|
205
|
+
|
|
206
|
+
It might happen that the delay time that is set is longer than what you requested. That is the case when the
|
|
207
|
+
execution of the `get_status()` function takes longer than the requested delay time. That should prevent the
|
|
208
|
+
server from blocking when a too short delay time is requested.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
seconds (float): Number of seconds between the monitoring calls
|
|
212
|
+
|
|
213
|
+
Returns: Delay that was set [ms].
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
execution_time = get_average_execution_time(self.device_protocol.get_status)
|
|
217
|
+
self.mon_delay = max(seconds * 1000, (execution_time + 0.2) * 1000)
|
|
218
|
+
|
|
219
|
+
return self.mon_delay
|
|
220
|
+
|
|
221
|
+
def set_hk_delay(self, seconds: float) -> float:
|
|
222
|
+
""" Sets the delay time for housekeeping.
|
|
223
|
+
|
|
224
|
+
The delay time is the time between two successive executions of the `get_housekeeping()` function of the device
|
|
225
|
+
protocol.
|
|
226
|
+
|
|
227
|
+
It might happen that the delay time that is set is longer than what you requested. That is the case when the
|
|
228
|
+
execution of the `get_housekeeping()` function takes longer than the requested delay time. That should prevent
|
|
229
|
+
the server from blocking when a too short delay time is requested.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
seconds (float): Number of seconds between the housekeeping calls
|
|
233
|
+
|
|
234
|
+
Returns: Delay that was set [ms].
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
execution_time = get_average_execution_time(self.device_protocol.get_housekeeping)
|
|
238
|
+
self.hk_delay = max(seconds * 1000, (execution_time + 0.2) * 1000)
|
|
239
|
+
|
|
240
|
+
return self.hk_delay
|
|
241
|
+
|
|
242
|
+
def set_logging_level(self, level: Union[int, str]) -> None:
|
|
243
|
+
""" Sets the logging level to the given level.
|
|
244
|
+
|
|
245
|
+
Allowed logging levels are:
|
|
246
|
+
- "CRITICAL" or 50
|
|
247
|
+
- "FATAL" or CRITICAL
|
|
248
|
+
- "ERROR" or 40
|
|
249
|
+
- "WARNING" or 30
|
|
250
|
+
- "WARN" or WARNING
|
|
251
|
+
- "INFO" or 20
|
|
252
|
+
- "DEBUG" or 10
|
|
253
|
+
- "NOTSET" or 0
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
level (int | str): Logging level to use, specified as either a string or an integer
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
self.logger.setLevel(level=level)
|
|
260
|
+
|
|
261
|
+
def quit(self) -> None:
|
|
262
|
+
""" Interrupts the Control Server."""
|
|
263
|
+
|
|
264
|
+
self.interrupted = True
|
|
265
|
+
|
|
266
|
+
def before_serve(self) -> None:
|
|
267
|
+
""" Steps to take before the Control Server is activated."""
|
|
268
|
+
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
def after_serve(self) -> None:
|
|
272
|
+
""" Steps to take after the Control Server has been deactivated."""
|
|
273
|
+
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
def is_storage_manager_active(self) -> bool:
|
|
277
|
+
""" Checks if the Storage Manager is active.
|
|
278
|
+
|
|
279
|
+
This method has to be implemented by the sub-class if you need to store information.
|
|
280
|
+
|
|
281
|
+
Note: You might want to set a specific timeout when checking for the Storage Manager.
|
|
282
|
+
|
|
283
|
+
Note: If this method returns True, the following methods shall also be implemented by the sub-class:
|
|
284
|
+
|
|
285
|
+
- register_to_storage_manager()
|
|
286
|
+
- unregister_from_storage_manager()
|
|
287
|
+
- store_housekeeping_information()
|
|
288
|
+
|
|
289
|
+
Returns: True if the Storage Manager is active; False otherwise.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def serve(self) -> None:
|
|
295
|
+
""" Activation of the Control Server.
|
|
296
|
+
|
|
297
|
+
This comprises the following steps:
|
|
298
|
+
|
|
299
|
+
- Executing the `before_serve` method;
|
|
300
|
+
- Checking if the Storage Manager is active and registering the Control Server to it;
|
|
301
|
+
- Start listening for keyboard interrupts;
|
|
302
|
+
- Start accepting (listening to) commands;
|
|
303
|
+
- Start sending out monitoring information;
|
|
304
|
+
- Start sending out housekeeping information;
|
|
305
|
+
- Start listening for quit commands;
|
|
306
|
+
- After a quit command has been received:
|
|
307
|
+
- Unregister from the Storage Manager;
|
|
308
|
+
- Execute the `after_serve` method;
|
|
309
|
+
- Close all sockets;
|
|
310
|
+
- Clean up all threads.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
self.before_serve()
|
|
314
|
+
|
|
315
|
+
# check if Storage Manager is available
|
|
316
|
+
|
|
317
|
+
storage_manager = self.is_storage_manager_active()
|
|
318
|
+
|
|
319
|
+
storage_manager and self.register_to_storage_manager()
|
|
320
|
+
|
|
321
|
+
# This approach is very simplistic and not time efficient
|
|
322
|
+
# We probably want to use a Timer that executes the monitoring and saving actions at
|
|
323
|
+
# dedicated times in the background.
|
|
324
|
+
|
|
325
|
+
# FIXME; we shall use the time.perf_counter() here!
|
|
326
|
+
|
|
327
|
+
last_time = time_in_ms()
|
|
328
|
+
last_time_hk = time_in_ms()
|
|
329
|
+
|
|
330
|
+
while True:
|
|
331
|
+
try:
|
|
332
|
+
socks = dict(self.poller.poll(50)) # timeout in milliseconds, do not block
|
|
333
|
+
except KeyboardInterrupt:
|
|
334
|
+
self.logger.warning("Keyboard interrupt caught!")
|
|
335
|
+
self.logger.warning(
|
|
336
|
+
"The ControlServer can not be interrupted with CTRL-C, send a quit command to the server instead."
|
|
337
|
+
)
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
if self.dev_ctrl_cmd_sock in socks:
|
|
341
|
+
self.device_protocol.execute()
|
|
342
|
+
|
|
343
|
+
if self.dev_ctrl_service_sock in socks:
|
|
344
|
+
self.service_protocol.execute()
|
|
345
|
+
|
|
346
|
+
# Handle sending out monitoring information periodically, based on the MON_DELAY time that is specified in
|
|
347
|
+
# the YAML configuration file for the device
|
|
348
|
+
|
|
349
|
+
if time_in_ms() - last_time >= self.mon_delay:
|
|
350
|
+
last_time = time_in_ms()
|
|
351
|
+
# self.logger.debug("Sending status to monitoring processes.")
|
|
352
|
+
self.monitoring_protocol.send_status(
|
|
353
|
+
save_average_execution_time(self.device_protocol.get_status)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Handle sending out housekeeping information periodically, based on the HK_DELAY time that is specified in
|
|
357
|
+
# the YAML configuration file for the device
|
|
358
|
+
|
|
359
|
+
if time_in_ms() - last_time_hk >= self.hk_delay:
|
|
360
|
+
last_time_hk = time_in_ms()
|
|
361
|
+
if storage_manager:
|
|
362
|
+
# self.logger.debug("Sending housekeeping information to Storage.")
|
|
363
|
+
self.store_housekeeping_information(
|
|
364
|
+
save_average_execution_time(self.device_protocol.get_housekeeping)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if self.interrupted:
|
|
368
|
+
self.logger.info(
|
|
369
|
+
f"Quit command received, closing down the {self.__class__.__name__}."
|
|
370
|
+
)
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
# Some device protocol sub-classes might start a number of threads or processes to support the commanding.
|
|
374
|
+
# Check if these threads/processes are still alive and terminate gracefully if they are not.
|
|
375
|
+
|
|
376
|
+
if not self.device_protocol.is_alive():
|
|
377
|
+
self.logger.error(
|
|
378
|
+
"Some Thread or sub-process that was started by Protocol has died, terminating..."
|
|
379
|
+
)
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
storage_manager and self.unregister_from_storage_manager()
|
|
383
|
+
|
|
384
|
+
self.after_serve()
|
|
385
|
+
|
|
386
|
+
self.device_protocol.quit()
|
|
387
|
+
|
|
388
|
+
self.dev_ctrl_mon_sock.close()
|
|
389
|
+
self.dev_ctrl_service_sock.close()
|
|
390
|
+
self.dev_ctrl_cmd_sock.close()
|
|
391
|
+
|
|
392
|
+
close_all_zmq_handlers()
|
|
393
|
+
|
|
394
|
+
self.zcontext.term()
|
|
395
|
+
|
|
396
|
+
def store_housekeeping_information(self, data: dict) -> None:
|
|
397
|
+
""" Sends housekeeping information to the Storage Manager.
|
|
398
|
+
|
|
399
|
+
This method has to be overwritten by the sub-classes if they want the device housekeeping information to be
|
|
400
|
+
saved.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
data (dict): a dictionary containing parameter name and value of all device housekeeping. There is also
|
|
404
|
+
a timestamp that represents the date/time when the HK was received from the device.
|
|
405
|
+
"""
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
def register_to_storage_manager(self) -> None:
|
|
409
|
+
""" Registers this Control Server to the Storage Manager.
|
|
410
|
+
|
|
411
|
+
By doing so, the housekeeping information of the device will be sent to the Storage Manager, which will store
|
|
412
|
+
the information in a dedicated CSV file.
|
|
413
|
+
|
|
414
|
+
This method has to be overwritten by the sub-classes if they have housekeeping information that must be stored.
|
|
415
|
+
|
|
416
|
+
Subclasses need to overwrite this method if they have housekeeping information to be stored.
|
|
417
|
+
|
|
418
|
+
The following information is required for the registration:
|
|
419
|
+
|
|
420
|
+
- origin: Storage mnemonic, which can be retrieved from `self.get_storage_mnemonic()`
|
|
421
|
+
- persistence_class: Persistence layer (one of the TYPES in egse.storage.persistence)
|
|
422
|
+
- prep: depending on the type of the persistence class (see respective documentation)
|
|
423
|
+
|
|
424
|
+
The `egse.storage` module provides a convenience method that can be called from the method in the subclass:
|
|
425
|
+
|
|
426
|
+
>>> from egse.storage import register_to_storage_manager # noqa
|
|
427
|
+
|
|
428
|
+
Note: the `egse.storage` module might not be available, it is provided by the `cgse-core` package.
|
|
429
|
+
"""
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
def unregister_from_storage_manager(self) -> None:
|
|
433
|
+
""" Unregisters the Control Server from the Storage Manager.
|
|
434
|
+
|
|
435
|
+
This method has to be overwritten by the sub-classes.
|
|
436
|
+
|
|
437
|
+
The following information is required for the registration:
|
|
438
|
+
|
|
439
|
+
- origin: Storage mnemonic, which can be retrieved from `self.get_storage_mnemonic()`
|
|
440
|
+
|
|
441
|
+
The `egse.storage` module provides a convenience method that can be called from the method in the sub-class:
|
|
442
|
+
|
|
443
|
+
>>> from egse.storage import unregister_from_storage_manager # noqa
|
|
444
|
+
|
|
445
|
+
Note: the `egse.storage` module might not be available, it is provided by the `cgse-core` package.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
pass
|
|
@@ -152,6 +152,7 @@ class DeviceConnectionInterface(DeviceConnectionObservable):
|
|
|
152
152
|
Raises:
|
|
153
153
|
ConnectionError: when the connection can not be opened.
|
|
154
154
|
"""
|
|
155
|
+
|
|
155
156
|
raise NotImplementedError
|
|
156
157
|
|
|
157
158
|
@dynamic_interface
|
|
@@ -187,13 +188,13 @@ class DeviceInterface(DeviceConnectionInterface):
|
|
|
187
188
|
|
|
188
189
|
@dynamic_interface
|
|
189
190
|
def is_simulator(self) -> bool:
|
|
190
|
-
"""
|
|
191
|
+
""" Checks whether the device is a simulator rather than a real hardware controller.
|
|
191
192
|
|
|
192
193
|
This can be useful for testing purposes or when doing actual movement simulations.
|
|
193
194
|
|
|
194
|
-
Returns:
|
|
195
|
-
True if the Device is a Simulator, False if the Device is connected to real hardware.
|
|
195
|
+
Returns: True if the Device is a Simulator; False if the Device is connected to real hardware.
|
|
196
196
|
"""
|
|
197
|
+
|
|
197
198
|
raise NotImplementedError
|
|
198
199
|
|
|
199
200
|
|
|
@@ -28,6 +28,7 @@ __all__ = [
|
|
|
28
28
|
"add_cr_lf",
|
|
29
29
|
"dynamic_command",
|
|
30
30
|
"DynamicCommandMixin",
|
|
31
|
+
"CommandType"
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
# ----- Mixin for dynamic commanding ---------------------------------------------------------------
|
|
@@ -107,6 +108,12 @@ def expand_kwargs(kwargs: Dict):
|
|
|
107
108
|
"""Expand keyword arguments and their values as 'key=value' separated by spaces."""
|
|
108
109
|
return " ".join(f"{k}={v}" for k, v in kwargs.items())
|
|
109
110
|
|
|
111
|
+
class CommandType(enum.Enum, str):
|
|
112
|
+
|
|
113
|
+
READ = "read"
|
|
114
|
+
WRITE = "write"
|
|
115
|
+
TRANSACTION = "transaction"
|
|
116
|
+
|
|
110
117
|
|
|
111
118
|
def dynamic_command(
|
|
112
119
|
*,
|
|
@@ -163,7 +163,7 @@ class BaseCommandProtocol(DeviceConnectionObserver):
|
|
|
163
163
|
"""
|
|
164
164
|
status = {
|
|
165
165
|
"timestamp": format_datetime(),
|
|
166
|
-
"delay": self.__control_server.
|
|
166
|
+
"delay": self.__control_server.mon_delay,
|
|
167
167
|
}
|
|
168
168
|
status.update(self.__control_server.get_process_status())
|
|
169
169
|
return status
|
|
@@ -386,7 +386,7 @@ class CommandProtocol(DeviceConnectionObserver, metaclass=abc.ABCMeta):
|
|
|
386
386
|
|
|
387
387
|
def quit(self):
|
|
388
388
|
"""
|
|
389
|
-
This method can be overridden by a sub-class to
|
|
389
|
+
This method can be overridden by a sub-class to clean up and stop threads that it
|
|
390
390
|
started.
|
|
391
391
|
"""
|
|
392
392
|
|
|
@@ -444,7 +444,7 @@ class CommandProtocol(DeviceConnectionObserver, metaclass=abc.ABCMeta):
|
|
|
444
444
|
"""
|
|
445
445
|
status = {
|
|
446
446
|
"timestamp": format_datetime(),
|
|
447
|
-
"delay": self.control_server.
|
|
447
|
+
"delay": self.control_server.mon_delay,
|
|
448
448
|
}
|
|
449
449
|
status.update(self.control_server.get_process_status())
|
|
450
450
|
return status
|
|
@@ -25,7 +25,7 @@ LOGGER = logging.getLogger(__name__)
|
|
|
25
25
|
|
|
26
26
|
HERE = Path(__file__).parent
|
|
27
27
|
|
|
28
|
-
SERVICE_SETTINGS = Settings.load(
|
|
28
|
+
SERVICE_SETTINGS = Settings.load(location=HERE, filename="services.yaml")
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class ServiceCommand(ClientServerCommand):
|
|
@@ -57,7 +57,7 @@ class ServiceProtocol(CommandProtocol):
|
|
|
57
57
|
Returns:
|
|
58
58
|
Sends back the selected delay time in milliseconds.
|
|
59
59
|
"""
|
|
60
|
-
delay = self.control_server.
|
|
60
|
+
delay = self.control_server.set_mon_delay(1.0 / freq)
|
|
61
61
|
|
|
62
62
|
LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {delay:.0f}ms.")
|
|
63
63
|
|
|
@@ -266,7 +266,8 @@ def _get_attribute(self, name, default):
|
|
|
266
266
|
def _parse_filename_for_setup_id(filename: str):
|
|
267
267
|
"""Returns the setup_id from the filename, or None when no match was found."""
|
|
268
268
|
|
|
269
|
-
match = re.search(r"SETUP_([^_]+)_(\d+)", filename)
|
|
269
|
+
# match = re.search(r"SETUP_([^_]+)_(\d+)", filename)
|
|
270
|
+
match = re.search(r"SETUP_(\w+)_([\d]{5})_([\d]{6})_([\d]{6})\.yaml", filename)
|
|
270
271
|
|
|
271
272
|
# TypeError when match is None
|
|
272
273
|
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module defines the abstract class for any control server and some convenience functions.
|
|
3
|
-
"""
|
|
4
|
-
import abc
|
|
5
|
-
import logging
|
|
6
|
-
import pickle
|
|
7
|
-
import threading
|
|
8
|
-
|
|
9
|
-
import zmq
|
|
10
|
-
|
|
11
|
-
from egse.system import time_in_ms
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
from egse.logger import close_all_zmq_handlers
|
|
15
|
-
except ImportError:
|
|
16
|
-
def close_all_zmq_handlers(): # noqa
|
|
17
|
-
pass
|
|
18
|
-
|
|
19
|
-
from egse.process import ProcessStatus
|
|
20
|
-
from egse.settings import Settings
|
|
21
|
-
from egse.system import do_every
|
|
22
|
-
from egse.system import get_average_execution_time
|
|
23
|
-
from egse.system import get_average_execution_times
|
|
24
|
-
from egse.system import get_full_classname
|
|
25
|
-
from egse.system import get_host_ip
|
|
26
|
-
from egse.system import save_average_execution_time
|
|
27
|
-
|
|
28
|
-
MODULE_LOGGER = logging.getLogger(__name__)
|
|
29
|
-
PROCESS_SETTINGS = Settings.load("PROCESS")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def is_control_server_active(endpoint: str = None, timeout: float = 0.5) -> bool:
|
|
33
|
-
"""
|
|
34
|
-
Check if the control server is running. This function sends a *Ping* message to the
|
|
35
|
-
control server and expects a *Pong* answer back within the timeout period.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
endpoint (str): the endpoint to connect to, i.e. <protocol>://<address>:<port>
|
|
39
|
-
timeout (float): timeout when waiting for a reply [seconds, default=0.5]
|
|
40
|
-
Returns:
|
|
41
|
-
True if the Control Server is running and replied with the expected answer.
|
|
42
|
-
"""
|
|
43
|
-
ctx = zmq.Context.instance()
|
|
44
|
-
|
|
45
|
-
return_code = False
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
socket = ctx.socket(zmq.REQ)
|
|
49
|
-
socket.connect(endpoint)
|
|
50
|
-
data = pickle.dumps("Ping")
|
|
51
|
-
socket.send(data)
|
|
52
|
-
rlist, _, _ = zmq.select([socket], [], [], timeout=timeout)
|
|
53
|
-
if socket in rlist:
|
|
54
|
-
data = socket.recv()
|
|
55
|
-
response = pickle.loads(data)
|
|
56
|
-
return_code = response == "Pong"
|
|
57
|
-
socket.close(linger=0)
|
|
58
|
-
except Exception as exc:
|
|
59
|
-
MODULE_LOGGER.warning(f"Caught an exception while pinging a control server at {endpoint}: {exc}.")
|
|
60
|
-
|
|
61
|
-
return return_code
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class ControlServer(metaclass=abc.ABCMeta):
|
|
65
|
-
"""
|
|
66
|
-
The base class for all device control servers and for the Storage Manager and Configuration
|
|
67
|
-
Manager. A Control Server reads commands from a ZeroMQ socket and executes these commands by
|
|
68
|
-
calling the `execute()` method of the commanding protocol class.
|
|
69
|
-
|
|
70
|
-
The sub-class shall define the following:
|
|
71
|
-
|
|
72
|
-
* Define the device protocol class -> `self.device_protocol`
|
|
73
|
-
* Bind the command socket to the device protocol -> `self.dev_ctrl_cmd_sock`
|
|
74
|
-
* Register the command socket in the poll set -> `self.poller`
|
|
75
|
-
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
def __init__(self):
|
|
79
|
-
from egse.monitoring import MonitoringProtocol
|
|
80
|
-
from egse.services import ServiceProtocol
|
|
81
|
-
|
|
82
|
-
self._process_status = ProcessStatus()
|
|
83
|
-
|
|
84
|
-
self._timer_thread = threading.Thread(
|
|
85
|
-
target=do_every, args=(PROCESS_SETTINGS.METRICS_INTERVAL, self._process_status.update))
|
|
86
|
-
self._timer_thread.daemon = True
|
|
87
|
-
self._timer_thread.start()
|
|
88
|
-
|
|
89
|
-
# The logger will be overwritten by the sub-class, if not, then we use this logger
|
|
90
|
-
# with the name of the sub-class. That will help us to identify which sub-class did not
|
|
91
|
-
# overwrite the logger attribute.
|
|
92
|
-
|
|
93
|
-
self.logger = logging.getLogger(get_full_classname(self))
|
|
94
|
-
|
|
95
|
-
self.interrupted = False
|
|
96
|
-
self.delay = 1000 # delay between publish status information [milliseconds]
|
|
97
|
-
self.hk_delay = 1000 # delay between saving housekeeping information [milliseconds]
|
|
98
|
-
|
|
99
|
-
self.zcontext = zmq.Context.instance()
|
|
100
|
-
self.poller = zmq.Poller()
|
|
101
|
-
|
|
102
|
-
self.device_protocol = None # This will be set in the sub-class
|
|
103
|
-
self.service_protocol = ServiceProtocol(self)
|
|
104
|
-
self.monitoring_protocol = MonitoringProtocol(self)
|
|
105
|
-
|
|
106
|
-
# Setup the control server waiting for service requests
|
|
107
|
-
|
|
108
|
-
self.dev_ctrl_service_sock = self.zcontext.socket(zmq.REP)
|
|
109
|
-
self.service_protocol.bind(self.dev_ctrl_service_sock)
|
|
110
|
-
|
|
111
|
-
# Setup the control server for sending monitoring info
|
|
112
|
-
|
|
113
|
-
self.dev_ctrl_mon_sock = self.zcontext.socket(zmq.PUB)
|
|
114
|
-
self.monitoring_protocol.bind(self.dev_ctrl_mon_sock)
|
|
115
|
-
|
|
116
|
-
# Setup the control server waiting for device commands.
|
|
117
|
-
# The device protocol shall bind the socket in the sub-class
|
|
118
|
-
|
|
119
|
-
self.dev_ctrl_cmd_sock = self.zcontext.socket(zmq.REP)
|
|
120
|
-
|
|
121
|
-
# Initialize the poll set
|
|
122
|
-
|
|
123
|
-
self.poller.register(self.dev_ctrl_service_sock, zmq.POLLIN)
|
|
124
|
-
self.poller.register(self.dev_ctrl_mon_sock, zmq.POLLIN)
|
|
125
|
-
|
|
126
|
-
@abc.abstractmethod
|
|
127
|
-
def get_communication_protocol(self):
|
|
128
|
-
pass
|
|
129
|
-
|
|
130
|
-
@abc.abstractmethod
|
|
131
|
-
def get_commanding_port(self):
|
|
132
|
-
pass
|
|
133
|
-
|
|
134
|
-
@abc.abstractmethod
|
|
135
|
-
def get_service_port(self):
|
|
136
|
-
pass
|
|
137
|
-
|
|
138
|
-
@abc.abstractmethod
|
|
139
|
-
def get_monitoring_port(self):
|
|
140
|
-
pass
|
|
141
|
-
|
|
142
|
-
def get_ip_address(self):
|
|
143
|
-
return get_host_ip()
|
|
144
|
-
|
|
145
|
-
def get_storage_mnemonic(self):
|
|
146
|
-
return self.__class__.__name__
|
|
147
|
-
|
|
148
|
-
def get_process_status(self):
|
|
149
|
-
return self._process_status.as_dict()
|
|
150
|
-
|
|
151
|
-
def get_average_execution_times(self):
|
|
152
|
-
return get_average_execution_times()
|
|
153
|
-
|
|
154
|
-
def set_delay(self, seconds: float) -> float:
|
|
155
|
-
"""
|
|
156
|
-
Sets the delay time for monitoring. The delay time is the time between two successive executions of the
|
|
157
|
-
`get_status()` function of the device protocol.
|
|
158
|
-
|
|
159
|
-
It might happen that the delay time that is set is longer than what you requested. That is the case when
|
|
160
|
-
the execution of the `get_status()` function takes longer than the requested delay time. That should
|
|
161
|
-
prevent the server from blocking when a too short delay time is requested.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
seconds: the number of seconds between the monitoring calls.
|
|
165
|
-
Returns:
|
|
166
|
-
The delay that was set in milliseconds.
|
|
167
|
-
"""
|
|
168
|
-
execution_time = get_average_execution_time(self.device_protocol.get_status)
|
|
169
|
-
self.delay = max(seconds * 1000, (execution_time + 0.2) * 1000)
|
|
170
|
-
return self.delay
|
|
171
|
-
|
|
172
|
-
def set_hk_delay(self, seconds) -> float:
|
|
173
|
-
"""
|
|
174
|
-
Sets the delay time for housekeeping. The delay time is the time between two successive executions of the
|
|
175
|
-
`get_housekeeping()` function of the device protocol.
|
|
176
|
-
|
|
177
|
-
It might happen that the delay time that is set is longer than what you requested. That is the case when
|
|
178
|
-
the execution of the `get_housekeeping()` function takes longer than the requested delay time. That should
|
|
179
|
-
prevent the server from blocking when a too short delay time is requested.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
seconds: the number of seconds between the housekeeping calls.
|
|
183
|
-
Returns:
|
|
184
|
-
The delay that was set in milliseconds.
|
|
185
|
-
"""
|
|
186
|
-
execution_time = get_average_execution_time(self.device_protocol.get_housekeeping)
|
|
187
|
-
self.hk_delay = max(seconds * 1000, (execution_time + 0.2) * 1000)
|
|
188
|
-
return self.hk_delay
|
|
189
|
-
|
|
190
|
-
def set_logging_level(self, level):
|
|
191
|
-
self.logger.setLevel(level=level)
|
|
192
|
-
|
|
193
|
-
def quit(self):
|
|
194
|
-
self.interrupted = True
|
|
195
|
-
|
|
196
|
-
def before_serve(self):
|
|
197
|
-
pass
|
|
198
|
-
|
|
199
|
-
def after_serve(self):
|
|
200
|
-
pass
|
|
201
|
-
|
|
202
|
-
def is_storage_manager_active(self):
|
|
203
|
-
"""
|
|
204
|
-
This method needs to be implemented by the subclass if you need to store information.
|
|
205
|
-
|
|
206
|
-
Note: you might want to set a specific timeout when checking for the Storage Manager.
|
|
207
|
-
|
|
208
|
-
Note: If this method returns True, the following methods shall also be implemented by the subclass:
|
|
209
|
-
|
|
210
|
-
* register_to_storage_manager()
|
|
211
|
-
* unregister_from_storage_manager()
|
|
212
|
-
* store_housekeeping_information()
|
|
213
|
-
|
|
214
|
-
"""
|
|
215
|
-
return False
|
|
216
|
-
|
|
217
|
-
def serve(self):
|
|
218
|
-
|
|
219
|
-
self.before_serve()
|
|
220
|
-
|
|
221
|
-
# check if Storage Manager is available
|
|
222
|
-
|
|
223
|
-
storage_manager = self.is_storage_manager_active()
|
|
224
|
-
|
|
225
|
-
storage_manager and self.register_to_storage_manager()
|
|
226
|
-
|
|
227
|
-
# This approach is very simplistic and not time efficient
|
|
228
|
-
# We probably want to use a Timer that executes the monitoring and saving actions at
|
|
229
|
-
# dedicated times in the background.
|
|
230
|
-
|
|
231
|
-
# FIXME; we shall use the time.perf_counter() here!
|
|
232
|
-
|
|
233
|
-
last_time = time_in_ms()
|
|
234
|
-
last_time_hk = time_in_ms()
|
|
235
|
-
|
|
236
|
-
while True:
|
|
237
|
-
try:
|
|
238
|
-
socks = dict(self.poller.poll(50)) # timeout in milliseconds, do not block
|
|
239
|
-
except KeyboardInterrupt:
|
|
240
|
-
self.logger.warning("Keyboard interrupt caught!")
|
|
241
|
-
self.logger.warning(
|
|
242
|
-
"The ControlServer can not be interrupted with CTRL-C, "
|
|
243
|
-
"send a quit command to the server."
|
|
244
|
-
)
|
|
245
|
-
continue
|
|
246
|
-
|
|
247
|
-
if self.dev_ctrl_cmd_sock in socks:
|
|
248
|
-
self.device_protocol.execute()
|
|
249
|
-
|
|
250
|
-
if self.dev_ctrl_service_sock in socks:
|
|
251
|
-
self.service_protocol.execute()
|
|
252
|
-
|
|
253
|
-
# Now handle the periodic sending out of status information. A dictionary with the
|
|
254
|
-
# status or HK info is sent out periodically based on the DELAY time that is in the
|
|
255
|
-
# YAML config file.
|
|
256
|
-
|
|
257
|
-
if time_in_ms() - last_time >= self.delay:
|
|
258
|
-
last_time = time_in_ms()
|
|
259
|
-
# self.logger.debug("Sending status to monitoring processes.")
|
|
260
|
-
self.monitoring_protocol.send_status(
|
|
261
|
-
save_average_execution_time(self.device_protocol.get_status)
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
if time_in_ms() - last_time_hk >= self.hk_delay:
|
|
265
|
-
last_time_hk = time_in_ms()
|
|
266
|
-
if storage_manager:
|
|
267
|
-
# self.logger.debug("Sending housekeeping information to Storage.")
|
|
268
|
-
self.store_housekeeping_information(
|
|
269
|
-
save_average_execution_time(self.device_protocol.get_housekeeping)
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
if self.interrupted:
|
|
273
|
-
self.logger.info(
|
|
274
|
-
f"Quit command received, closing down the {self.__class__.__name__}."
|
|
275
|
-
)
|
|
276
|
-
break
|
|
277
|
-
|
|
278
|
-
# Some device protocol subclasses might start a number of threads or processes to
|
|
279
|
-
# support the commanding. Check if these threads/processes are still alive and
|
|
280
|
-
# terminate gracefully if they are not.
|
|
281
|
-
|
|
282
|
-
if not self.device_protocol.is_alive():
|
|
283
|
-
self.logger.error(
|
|
284
|
-
"Some Thread or sub-process that was started by Protocol has "
|
|
285
|
-
"died, terminating..."
|
|
286
|
-
)
|
|
287
|
-
break
|
|
288
|
-
|
|
289
|
-
storage_manager and self.unregister_from_storage_manager()
|
|
290
|
-
|
|
291
|
-
self.after_serve()
|
|
292
|
-
|
|
293
|
-
self.device_protocol.quit()
|
|
294
|
-
|
|
295
|
-
self.dev_ctrl_mon_sock.close()
|
|
296
|
-
self.dev_ctrl_service_sock.close()
|
|
297
|
-
self.dev_ctrl_cmd_sock.close()
|
|
298
|
-
|
|
299
|
-
close_all_zmq_handlers()
|
|
300
|
-
|
|
301
|
-
self.zcontext.term()
|
|
302
|
-
|
|
303
|
-
def store_housekeeping_information(self, data: dict):
|
|
304
|
-
"""
|
|
305
|
-
Send housekeeping information to the Storage manager.
|
|
306
|
-
|
|
307
|
-
Subclasses need to overwrite this method if they want the device housekeeping information to be saved.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
data (dict): a dictionary containing parameter name and value of all device housekeeping. There is also
|
|
311
|
-
a timestamp that represents the date/time when the HK was received from the device.
|
|
312
|
-
"""
|
|
313
|
-
pass
|
|
314
|
-
|
|
315
|
-
def register_to_storage_manager(self):
|
|
316
|
-
"""
|
|
317
|
-
Register this ControlServer to the Storage Manager so the housekeeping information of the device can be saved.
|
|
318
|
-
|
|
319
|
-
Subclasses need to overwrite this method if they have housekeeping information to be stored. The following
|
|
320
|
-
information is required for the registration:
|
|
321
|
-
|
|
322
|
-
* origin: can be retrieved from `self.get_storage_mnemonic()`
|
|
323
|
-
* persistence_class: one of the TYPES in egse.storage.persistence
|
|
324
|
-
* prep: depending on the type of the persistence class (see respective documentation)
|
|
325
|
-
|
|
326
|
-
The `egse.storage` module provides a convenience method that can be called from the method in the subclass:
|
|
327
|
-
|
|
328
|
-
>>> from egse.storage import register_to_storage_manager # noqa
|
|
329
|
-
|
|
330
|
-
Note: the `egse.storage` module might not be available, it is provided by the `cgse-core` package.
|
|
331
|
-
"""
|
|
332
|
-
pass
|
|
333
|
-
|
|
334
|
-
def unregister_from_storage_manager(self):
|
|
335
|
-
"""
|
|
336
|
-
Unregister this ControlServer from the Storage manager.
|
|
337
|
-
|
|
338
|
-
Subclasses need to overwrite this method. The following information is required for the registration:
|
|
339
|
-
|
|
340
|
-
* origin: can be retrieved from `self.get_storage_mnemonic()`
|
|
341
|
-
|
|
342
|
-
The `egse.storage` module provides a convenience method that can be called from the method in the subclass:
|
|
343
|
-
|
|
344
|
-
>>> from egse.storage import unregister_from_storage_manager # noqa
|
|
345
|
-
|
|
346
|
-
Note: the `egse.storage` module might not be available, it is provided by the `cgse-core` package.
|
|
347
|
-
"""
|
|
348
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|