cgse-common 2024.1.1__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.
egse/resource.py ADDED
@@ -0,0 +1,438 @@
1
+ """
2
+ This module provides convenience functions to use resources in your code without
3
+ the need to specify an absolute path or try to locate the resources in your local
4
+ installation (which is time-consuming, error-prone, and introduces quite some
5
+ redundancy).
6
+
7
+ Resources can be files of different format that are distributed together with the
8
+ source code, e.g.
9
+
10
+ * image data
11
+ * icons
12
+ * YAML files
13
+ * binary files, e.g. dynamic libraries
14
+ * style files for GUI applications
15
+ * calibration files distributed with the source code
16
+
17
+ Each of the resources have a fixed location within the source tree and is identified
18
+ with a resource identifier. There are a number of default identifier that are defined
19
+ as follows:
20
+
21
+ * `icons`: located in the sub-folder 'icons'
22
+ * `images`: located in the sub-folder 'images'
23
+ * `styles`: located in the sub-folder 'data'
24
+ * `data`: located in the sub-folder 'data'
25
+ * `lib`: located in sub-folder 'lib'
26
+
27
+ Resource shall be initialised by either the process or the library using the function
28
+ `initialise_resources()`. The function optionally takes a locations in which it will
29
+ search for the default resource ids. The usual way to initialise is:
30
+
31
+ >>> initialise_resources(Path(__file__).parent)
32
+
33
+ Another way to make your resource available is through entry points in the
34
+ distribution of your package. In the `pyproject.toml` file, you can specify resources
35
+ as follows:
36
+
37
+ [project.entry-points."cgse.resource"]
38
+ icons = 'egse.gui.icons'
39
+ styles = 'egse.gui.styles'
40
+
41
+ This will automatically add these resources during initialisation.
42
+
43
+ Resources can be accessed from the code without specifying the absolute pathname,
44
+ using a `:/resource_id/` that is known by the resource module. A wildcard can be
45
+ introduced after the `resource_id` to indicate the resource is in one of the
46
+ subdirectories.
47
+
48
+ Example usage:
49
+ * get_resource(":/icons/open-document.png")
50
+ * get_resource(":/styles/dark.qss")
51
+ * get_resource(":/lib/*/EtherSpaceLink_v34_86.dylib")
52
+
53
+ A new `resource_id` can be added with the `add_resource_id()` function, specifying a
54
+ resource_id string and a location. That location will be added to the list of locations
55
+ for that resource id. To add a resource location for e.g. configuration files, do
56
+
57
+ >>> from egse.resource import add_resource_id
58
+ >>> add_resource_id('styles', Path(__file__).parent)
59
+
60
+ Alternatives
61
+
62
+ The `egse.config` module has a number of alternatives for locating files and resources.
63
+
64
+ * find_file(..) and find_files(..)
65
+ * find_dir(..) and find_dirs(..)
66
+ * get_resource_dirs()
67
+ * get_resource_path()
68
+
69
+ The functions for finding files and directories are more flexible, but take more
70
+ time and effort. They are mainly used for dynamically searching for a file or
71
+ folder, not necessarily within the source code location.
72
+
73
+ The resource specific functions in the egse.config module will be deprecated when
74
+ their functionality is fully replaced by this `egse.resource` module.
75
+
76
+ """
77
+
78
+ import errno
79
+ import logging
80
+ import re
81
+ from pathlib import Path
82
+ from pathlib import PurePath
83
+ from typing import Dict
84
+ from typing import List
85
+ from typing import Union
86
+
87
+ from os.path import exists
88
+ from os.path import join
89
+
90
+ from egse.config import find_first_occurrence_of_dir
91
+ from egse.config import find_files
92
+ from egse.exceptions import InternalError
93
+ from egse.plugin import entry_points
94
+ from egse.system import get_package_location
95
+
96
+ _LOGGER = logging.getLogger(__name__)
97
+
98
+
99
+ class ResourceError(Exception):
100
+ """Base class, raised when a resource is not defined."""
101
+
102
+
103
+ class AmbiguityError(ResourceError):
104
+ """Raised when more than one option is possible."""
105
+
106
+
107
+ class NoSuchFileError(ResourceError):
108
+ """Raised when no file could be found for the given resource."""
109
+
110
+
111
+ __all__ = [
112
+ "get_resource",
113
+ "get_resource_locations",
114
+ "get_resource_dirs",
115
+ "get_resource_path",
116
+ "add_resource_id",
117
+ "initialise_resources",
118
+ "ResourceError",
119
+ "AmbiguityError",
120
+ "NoSuchFileError",
121
+ ]
122
+
123
+ # Testing regex: https://pythex.org
124
+
125
+ PATTERN = re.compile(r"^:/(\w+)/(\*+/)?(.*)$")
126
+
127
+ # Default resources will be checked when executing the initialise_resources()
128
+ # optionally with a path as root. It is not needed to use add_resource_id()
129
+ # for these resources.
130
+
131
+ DEFAULT_RESOURCES = {
132
+ "icons": "/icons",
133
+ "images": "/images",
134
+ "styles": "/styles",
135
+ "lib": "/lib",
136
+ "data": "/data",
137
+ }
138
+
139
+ _RESOURCE_DIRS = ["resources", "icons", "images", "styles", "data"]
140
+
141
+ resources = {}
142
+
143
+
144
+ def check_if_file_exists(filename: Union[Path, str], resource_id: str = None) -> Path:
145
+ """
146
+ Check if the given filename exists. If the filename exists, return the filename, else raise a
147
+ NoSuchFileError.
148
+
149
+ Args:
150
+ filename (Path|str): an absolute filename
151
+ resource_id (str): a resource identifier
152
+
153
+ Return:
154
+ The given filename if it exists.
155
+
156
+ Raises:
157
+ NoSuchFileError if the given filename doesn't exist.
158
+ """
159
+ filename = Path(filename)
160
+ if filename.is_file():
161
+ return filename
162
+
163
+ if resource_id:
164
+ raise NoSuchFileError(
165
+ f"The file '{filename.name}' could not be found for the given resource '{resource_id}'")
166
+ else:
167
+ raise NoSuchFileError(f"The file '{filename.name}' doesn't exist.")
168
+
169
+
170
+ def contains_wildcard(filename: str):
171
+ """
172
+ Returns True if the filename contains a wildcard, otherwise False.
173
+ A wildcard is an asterisk '*' or a question mark '?' character.
174
+ """
175
+ if '*' in filename:
176
+ return True
177
+ if '?' in filename:
178
+ return True
179
+
180
+ return False
181
+
182
+
183
+ def get_resource_locations() -> Dict[str, List[Path]]:
184
+ """
185
+ Returns a dictionary of names that can be used as resource location.
186
+ The keys are strings that are recognised as valid resource identifiers, the
187
+ values are a list of their actual absolute path names.
188
+ """
189
+ return resources.copy()
190
+
191
+
192
+ def get_resource_dirs(root_dir: Union[str, PurePath]) -> List[Path]:
193
+ """
194
+ Define directories that contain resources like images, icons, and data files.
195
+
196
+ Resource directories can have the following names: `resources`, `data`, `icons`, or `images`.
197
+ This function checks if any of the resource directories exist in the `root_dir` that is given as an argument.
198
+
199
+ For all existing directories the function returns the absolute path.
200
+
201
+ If the argument root_dir is None, an empty list will be returned and a warning message will be issued.
202
+
203
+ Args:
204
+ root_dir (str): the directory to search for resource folders
205
+
206
+ Returns:
207
+ a list of absolute Paths.
208
+ """
209
+
210
+ if root_dir is None:
211
+ _LOGGER.warning("The argument root_dir can not be None, an empty list is returned.")
212
+ return []
213
+
214
+ root_dir = Path(root_dir).resolve()
215
+ if not root_dir.is_dir():
216
+ root_dir = root_dir.parent
217
+
218
+ result = []
219
+ for dir_ in _RESOURCE_DIRS:
220
+ if (root_dir / dir_).is_dir():
221
+ result.append(Path(root_dir, dir_).resolve())
222
+
223
+ return result
224
+
225
+
226
+ def get_resource_path(name: str, resource_root_dir: Union[str, PurePath] = None) -> PurePath:
227
+ """
228
+ Searches for a data file (resource) with the given name.
229
+
230
+ When `resource_root_dir` is not given, the search for resources will start at the root
231
+ folder of the project (using the function `get_common_egse_root()`). Any other root
232
+ directory can be given, e.g. if you want to start the search from the location of your
233
+ source code file, use `Path(__file__).parent` as the `resource_root_dir` argument.
234
+
235
+ Args:
236
+ name (str): the name of the resource that is requested
237
+ resource_root_dir (str): the root directory w_HERE the search for resources should be started
238
+
239
+ Returns:
240
+ the absolute path of the data file with the given name. The first name that matches
241
+ is returned. If no file with the given name or path exists, a FileNotFoundError is raised.
242
+
243
+ """
244
+ for resource_dir in get_resource_dirs(resource_root_dir):
245
+ resource_path = join(resource_dir, name)
246
+ if exists(resource_path):
247
+ return Path(resource_path).absolute()
248
+ raise FileNotFoundError(errno.ENOENT, f"Could not locate resource '{name}'")
249
+
250
+
251
+ def initialise_resources(root: Union[Path, str] = Path(__file__).parent):
252
+ """
253
+ Initialise the default resources and any resource published by a package entry point.
254
+
255
+ The argument `root` specifies the root location for the resources. If not specified,
256
+ the location of this module is taken as the root location. So, if you have installed
257
+ this package with `pip install`, you should give the location of your project's source
258
+ code as the root argument.
259
+
260
+ When you have specified entry points for the group 'cgse.resource' in your project,
261
+ these resources will also be initialised. Check the global documentation of this module
262
+ for an example entry point.
263
+
264
+ Args:
265
+ root (Path|str): the root location for the resources.
266
+
267
+ Returns:
268
+ None.
269
+ """
270
+
271
+ # the resources with their absolute path names
272
+
273
+ for resource_id in DEFAULT_RESOURCES:
274
+ folder = find_first_occurrence_of_dir(DEFAULT_RESOURCES[resource_id], root=root)
275
+ if folder is not None:
276
+ x = resources.setdefault(resource_id, [])
277
+ if folder not in x:
278
+ x.append(folder)
279
+
280
+ for ep in entry_points("cgse.resource"):
281
+ for location in get_package_location(ep.value):
282
+ x = resources.setdefault(ep.name, [])
283
+ if location not in x:
284
+ x.append(location)
285
+
286
+ _LOGGER.debug(f"Resources have been initialised: {resources = }")
287
+
288
+
289
+ def print_resources():
290
+ """Prints the currently defined resources."""
291
+
292
+ if resources:
293
+ print("Available resources:")
294
+ else:
295
+ print("No resources defined.")
296
+ return
297
+
298
+ for resource_id, locations in resources.items():
299
+ print(f" {resource_id}:")
300
+ for location in locations:
301
+ print(f" {location}")
302
+
303
+
304
+ def add_resource_id(resource_id: str, location: Union[Path, str]):
305
+ """
306
+ Adds a resource identifier with the given location. Resources can then be specified
307
+ using this resource id.
308
+
309
+ The location can be an absolute or relative pathname. In the latter case the path
310
+ will be expanded against the current working directory.
311
+
312
+ Args:
313
+ resource_id (str): a resource identifier
314
+ location (Path|str): an absolute or relative pathname
315
+
316
+ Returns:
317
+ ValueError if the location can not be determined or is not a directory.
318
+ """
319
+
320
+ # Check if location exists and is a directory.
321
+
322
+ location = Path(location).expanduser().resolve()
323
+
324
+ if not location.exists():
325
+ raise ValueError(f"Unknown location '{location}'")
326
+
327
+ if location.is_dir():
328
+ x = resources.setdefault(resource_id, [])
329
+ if location not in x:
330
+ x.append(location)
331
+ else:
332
+ raise ValueError(f"Location is not a directory: {location=}")
333
+
334
+
335
+ def get_resource(resource_locator: str) -> Path:
336
+ """
337
+ Returns the absolute Path for the given resource_locator. The resource_locator consists of
338
+ a resource_id, an optional wildcard and a filename separated by a forward slash '/' and
339
+ started by a colon ':'.
340
+
341
+ ':/<resource_id>/[*/]<filename>'
342
+
343
+ If the resource_locator starts with a colon ':', the name will be interpreted as a resource_id
344
+ and filename combination and parsed as such
345
+
346
+ If the resource_locator doesn't start with a colon ':', then the string will be interpreted as a
347
+ Path name and returned if that path exists, otherwise a ResourceError is raised.
348
+
349
+ The filename can contain the wildcard '*' and/or '?', however the use of a wildcard in the
350
+ filename can still only match one unique filename. This can be useful e.g. if you know the
351
+ filename except for one part of it like a timestamp. Used, e.g., for matching Setup files which
352
+ are unique filenames with a timestamp.
353
+
354
+ Args:
355
+ resource_locator (str): a special resource name or a filename
356
+
357
+ Returns:
358
+ a Path with the absolute filename for the resource.
359
+
360
+ Raises:
361
+ ResourceError when no file could be found or the search is ambiguous.
362
+
363
+ """
364
+ # Try to match the special resource syntax `:/resource_id/` or `:/resource_id/*/`
365
+
366
+ if resource_locator.startswith(':'):
367
+
368
+ match = PATTERN.fullmatch(resource_locator)
369
+ resource_id = match[1]
370
+ filename = match[3]
371
+ try:
372
+ resource_locations = resources[resource_id]
373
+ except KeyError:
374
+ raise ResourceError(f"Resource not defined: {resource_id}")
375
+
376
+ # Match can be only three things
377
+ # - None in which case the file must be in the resource location directly
378
+ # - '*/' in which case the file must be in a sub-folder of the resource
379
+ # - '**/' to find the file in any sub-folder below the given resource
380
+
381
+ if match[2] is None:
382
+
383
+ # This will return the first occurrence of the filename
384
+
385
+ for resource_location in resource_locations:
386
+ if contains_wildcard(filename):
387
+ files = list(find_files(filename, root=resource_location))
388
+
389
+ if len(files) == 1:
390
+ filename = files[0]
391
+ elif len(files) == 0:
392
+ raise NoSuchFileError(f"No file found that matches {filename=} for the given "
393
+ f"resource '{resource_id}'.")
394
+ else:
395
+ raise AmbiguityError(f"The {filename=} found {len(files)} matches for "
396
+ f"the given resource '{resource_id}'.")
397
+
398
+ return check_if_file_exists(resource_location / filename, resource_id)
399
+
400
+ elif match[2] == "*/":
401
+ # This will return the first occurrence of the filename
402
+
403
+ for resource_location in resource_locations:
404
+
405
+ files = list(find_files(filename, root=resource_location))
406
+
407
+ if len(files) == 1:
408
+ return files[0]
409
+ elif len(files) == 0:
410
+ raise NoSuchFileError(
411
+ f"The {filename=} could not be found for the given resource '{resource_id}'."
412
+ )
413
+ else:
414
+ raise AmbiguityError(f"The {filename=} was found {len(files)} times for "
415
+ f"the given resource location '{resource_location}'.")
416
+ elif match[2] == '**/':
417
+ raise NotImplementedError("The '**' to walk the tree is not yet implemented.")
418
+ else:
419
+ raise InternalError(
420
+ f"This shouldn't happen, the match is {match[2]=} for {resource_locator=}")
421
+ else:
422
+ return check_if_file_exists(Path(resource_locator))
423
+
424
+
425
+ # Now initialise the resources, this will
426
+ #
427
+ # * Add resource locations in this project (cgse-core) for the default resource ids
428
+ # * Add any other resource locations for the rentry points 'cgse.resource'
429
+
430
+ initialise_resources()
431
+
432
+
433
+ if __name__ == "__main__":
434
+
435
+ import rich
436
+
437
+ rich.print("Default resources:")
438
+ rich.print(get_resource_locations())
egse/services.py ADDED
@@ -0,0 +1,212 @@
1
+ """
2
+ This module provides the services to the control servers.
3
+
4
+ Each control server has a services protocol which provides commands that will
5
+ be executed on the control server instead of the device controller. This is
6
+ typically used to access control server specific settings like monitoring frequency,
7
+ logging levels, or to quit the control server in a controlled way.
8
+
9
+ """
10
+
11
+ import inspect
12
+ import logging
13
+
14
+ from egse.command import ClientServerCommand
15
+ from egse.control import ControlServer
16
+ from egse.decorators import dynamic_interface
17
+ from egse.protocol import CommandProtocol
18
+ from egse.proxy import Proxy
19
+ from egse.settings import Settings
20
+ from egse.zmq_ser import bind_address
21
+ from egse.zmq_ser import connect_address
22
+
23
+ LOGGER = logging.getLogger(__name__)
24
+
25
+ SERVICE_SETTINGS = Settings.load(filename="services.yaml")
26
+
27
+
28
+ class ServiceCommand(ClientServerCommand):
29
+ pass
30
+
31
+
32
+ class ServiceProtocol(CommandProtocol):
33
+ def __init__(self, control_server: ControlServer):
34
+ super().__init__()
35
+ self.control_server = control_server
36
+
37
+ self.load_commands(SERVICE_SETTINGS.Commands, ServiceCommand, ServiceProtocol)
38
+
39
+ def get_bind_address(self):
40
+ return bind_address(self.control_server.get_communication_protocol(), self.control_server.get_service_port())
41
+
42
+ def get_status(self):
43
+ return super().get_status()
44
+
45
+ def handle_set_monitoring_frequency(self, freq: float):
46
+ """
47
+ Sets the monitoring frequency (Hz) to the given freq value. This is only approximate since the frequency is
48
+ converted into a delay time and the actual execution of the status function is subject to the load on the
49
+ server and the overhead of the timing.
50
+
51
+ Args:
52
+ freq: frequency of execution (Hz)
53
+
54
+ Returns:
55
+ Sends back the selected delay time in milliseconds.
56
+ """
57
+ delay = self.control_server.set_delay(1.0 / freq)
58
+
59
+ LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {delay:.0f}ms.")
60
+
61
+ self.send(delay)
62
+
63
+ def handle_set_hk_frequency(self, freq: float):
64
+ """
65
+ Sets the housekeeping frequency (Hz) to the given freq value. This is only approximate since the frequency is
66
+ converted into a delay time and the actual execution of the `housekeeping` function is subject to the load on
67
+ the server and the overhead of the timing.
68
+
69
+ Args:
70
+ freq: frequency of execution (Hz)
71
+
72
+ Returns:
73
+ Sends back the selected delay time in milliseconds.
74
+ """
75
+ delay = self.control_server.set_hk_delay(1.0 / freq)
76
+
77
+ LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {delay:.0f}ms.")
78
+
79
+ self.send(delay)
80
+
81
+ def handle_set_logging_level(self, *args, **kwargs):
82
+ """
83
+ Set the logging level for the logger with the given name.
84
+
85
+ When 'all' is given for the name of the logger, the level of all loggers for which the name
86
+ starts with 'egse' will be changed to `level`.
87
+
88
+ Args:
89
+ name (str): the name of an existing Logger
90
+ level (int): the logging level
91
+
92
+ Returns:
93
+ Sends back an info message on what level was set.
94
+ """
95
+ if args:
96
+ name = args[0]
97
+ level = int(args[1])
98
+ else:
99
+ name = kwargs['name']
100
+ level = int(kwargs['level'])
101
+
102
+ if name == 'all':
103
+ for logger in [logging.getLogger(logger_name)
104
+ for logger_name in logging.root.manager.loggerDict
105
+ if logger_name.startswith('egse')]:
106
+ logger.setLevel(level)
107
+ msg = f"Logging level set to {level} for ALL 'egse' loggers"
108
+ elif name in logging.root.manager.loggerDict:
109
+ logger = logging.getLogger(name)
110
+ logger.setLevel(level)
111
+ msg = f"Logging level for {name} set to {level}."
112
+ else:
113
+ msg = f"Logger with name '{name}' doesn't exist at the server side."
114
+
115
+ # self.control_server.set_logging_level(level)
116
+ logging.debug(msg)
117
+ self.send(msg)
118
+
119
+ def handle_quit(self):
120
+ LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.")
121
+ self.control_server.quit()
122
+ self.send(f"Sent interrupt to {self.control_server.__class__.__name__}.")
123
+
124
+ def handle_get_process_status(self):
125
+ LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.")
126
+ self.send(self.get_status())
127
+
128
+ def handle_get_cs_module(self):
129
+ """
130
+ Returns the module in which the control server has been implemented.
131
+ """
132
+ LOGGER.debug(f"Asking for module of {self.control_server.__class__.__name__}.")
133
+ self.send(inspect.getmodule(self.control_server).__spec__.name)
134
+
135
+ def handle_get_average_execution_times(self):
136
+ LOGGER.debug(f"Asking for average execution times of {self.control_server.__class__.__name__} functions.")
137
+ self.send(self.control_server.get_average_execution_times())
138
+
139
+ def handle_get_storage_mnemonic(self):
140
+ LOGGER.debug(f"Asking for the storage menmonic of {self.control_server.__class__.__name__}.")
141
+ self.send(self.control_server.get_storage_mnemonic())
142
+
143
+ class ServiceInterface:
144
+ @dynamic_interface
145
+ def set_monitoring_frequency(self, freq: float):
146
+ ...
147
+ @dynamic_interface
148
+ def set_hk_frequency(self, freq: float):
149
+ ...
150
+ @dynamic_interface
151
+ def set_logging_level(self, name: str, level: int):
152
+ ...
153
+ @dynamic_interface
154
+ def quit_server(self):
155
+ ...
156
+ @dynamic_interface
157
+ def get_process_status(self):
158
+ ...
159
+ @dynamic_interface
160
+ def get_cs_module(self):
161
+ ...
162
+ @dynamic_interface
163
+ def get_average_execution_times(self):
164
+ ...
165
+ @dynamic_interface
166
+ def get_storage_mnemonic(self):
167
+ ...
168
+
169
+ class ServiceProxy(Proxy, ServiceInterface):
170
+ """
171
+ A ServiceProxy is a simple class that forwards service commands to a control server.
172
+ """
173
+
174
+ def __init__(self, ctrl_settings=None, *, protocol=None, hostname=None, port=None):
175
+ """
176
+ A ServiceProxy can be configured from the specific control server settings, or additional
177
+ arguments `protocol`, `hostname` and `port` can be passed.
178
+
179
+ The additional arguments always overwrite the values loaded from ctrl_settings. Either ctrl_settings or
180
+ hostname and port must be provided, protocol is optional and defaults to 'tcp'.
181
+
182
+ Args:
183
+ ctrl_settings: an AttributeDict with HOSTNAME, PORT and PROTOCOL attributes
184
+ protocol: the transport protocol [default: tcp]
185
+ hostname: the IP addrress of the control server
186
+ port: the port on which the control server is listening for service commands
187
+ """
188
+ _protocol = _hostname = _port = None
189
+ if ctrl_settings:
190
+ _protocol = ctrl_settings.PROTOCOL
191
+ _hostname = ctrl_settings.HOSTNAME
192
+ _port = ctrl_settings.SERVICE_PORT
193
+
194
+ # the protocol argument is overwriting the standard crtl_settings
195
+
196
+ if protocol:
197
+ _protocol = protocol
198
+
199
+ # if still _protocol is not set, neither by ctrl_settings, nor by the protocol argument, use a default
200
+
201
+ if _protocol is None:
202
+ _protocol = 'tcp'
203
+
204
+ if hostname:
205
+ _hostname = hostname
206
+ if port:
207
+ _port = port
208
+
209
+ if _hostname is None or _port is None:
210
+ raise ValueError("Expected ctrl-settings or hostname and port as arguments")
211
+
212
+ super().__init__(connect_address(_protocol, _hostname, _port))
egse/services.yaml ADDED
@@ -0,0 +1,51 @@
1
+
2
+ ProxyClass:
3
+ egse.services.ServiceProxy
4
+
5
+ ClassDescription:
6
+ The Service Proxy class is used to send control commands to any of the control servers.
7
+
8
+ Commands:
9
+
10
+ set_monitoring_frequency:
11
+ description: Sets the monitoring frequency (Hz) to the given freq value.
12
+ cmd: '{freq}'
13
+ device_method: None
14
+ response: handle_set_monitoring_frequency
15
+
16
+ set_hk_frequency:
17
+ description: Sets the housekeeping frequency (Hz) to the given freq value.
18
+ cmd: '{freq}'
19
+ device_method: None
20
+ response: handle_set_hk_frequency
21
+
22
+ set_logging_level:
23
+ description: Set the logging level for the logger with the given name.
24
+ cmd: '{name} {level}'
25
+ device_method: None
26
+ response: handle_set_logging_level
27
+
28
+ quit_server:
29
+ description: Send an interrupt to the control server. The server will close all connections and exit.
30
+ device_method: None
31
+ response: handle_quit
32
+
33
+ get_process_status:
34
+ description: Ask for the process status of the control server.
35
+ device_method: None
36
+ response: handle_get_process_status
37
+
38
+ get_cs_module:
39
+ description: Returns the module in which the control server has been implemented.
40
+ device_method: None
41
+ response: handle_get_cs_module
42
+
43
+ get_average_execution_times:
44
+ description: Returns a dictionary with the average execution times of the get_housekeeping and get_status methods
45
+ device_method: None
46
+ response: handle_get_average_execution_times
47
+
48
+ get_storage_mnemonic:
49
+ description: Returns the mnemonic that is part of the filename where the housekeeping data are stored.
50
+ device_method: None
51
+ response: handle_get_storage_mnemonic