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.
- cgse_common-2024.1.1.dist-info/METADATA +64 -0
- cgse_common-2024.1.1.dist-info/RECORD +32 -0
- cgse_common-2024.1.1.dist-info/WHEEL +4 -0
- cgse_common-2024.1.1.dist-info/entry_points.txt +2 -0
- egse/bits.py +318 -0
- egse/command.py +699 -0
- egse/config.py +289 -0
- egse/control.py +429 -0
- egse/decorators.py +419 -0
- egse/device.py +269 -0
- egse/env.py +279 -0
- egse/exceptions.py +88 -0
- egse/mixin.py +464 -0
- egse/monitoring.py +96 -0
- egse/observer.py +41 -0
- egse/obsid.py +161 -0
- egse/persistence.py +58 -0
- egse/plugin.py +97 -0
- egse/process.py +460 -0
- egse/protocol.py +607 -0
- egse/proxy.py +522 -0
- egse/reload.py +122 -0
- egse/resource.py +438 -0
- egse/services.py +212 -0
- egse/services.yaml +51 -0
- egse/settings.py +379 -0
- egse/settings.yaml +981 -0
- egse/setup.py +1180 -0
- egse/state.py +173 -0
- egse/system.py +1499 -0
- egse/version.py +178 -0
- egse/zmq_ser.py +69 -0
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
|