cgse-common 2025.0.6__py3-none-any.whl → 2025.0.8__py3-none-any.whl
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/__init__.py +0 -0
- cgse_common/settings.yaml +10 -0
- {cgse_common-2025.0.6.dist-info → cgse_common-2025.0.8.dist-info}/METADATA +1 -1
- {cgse_common-2025.0.6.dist-info → cgse_common-2025.0.8.dist-info}/RECORD +12 -10
- egse/control.py +196 -96
- egse/device.py +4 -3
- egse/mixin.py +7 -0
- egse/protocol.py +3 -3
- egse/services.py +2 -2
- egse/setup.py +2 -1
- {cgse_common-2025.0.6.dist-info → cgse_common-2025.0.8.dist-info}/WHEEL +0 -0
- {cgse_common-2025.0.6.dist-info → cgse_common-2025.0.8.dist-info}/entry_points.txt +0 -0
cgse_common/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
PACKAGES:
|
|
2
|
+
CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE
|
|
3
|
+
|
|
4
|
+
SITE:
|
|
5
|
+
ID: XXXX # The SITE ID shall be filled in by the local settings file
|
|
6
|
+
SSH_SERVER: localhost # The IP address of the SSH server on your site
|
|
7
|
+
SSH_PORT: 22 # The TCP/IP port on which the SSH server is listening
|
|
8
|
+
|
|
9
|
+
PROCESS:
|
|
10
|
+
METRICS_INTERVAL: 10
|
|
@@ -1,36 +1,38 @@
|
|
|
1
|
+
cgse_common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cgse_common/settings.yaml,sha256=PS8HOoxbhwVoQ7zzDtZyhx25RZB6SGq3uXNuJTgtRIw,443
|
|
1
3
|
egse/bits.py,sha256=1DPcC5R-8w-KLfbr2WLx-4Ezfwm6jpRMz-E5QfQ_YKw,11138
|
|
2
4
|
egse/calibration.py,sha256=DZ-LgEwamTWDukMqTr2JLCPt3M1lw6W0SfpHQfG7cXg,8989
|
|
3
5
|
egse/command.py,sha256=su35PU_0Bru1Es4up1lJFps9FTQpJ7xcxl9Y4w8suxA,22880
|
|
4
6
|
egse/config.py,sha256=Ib1Ddt0wFwRrioBvMs7Md-wgEEPdK3eaXCt0H7HJEow,14888
|
|
5
|
-
egse/control.py,sha256=
|
|
7
|
+
egse/control.py,sha256=Z4aeYfZkzdULMWFgPilAR9AlZu5I0vAUizCPSdINJaY,16567
|
|
6
8
|
egse/decorators.py,sha256=YDoFNn7Fe-voKX2iui85POciXuLs0P5rW1TNtxpejUM,13629
|
|
7
|
-
egse/device.py,sha256=
|
|
9
|
+
egse/device.py,sha256=sc4cx-xOXjmewVZuXSSzROv1gqP8BqUycojiEmqV6ck,8501
|
|
8
10
|
egse/env.py,sha256=IzT_DYTHKlqtXDR8fWI9n7xj7KnL58UE6_mZUCdQMss,28279
|
|
9
11
|
egse/exceptions.py,sha256=Tc1xqUrWxV6SXxc9xB9viK_O-GVa_SpzqZUVEhZkwzA,1801
|
|
10
12
|
egse/hk.py,sha256=5WMOGlx043XmEPj7ML1TkDzA8ZaTYLD-i5OcNUxE268,31163
|
|
11
13
|
egse/metrics.py,sha256=cZKMEe3OTT2uomj7vXjEl54JD0CStfEC4nCgS6U5YSM,3794
|
|
12
|
-
egse/mixin.py,sha256=
|
|
14
|
+
egse/mixin.py,sha256=xiBcN0jQHwaeTRceCQbYaWNmL4ZfejGT0RPooXfi7Tc,17470
|
|
13
15
|
egse/monitoring.py,sha256=-pwXqPoiNKzQYKQSpKddFlaPkCTJZYdxvG1d2MBN3l0,3033
|
|
14
16
|
egse/observer.py,sha256=6faaLHqgpOQs_oEvdBygQ5HF7mGneKJEfyQEFUFA5VY,1069
|
|
15
17
|
egse/obsid.py,sha256=-HPuHApZrr3Nj1J2-qqnIiE814C-gm4FSHdM2afKdRY,5883
|
|
16
18
|
egse/persistence.py,sha256=Lx6LMJ1-dh8N43XF7tTM6RwD0sSETiGQ9DNqug-G-zQ,2160
|
|
17
19
|
egse/plugin.py,sha256=Vuy4WkymGEt-_fibUIoVxdF1pcCS3FaXwtiYhUIdjcE,4820
|
|
18
20
|
egse/process.py,sha256=mQ2ojeL_9oE_QkMJlQDPd1290z0j2mOrGXrlrWtOtzI,16615
|
|
19
|
-
egse/protocol.py,sha256=
|
|
21
|
+
egse/protocol.py,sha256=dQVQ8U0AitjvkOxJRrJJ7dvYAn0vxOfi5IL6gxmCyeQ,23837
|
|
20
22
|
egse/proxy.py,sha256=pMKdnF62SXm0quLoKfgvK9GFkH2mLMB5fWNrZenfqQQ,18100
|
|
21
23
|
egse/reload.py,sha256=rDT0bC6RFeRhW38wSgFcxr30h8FvaKkoGp_OE-AwBB4,4388
|
|
22
24
|
egse/resource.py,sha256=VoB7BVrQULT_SJ1XioDzB59-uH47nUcN-KNVLvFxiFE,15163
|
|
23
25
|
egse/response.py,sha256=WFtWftovrmmn92NW4mhJjibMtCWteZmAzNuPLVsV-lg,2716
|
|
24
|
-
egse/services.py,sha256=
|
|
26
|
+
egse/services.py,sha256=nobKsApRxBIg7Vxy67MJWH2qT47mJfDOaxMIpc0n7fg,7716
|
|
25
27
|
egse/services.yaml,sha256=p8QBF56zLI21iJ9skt65VlNz4rIqRoFfBTZxOIUZCZ4,1853
|
|
26
28
|
egse/settings.py,sha256=lmIfg3lwIuHrI8JL5qfiAaXHjblC87_W0c2mjbj-vRA,15562
|
|
27
29
|
egse/settings.yaml,sha256=mz9O2QqmiptezsMvxJRLhnC1ROwIHENX0nbnhMaXUpE,190
|
|
28
|
-
egse/setup.py,sha256=
|
|
30
|
+
egse/setup.py,sha256=YaJaCMfU1OO92Rg3Yt85Ug5M0y713fmXSvJzJ4l-t1M,44117
|
|
29
31
|
egse/state.py,sha256=ekcCZu_DZKkKYn-5iWG7ij7Aif2WYMNVs5h3cia-cVc,5352
|
|
30
32
|
egse/system.py,sha256=KCXz8eH3PNwLaB7ww3OCkK_tQoaTYhh2FEwX1mvN8RQ,48996
|
|
31
33
|
egse/version.py,sha256=-EMuiSn2eEp8QJX6csmBEu1m2yGqlXHJj2Hj49w6a2k,6217
|
|
32
34
|
egse/zmq_ser.py,sha256=2-nwVUBWZ3vvosKNmlWobHJrIJA2HlM3V5a63Gz2JY0,1819
|
|
33
|
-
cgse_common-2025.0.
|
|
34
|
-
cgse_common-2025.0.
|
|
35
|
-
cgse_common-2025.0.
|
|
36
|
-
cgse_common-2025.0.
|
|
35
|
+
cgse_common-2025.0.8.dist-info/METADATA,sha256=RjnnRJWVNUQIMsGfZAmOUM6k5VNsmV_IfCJbOUfdgjo,2574
|
|
36
|
+
cgse_common-2025.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
37
|
+
cgse_common-2025.0.8.dist-info/entry_points.txt,sha256=MtsCYBzfuc3afzsjbdGJ-TZRSS8rhof5fI9w86_nGkQ,91
|
|
38
|
+
cgse_common-2025.0.8.dist-info/RECORD,,
|
egse/control.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module defines the abstract class for any
|
|
2
|
+
This module defines the abstract class for any Control Server and some convenience functions.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import abc
|
|
5
6
|
import logging
|
|
6
7
|
import pickle
|
|
7
8
|
import threading
|
|
9
|
+
from typing import Union
|
|
8
10
|
|
|
9
11
|
import zmq
|
|
10
12
|
|
|
@@ -30,16 +32,18 @@ PROCESS_SETTINGS = Settings.load("PROCESS")
|
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def is_control_server_active(endpoint: str = None, timeout: float = 0.5) -> bool:
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
36
39
|
|
|
37
40
|
Args:
|
|
38
|
-
endpoint (str):
|
|
39
|
-
timeout (float):
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
42
45
|
"""
|
|
46
|
+
|
|
43
47
|
ctx = zmq.Context.instance()
|
|
44
48
|
|
|
45
49
|
return_code = False
|
|
@@ -50,6 +54,7 @@ def is_control_server_active(endpoint: str = None, timeout: float = 0.5) -> bool
|
|
|
50
54
|
data = pickle.dumps("Ping")
|
|
51
55
|
socket.send(data)
|
|
52
56
|
rlist, _, _ = zmq.select([socket], [], [], timeout=timeout)
|
|
57
|
+
|
|
53
58
|
if socket in rlist:
|
|
54
59
|
data = socket.recv()
|
|
55
60
|
response = pickle.loads(data)
|
|
@@ -62,20 +67,22 @@ def is_control_server_active(endpoint: str = None, timeout: float = 0.5) -> bool
|
|
|
62
67
|
|
|
63
68
|
|
|
64
69
|
class ControlServer(metaclass=abc.ABCMeta):
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
69
74
|
|
|
70
75
|
The sub-class shall define the following:
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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`
|
|
75
80
|
|
|
76
81
|
"""
|
|
77
82
|
|
|
78
83
|
def __init__(self):
|
|
84
|
+
""" Initialisation of a new Control Server."""
|
|
85
|
+
|
|
79
86
|
from egse.monitoring import MonitoringProtocol
|
|
80
87
|
from egse.services import ServiceProtocol
|
|
81
88
|
|
|
@@ -86,15 +93,14 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
86
93
|
self._timer_thread.daemon = True
|
|
87
94
|
self._timer_thread.start()
|
|
88
95
|
|
|
89
|
-
# The logger will be overwritten by the sub-class, if not,
|
|
90
|
-
#
|
|
91
|
-
# overwrite the logger attribute.
|
|
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.
|
|
92
98
|
|
|
93
99
|
self.logger = logging.getLogger(get_full_classname(self))
|
|
94
100
|
|
|
95
101
|
self.interrupted = False
|
|
96
|
-
self.
|
|
97
|
-
self.hk_delay = 1000
|
|
102
|
+
self.mon_delay = 1000 # Delay between publish status information [ms]
|
|
103
|
+
self.hk_delay = 1000 # Delay between saving housekeeping information [ms]
|
|
98
104
|
|
|
99
105
|
self.zcontext = zmq.Context.instance()
|
|
100
106
|
self.poller = zmq.Poller()
|
|
@@ -103,118 +109,206 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
103
109
|
self.service_protocol = ServiceProtocol(self)
|
|
104
110
|
self.monitoring_protocol = MonitoringProtocol(self)
|
|
105
111
|
|
|
106
|
-
#
|
|
112
|
+
# Set up the Control Server waiting for service requests
|
|
107
113
|
|
|
108
114
|
self.dev_ctrl_service_sock = self.zcontext.socket(zmq.REP)
|
|
109
115
|
self.service_protocol.bind(self.dev_ctrl_service_sock)
|
|
110
116
|
|
|
111
|
-
#
|
|
117
|
+
# Set up the Control Server for sending monitoring info
|
|
112
118
|
|
|
113
119
|
self.dev_ctrl_mon_sock = self.zcontext.socket(zmq.PUB)
|
|
114
120
|
self.monitoring_protocol.bind(self.dev_ctrl_mon_sock)
|
|
115
121
|
|
|
116
|
-
#
|
|
122
|
+
# Set up the Control Server waiting for device commands.
|
|
117
123
|
# The device protocol shall bind the socket in the sub-class
|
|
118
124
|
|
|
119
125
|
self.dev_ctrl_cmd_sock = self.zcontext.socket(zmq.REP)
|
|
120
126
|
|
|
121
|
-
#
|
|
127
|
+
# Initialise the poll set
|
|
122
128
|
|
|
123
129
|
self.poller.register(self.dev_ctrl_service_sock, zmq.POLLIN)
|
|
124
130
|
self.poller.register(self.dev_ctrl_mon_sock, zmq.POLLIN)
|
|
125
131
|
|
|
126
132
|
@abc.abstractmethod
|
|
127
|
-
def get_communication_protocol(self):
|
|
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
|
+
|
|
128
139
|
pass
|
|
129
140
|
|
|
130
141
|
@abc.abstractmethod
|
|
131
|
-
def get_commanding_port(self):
|
|
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
|
+
|
|
132
148
|
pass
|
|
133
149
|
|
|
134
150
|
@abc.abstractmethod
|
|
135
|
-
def get_service_port(self):
|
|
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
|
+
|
|
136
157
|
pass
|
|
137
158
|
|
|
138
159
|
@abc.abstractmethod
|
|
139
|
-
def get_monitoring_port(self):
|
|
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
|
+
|
|
140
166
|
pass
|
|
141
167
|
|
|
142
|
-
def get_ip_address(self):
|
|
168
|
+
def get_ip_address(self) -> str:
|
|
169
|
+
|
|
143
170
|
return get_host_ip()
|
|
144
171
|
|
|
145
|
-
def get_storage_mnemonic(self):
|
|
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
|
+
|
|
146
181
|
return self.__class__.__name__
|
|
147
182
|
|
|
148
|
-
def get_process_status(self):
|
|
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
|
+
|
|
149
189
|
return self._process_status.as_dict()
|
|
150
190
|
|
|
151
|
-
def get_average_execution_times(self):
|
|
152
|
-
|
|
191
|
+
def get_average_execution_times(self) -> dict:
|
|
192
|
+
""" Returns the average execution times of all functions that have been monitored by this process.
|
|
153
193
|
|
|
154
|
-
|
|
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.
|
|
155
196
|
"""
|
|
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
197
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
162
209
|
|
|
163
210
|
Args:
|
|
164
|
-
seconds:
|
|
165
|
-
|
|
166
|
-
|
|
211
|
+
seconds (float): Number of seconds between the monitoring calls
|
|
212
|
+
|
|
213
|
+
Returns: Delay that was set [ms].
|
|
167
214
|
"""
|
|
215
|
+
|
|
168
216
|
execution_time = get_average_execution_time(self.device_protocol.get_status)
|
|
169
|
-
self.
|
|
170
|
-
return self.delay
|
|
217
|
+
self.mon_delay = max(seconds * 1000, (execution_time + 0.2) * 1000)
|
|
171
218
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
219
|
+
return self.mon_delay
|
|
220
|
+
|
|
221
|
+
def set_hk_delay(self, seconds: float) -> float:
|
|
222
|
+
""" Sets the delay time for housekeeping.
|
|
176
223
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.
|
|
180
230
|
|
|
181
231
|
Args:
|
|
182
|
-
seconds:
|
|
183
|
-
|
|
184
|
-
|
|
232
|
+
seconds (float): Number of seconds between the housekeeping calls
|
|
233
|
+
|
|
234
|
+
Returns: Delay that was set [ms].
|
|
185
235
|
"""
|
|
236
|
+
|
|
186
237
|
execution_time = get_average_execution_time(self.device_protocol.get_housekeeping)
|
|
187
238
|
self.hk_delay = max(seconds * 1000, (execution_time + 0.2) * 1000)
|
|
239
|
+
|
|
188
240
|
return self.hk_delay
|
|
189
241
|
|
|
190
|
-
def set_logging_level(self, level):
|
|
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
|
+
|
|
191
259
|
self.logger.setLevel(level=level)
|
|
192
260
|
|
|
193
|
-
def quit(self):
|
|
261
|
+
def quit(self) -> None:
|
|
262
|
+
""" Interrupts the Control Server."""
|
|
263
|
+
|
|
194
264
|
self.interrupted = True
|
|
195
265
|
|
|
196
|
-
def before_serve(self):
|
|
266
|
+
def before_serve(self) -> None:
|
|
267
|
+
""" Steps to take before the Control Server is activated."""
|
|
268
|
+
|
|
197
269
|
pass
|
|
198
270
|
|
|
199
|
-
def after_serve(self):
|
|
271
|
+
def after_serve(self) -> None:
|
|
272
|
+
""" Steps to take after the Control Server has been deactivated."""
|
|
273
|
+
|
|
200
274
|
pass
|
|
201
275
|
|
|
202
|
-
def is_storage_manager_active(self):
|
|
203
|
-
"""
|
|
204
|
-
|
|
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.
|
|
205
280
|
|
|
206
|
-
Note:
|
|
281
|
+
Note: You might want to set a specific timeout when checking for the Storage Manager.
|
|
207
282
|
|
|
208
|
-
Note: If this method returns True, the following methods shall also be implemented by the
|
|
283
|
+
Note: If this method returns True, the following methods shall also be implemented by the sub-class:
|
|
209
284
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
- register_to_storage_manager()
|
|
286
|
+
- unregister_from_storage_manager()
|
|
287
|
+
- store_housekeeping_information()
|
|
213
288
|
|
|
289
|
+
Returns: True if the Storage Manager is active; False otherwise.
|
|
214
290
|
"""
|
|
291
|
+
|
|
215
292
|
return False
|
|
216
293
|
|
|
217
|
-
def serve(self):
|
|
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
|
+
"""
|
|
218
312
|
|
|
219
313
|
self.before_serve()
|
|
220
314
|
|
|
@@ -239,8 +333,7 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
239
333
|
except KeyboardInterrupt:
|
|
240
334
|
self.logger.warning("Keyboard interrupt caught!")
|
|
241
335
|
self.logger.warning(
|
|
242
|
-
"The ControlServer can not be interrupted with CTRL-C, "
|
|
243
|
-
"send a quit command to the server."
|
|
336
|
+
"The ControlServer can not be interrupted with CTRL-C, send a quit command to the server instead."
|
|
244
337
|
)
|
|
245
338
|
continue
|
|
246
339
|
|
|
@@ -250,17 +343,19 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
250
343
|
if self.dev_ctrl_service_sock in socks:
|
|
251
344
|
self.service_protocol.execute()
|
|
252
345
|
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
# YAML config file.
|
|
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
|
|
256
348
|
|
|
257
|
-
if time_in_ms() - last_time >= self.
|
|
349
|
+
if time_in_ms() - last_time >= self.mon_delay:
|
|
258
350
|
last_time = time_in_ms()
|
|
259
351
|
# self.logger.debug("Sending status to monitoring processes.")
|
|
260
352
|
self.monitoring_protocol.send_status(
|
|
261
353
|
save_average_execution_time(self.device_protocol.get_status)
|
|
262
354
|
)
|
|
263
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
|
+
|
|
264
359
|
if time_in_ms() - last_time_hk >= self.hk_delay:
|
|
265
360
|
last_time_hk = time_in_ms()
|
|
266
361
|
if storage_manager:
|
|
@@ -275,14 +370,12 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
275
370
|
)
|
|
276
371
|
break
|
|
277
372
|
|
|
278
|
-
# Some device protocol
|
|
279
|
-
#
|
|
280
|
-
# terminate gracefully if they are not.
|
|
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.
|
|
281
375
|
|
|
282
376
|
if not self.device_protocol.is_alive():
|
|
283
377
|
self.logger.error(
|
|
284
|
-
"Some Thread or sub-process that was started by Protocol has "
|
|
285
|
-
"died, terminating..."
|
|
378
|
+
"Some Thread or sub-process that was started by Protocol has died, terminating..."
|
|
286
379
|
)
|
|
287
380
|
break
|
|
288
381
|
|
|
@@ -300,11 +393,11 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
300
393
|
|
|
301
394
|
self.zcontext.term()
|
|
302
395
|
|
|
303
|
-
def store_housekeeping_information(self, data: dict):
|
|
304
|
-
"""
|
|
305
|
-
Send housekeeping information to the Storage manager.
|
|
396
|
+
def store_housekeeping_information(self, data: dict) -> None:
|
|
397
|
+
""" Sends housekeeping information to the Storage Manager.
|
|
306
398
|
|
|
307
|
-
|
|
399
|
+
This method has to be overwritten by the sub-classes if they want the device housekeeping information to be
|
|
400
|
+
saved.
|
|
308
401
|
|
|
309
402
|
Args:
|
|
310
403
|
data (dict): a dictionary containing parameter name and value of all device housekeeping. There is also
|
|
@@ -312,16 +405,21 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
312
405
|
"""
|
|
313
406
|
pass
|
|
314
407
|
|
|
315
|
-
def register_to_storage_manager(self):
|
|
316
|
-
"""
|
|
317
|
-
|
|
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.
|
|
318
417
|
|
|
319
|
-
|
|
320
|
-
information is required for the registration:
|
|
418
|
+
The following information is required for the registration:
|
|
321
419
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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)
|
|
325
423
|
|
|
326
424
|
The `egse.storage` module provides a convenience method that can be called from the method in the subclass:
|
|
327
425
|
|
|
@@ -331,18 +429,20 @@ class ControlServer(metaclass=abc.ABCMeta):
|
|
|
331
429
|
"""
|
|
332
430
|
pass
|
|
333
431
|
|
|
334
|
-
def unregister_from_storage_manager(self):
|
|
335
|
-
"""
|
|
336
|
-
Unregister this ControlServer from the Storage manager.
|
|
432
|
+
def unregister_from_storage_manager(self) -> None:
|
|
433
|
+
""" Unregisters the Control Server from the Storage Manager.
|
|
337
434
|
|
|
338
|
-
|
|
435
|
+
This method has to be overwritten by the sub-classes.
|
|
339
436
|
|
|
340
|
-
|
|
437
|
+
The following information is required for the registration:
|
|
341
438
|
|
|
342
|
-
|
|
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:
|
|
343
442
|
|
|
344
443
|
>>> from egse.storage import unregister_from_storage_manager # noqa
|
|
345
444
|
|
|
346
445
|
Note: the `egse.storage` module might not be available, it is provided by the `cgse-core` package.
|
|
347
446
|
"""
|
|
447
|
+
|
|
348
448
|
pass
|
egse/device.py
CHANGED
|
@@ -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
|
|
egse/mixin.py
CHANGED
|
@@ -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
|
*,
|
egse/protocol.py
CHANGED
|
@@ -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
|
egse/services.py
CHANGED
|
@@ -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
|
|
egse/setup.py
CHANGED
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|