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/command.py
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines a number of classes and helper functions to define and work
|
|
3
|
+
with commands that operate hardware devices. The goal is to be able to define /
|
|
4
|
+
create commands transparently from a YAML file without having to write (too much)
|
|
5
|
+
code.
|
|
6
|
+
|
|
7
|
+
A few definitions for the classes defined in this module:
|
|
8
|
+
|
|
9
|
+
**command**
|
|
10
|
+
|
|
11
|
+
a string that is sent to a device over an interface like TCP/IP or USB. This
|
|
12
|
+
string is generated by the get_cmd_string() method of the Command class.
|
|
13
|
+
|
|
14
|
+
The string contains format like syntax that looks like an f-string, but is
|
|
15
|
+
interpreted differently. See further: How to format device command strings.
|
|
16
|
+
|
|
17
|
+
**Command**
|
|
18
|
+
|
|
19
|
+
the base class for commands. This class contains the definition of the command
|
|
20
|
+
and provides methods to parse and check arguments. The Command can be 'called'
|
|
21
|
+
or 'executed' in which case a number of actions are performed based on the
|
|
22
|
+
provided arguments.
|
|
23
|
+
|
|
24
|
+
**CommandExecution**
|
|
25
|
+
|
|
26
|
+
this class contains all the information needed to execute a command, without
|
|
27
|
+
actually executing it. A CommandExecution contains the command definition and
|
|
28
|
+
the parameters for the execution. It is mainly served as a communication
|
|
29
|
+
mechanism to the control servers, i.e. the client side (Proxy) defines a
|
|
30
|
+
command execution and the server then executes the command.
|
|
31
|
+
|
|
32
|
+
**CommandError**
|
|
33
|
+
|
|
34
|
+
a catch-all exception for unrecoverable errors in this module
|
|
35
|
+
|
|
36
|
+
**InvalidArgumentsError**
|
|
37
|
+
|
|
38
|
+
a CommandError raised when the arguments provided are themselve invalid
|
|
39
|
+
or if the number of arguments is not matching expectations
|
|
40
|
+
|
|
41
|
+
The basic interface is:
|
|
42
|
+
|
|
43
|
+
cmd = Command(name = <command name>,
|
|
44
|
+
cmd = <command string>,
|
|
45
|
+
response = <callable to retreive a response>,
|
|
46
|
+
wait = <callable to wait a specific time/delay>)
|
|
47
|
+
|
|
48
|
+
where:
|
|
49
|
+
|
|
50
|
+
* name: a name for the command, this is just needed for reporting, not used in commanding
|
|
51
|
+
* cmd: the command string to send or execute, see further for details
|
|
52
|
+
* response: send a second command to read or get a response on the 'cmd' sent
|
|
53
|
+
* wait: a function object that will wait for a specific duration,
|
|
54
|
+
e.g. ```partial(time.sleep, 10)```
|
|
55
|
+
|
|
56
|
+
How to format device command strings
|
|
57
|
+
|
|
58
|
+
The ``cmd`` argument is a string that contains placeholders (replacement fields)
|
|
59
|
+
for future arguments that will be passed when calling the Command. The replacement
|
|
60
|
+
fields are marked with curly braces and are mandatory. When a name is provided
|
|
61
|
+
in the curly braces, the argument shall be provided as a keyword argument, otherwise
|
|
62
|
+
a positional argument is expected. In the current implementation the ``cmd``
|
|
63
|
+
can only contain either positional arguments or keyword argument, not a mix of both.
|
|
64
|
+
|
|
65
|
+
The replacement fields may also have a format specifier to specify a precise format
|
|
66
|
+
for that field.
|
|
67
|
+
|
|
68
|
+
**Examples**
|
|
69
|
+
|
|
70
|
+
moveAbsolute = Command(
|
|
71
|
+
name = "moveAbsolute",
|
|
72
|
+
cmd = "&2 Q70=0 Q71={tx:.6f} Q72={ty:.6f} Q73={tz:.6f} "
|
|
73
|
+
"Q74={rx:.6f} Q75={ry:.6f} Q76={rz:.6f} Q20=11"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
response = moveAbsolute(1, 1, 1, 0, 0, 20)
|
|
77
|
+
response = moveAbsolute(tx=1, ty=1, tz=1, rx=0, ry=0, rz=20)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Questions
|
|
81
|
+
|
|
82
|
+
Do we need additional hooks into this commanding?
|
|
83
|
+
|
|
84
|
+
* add a meaning to the check, what is it and what is it used for?
|
|
85
|
+
* add a output processor possibility. A callback function that will process the
|
|
86
|
+
output value before returning it by the __call__.
|
|
87
|
+
* provide an execute method for the CommandExecution that executes the command
|
|
88
|
+
with the saved parameters
|
|
89
|
+
"""
|
|
90
|
+
import functools
|
|
91
|
+
import inspect
|
|
92
|
+
import logging
|
|
93
|
+
import re
|
|
94
|
+
from collections import namedtuple
|
|
95
|
+
from typing import Callable
|
|
96
|
+
|
|
97
|
+
from egse.control import Success
|
|
98
|
+
from egse.exceptions import Error
|
|
99
|
+
|
|
100
|
+
logger = logging.getLogger(__name__)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def stringify_function_call(function_info: dict) -> str:
|
|
104
|
+
def quote(value):
|
|
105
|
+
return f'"{value}"' if isinstance(value, str) else value
|
|
106
|
+
|
|
107
|
+
description = function_info.get("description")
|
|
108
|
+
if description:
|
|
109
|
+
return description
|
|
110
|
+
|
|
111
|
+
result = ""
|
|
112
|
+
|
|
113
|
+
name = function_info.get("func_name")
|
|
114
|
+
args = function_info.get("args")
|
|
115
|
+
kwargs = function_info.get("kwargs")
|
|
116
|
+
|
|
117
|
+
if name:
|
|
118
|
+
result += name
|
|
119
|
+
else:
|
|
120
|
+
result += "unknown_function"
|
|
121
|
+
|
|
122
|
+
result += "("
|
|
123
|
+
|
|
124
|
+
if args:
|
|
125
|
+
result += f"{args}"[1:-1]
|
|
126
|
+
|
|
127
|
+
if kwargs:
|
|
128
|
+
result += ", " if args else ""
|
|
129
|
+
result += ", ".join([f"{k}={quote(v)}" for k, v in kwargs.items()])
|
|
130
|
+
|
|
131
|
+
result += ")"
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def dry_run(func: Callable) -> Callable:
|
|
137
|
+
"""This decorator prepares the function to handle a dry run.
|
|
138
|
+
|
|
139
|
+
A dry run is used to check the logic of an instrument commanding script without
|
|
140
|
+
actually executing the instrument commands. The commands are instead added to the
|
|
141
|
+
command sequence in the global state.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
func: the function that needs to be executed
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A wrapper around the given function.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
@functools.wraps(func)
|
|
151
|
+
def func_wrapper(self, *args, **kwargs):
|
|
152
|
+
|
|
153
|
+
from egse.state import GlobalState # prevent circular import
|
|
154
|
+
|
|
155
|
+
if GlobalState.dry_run:
|
|
156
|
+
if callable(func) and func.__name__ == "client_call":
|
|
157
|
+
# This client_call function takes an additional argument which is the Proxy.
|
|
158
|
+
# the Proxy is not part of the CommandExecution signature and shall be removed
|
|
159
|
+
# FIXME: do we introduce a memory leak here by adding 'self' to this GlobalState?
|
|
160
|
+
args = args[1:]
|
|
161
|
+
try:
|
|
162
|
+
self.validate_arguments(*args, **kwargs)
|
|
163
|
+
except CommandError as e_ce:
|
|
164
|
+
GlobalState.add_command(InvalidCommandExecution(e_ce, self, *args, **kwargs))
|
|
165
|
+
else:
|
|
166
|
+
GlobalState.add_command(CommandExecution(self, *args, **kwargs))
|
|
167
|
+
else:
|
|
168
|
+
FunctionExecution = namedtuple("FunctionExecution", ["name", "args", "kwargs"])
|
|
169
|
+
GlobalState.add_command(FunctionExecution(func.__name__, args, kwargs))
|
|
170
|
+
return Success(
|
|
171
|
+
"Command execution appended to command sequence, function not executed in dry_run."
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
return func(self, *args, **kwargs)
|
|
175
|
+
|
|
176
|
+
return func_wrapper
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def parse_format_string(fstring):
|
|
180
|
+
"""
|
|
181
|
+
Parse and decode the format string.
|
|
182
|
+
"""
|
|
183
|
+
# Remove occurrences of {{ }} from the fstring as they are not replacement fields
|
|
184
|
+
# and will occur after formatting as single braces.
|
|
185
|
+
|
|
186
|
+
fstring = re.sub(r"{{.*}}", lambda x: "_", fstring)
|
|
187
|
+
|
|
188
|
+
# logger.debug(f"fstring = '{fstring}', replaced {{{{.*}}}} with _")
|
|
189
|
+
|
|
190
|
+
parts = re.findall(r"\{(.*?)\}", fstring)
|
|
191
|
+
|
|
192
|
+
# logger.debug(f"Parts: {parts!r}, n={len(parts)}")
|
|
193
|
+
|
|
194
|
+
tot_n_args = len(parts)
|
|
195
|
+
n_args = 0
|
|
196
|
+
n_kwargs = 0
|
|
197
|
+
keys = []
|
|
198
|
+
|
|
199
|
+
for part in parts:
|
|
200
|
+
result = re.split(r"(:)", part)
|
|
201
|
+
if result[0] == "":
|
|
202
|
+
n_args += 1
|
|
203
|
+
else:
|
|
204
|
+
n_kwargs += 1
|
|
205
|
+
keys.append(result[0])
|
|
206
|
+
|
|
207
|
+
# If this assertion fails, there is a flaw in the algorithm above
|
|
208
|
+
assert tot_n_args == n_args + n_kwargs, (
|
|
209
|
+
f"Total number of arguments ({tot_n_args}) doesn't match # args ({n_args}) + "
|
|
210
|
+
f"# kwargs ({n_kwargs})."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if n_args > 0 and n_kwargs > 0:
|
|
214
|
+
raise InvalidArgumentsError("Mixing of positional and keyword arguments is not available.")
|
|
215
|
+
|
|
216
|
+
return tot_n_args, n_args, n_kwargs, keys
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class CommandError(Error):
|
|
220
|
+
"""A Command Exception as a base class for this module."""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class InvalidArgumentsError(CommandError):
|
|
224
|
+
"""The arguments provided are invalid"""
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class CommandExecution:
|
|
228
|
+
"""
|
|
229
|
+
This class contains all the information that is needed to execute a command
|
|
230
|
+
with a set of parameters/arguments. The command is however not executed
|
|
231
|
+
automatically. That is the responsibility of the caller to actually execute
|
|
232
|
+
the command with the given parameters.
|
|
233
|
+
|
|
234
|
+
Developer info
|
|
235
|
+
|
|
236
|
+
you can see this as a partial (functools) which defines the command and
|
|
237
|
+
its arguments, but doesn't execute until explicitly called. You can execute
|
|
238
|
+
the command by calling the `cmd` with the given arguments:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
ce = CommandExecution(cmd, 20.0)
|
|
242
|
+
...
|
|
243
|
+
response = ce.run()
|
|
244
|
+
```
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def __init__(self, cmd, *args, **kwargs):
|
|
248
|
+
self._cmd = cmd
|
|
249
|
+
self._args = args
|
|
250
|
+
self._kwargs = kwargs
|
|
251
|
+
|
|
252
|
+
def get_name(self):
|
|
253
|
+
return self._cmd.get_name()
|
|
254
|
+
|
|
255
|
+
def get_cmd(self):
|
|
256
|
+
return self._cmd
|
|
257
|
+
|
|
258
|
+
def get_args(self):
|
|
259
|
+
return self._args
|
|
260
|
+
|
|
261
|
+
def get_kwargs(self):
|
|
262
|
+
return self._kwargs
|
|
263
|
+
|
|
264
|
+
def run(self):
|
|
265
|
+
return self._cmd(*self._args, **self._kwargs)
|
|
266
|
+
|
|
267
|
+
def __str__(self):
|
|
268
|
+
msg = f"[{self.get_cmd().__class__.__name__}] {self.get_name()}("
|
|
269
|
+
for arg in self.get_args():
|
|
270
|
+
msg += f"{arg}, "
|
|
271
|
+
for k, v in self.get_kwargs().items():
|
|
272
|
+
msg += f"{k}={v}, "
|
|
273
|
+
if msg.endswith(", "):
|
|
274
|
+
msg = msg[:-2]
|
|
275
|
+
msg += ")"
|
|
276
|
+
return msg
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class InvalidCommandExecution(CommandExecution):
|
|
280
|
+
"""A invalid command execution."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, exc, cmd, *args, **kwargs):
|
|
283
|
+
"""
|
|
284
|
+
Args:
|
|
285
|
+
exc: the Exception that was raised and describes the problem
|
|
286
|
+
cmd: the Command object
|
|
287
|
+
*args: the positional arguments that were given
|
|
288
|
+
**kwargs: the keyword arguments that were given
|
|
289
|
+
"""
|
|
290
|
+
super().__init__(cmd, *args, **kwargs)
|
|
291
|
+
self._exc = exc
|
|
292
|
+
|
|
293
|
+
def run(self):
|
|
294
|
+
raise InvalidArgumentsError(
|
|
295
|
+
f"The command {self.get_name()} can not be executed. Reason: {self._exc}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def __str__(self):
|
|
299
|
+
msg = super().__str__()
|
|
300
|
+
msg += f" [ERROR: {self._exc}]"
|
|
301
|
+
return msg
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class WaitCommand:
|
|
305
|
+
def __init__(self, command, condition):
|
|
306
|
+
self._command = command
|
|
307
|
+
self._condition = condition
|
|
308
|
+
|
|
309
|
+
def __call__(self):
|
|
310
|
+
|
|
311
|
+
# .. todo:: do we need a timeout possibility here?
|
|
312
|
+
|
|
313
|
+
while True:
|
|
314
|
+
return_code = self._command()
|
|
315
|
+
if self._condition(return_code):
|
|
316
|
+
break
|
|
317
|
+
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class Command:
|
|
322
|
+
"""
|
|
323
|
+
A Command is basically a string that is send to a device and for which the
|
|
324
|
+
device returns a response.
|
|
325
|
+
|
|
326
|
+
The command string can contain placeholders that will be filled when the
|
|
327
|
+
command is 'called'.
|
|
328
|
+
|
|
329
|
+
The arguments that are given will be filled into the formatted string.
|
|
330
|
+
Arguments can be positional or keyword arguments, not both.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(
|
|
334
|
+
self, name, cmd, response=None, wait=None, check=None, description=None,
|
|
335
|
+
device_method=None
|
|
336
|
+
):
|
|
337
|
+
self._name = name
|
|
338
|
+
self._cmd = cmd
|
|
339
|
+
self._response = response
|
|
340
|
+
self._wait = wait
|
|
341
|
+
self._check = check
|
|
342
|
+
self._description = description
|
|
343
|
+
self._device_method = device_method
|
|
344
|
+
|
|
345
|
+
tot_n_args, n_args, n_kwargs, keys = parse_format_string(cmd)
|
|
346
|
+
|
|
347
|
+
self._tot_n_args = tot_n_args
|
|
348
|
+
self._n_args = n_args
|
|
349
|
+
self._n_kwargs = n_kwargs
|
|
350
|
+
self._keys = keys
|
|
351
|
+
|
|
352
|
+
self.__doc__ = self.doc_string()
|
|
353
|
+
|
|
354
|
+
def doc_string(self):
|
|
355
|
+
msg = f"usage: {self._name}(nargs={self._tot_n_args}, keys={self._keys})\n"
|
|
356
|
+
msg += " args & kwargs can be mixed (limited)"
|
|
357
|
+
|
|
358
|
+
if self._description is not None:
|
|
359
|
+
msg += "\n"
|
|
360
|
+
msg += f"{self._description}\n"
|
|
361
|
+
|
|
362
|
+
return msg
|
|
363
|
+
|
|
364
|
+
def validate_arguments(self, *args, **kwargs):
|
|
365
|
+
|
|
366
|
+
# Special case for commands with *args or **kwargs, we don't validate
|
|
367
|
+
|
|
368
|
+
if self._cmd in ("*", "**"):
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
nargs = len(args)
|
|
372
|
+
nkwargs = len(kwargs)
|
|
373
|
+
|
|
374
|
+
if self._tot_n_args != nargs + nkwargs:
|
|
375
|
+
raise InvalidArgumentsError(
|
|
376
|
+
f"Expected {self._tot_n_args} arguments for command {self._name}, "
|
|
377
|
+
f"got {nargs + nkwargs} arguments."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if self._tot_n_args == 0:
|
|
381
|
+
pass
|
|
382
|
+
elif nargs and nargs == self._n_args:
|
|
383
|
+
pass
|
|
384
|
+
elif nkwargs and nkwargs == self._n_kwargs:
|
|
385
|
+
pass
|
|
386
|
+
elif nargs == self._n_kwargs and nkwargs == 0:
|
|
387
|
+
pass
|
|
388
|
+
else:
|
|
389
|
+
raise InvalidArgumentsError(
|
|
390
|
+
f"Expected {self._n_args} positional arguments and {self._n_kwargs} "
|
|
391
|
+
f"keyword arguments for command {self._name}, got {nargs} positional "
|
|
392
|
+
f"and {nkwargs} keyword arguments instead."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def execute(self, cmd):
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
def get_name(self):
|
|
399
|
+
return self._name
|
|
400
|
+
|
|
401
|
+
def needs_argument(self, name):
|
|
402
|
+
if name in self._keys:
|
|
403
|
+
return True
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
def __repr__(self):
|
|
407
|
+
name = self._name if hasattr(self, "_name") else None
|
|
408
|
+
return f"<{self.__class__.__name__}({name})>"
|
|
409
|
+
|
|
410
|
+
def get_device_method(self):
|
|
411
|
+
return self._device_method
|
|
412
|
+
|
|
413
|
+
def get_device_method_name(self):
|
|
414
|
+
return self._device_method.__name__
|
|
415
|
+
|
|
416
|
+
def get_command_execution(self, *args, **kwargs):
|
|
417
|
+
|
|
418
|
+
return CommandExecution(self, *args, **kwargs)
|
|
419
|
+
|
|
420
|
+
def __call__(self, *args, **kwargs):
|
|
421
|
+
cmd_string = self.get_cmd_string(*args, **kwargs)
|
|
422
|
+
|
|
423
|
+
# Now execute the cmd_string
|
|
424
|
+
|
|
425
|
+
response = self.execute(cmd_string)
|
|
426
|
+
|
|
427
|
+
if self._wait is not None:
|
|
428
|
+
self._wait()
|
|
429
|
+
|
|
430
|
+
if self._response is not None:
|
|
431
|
+
response = self._response()
|
|
432
|
+
|
|
433
|
+
if self._check is not None:
|
|
434
|
+
response = self._check(response)
|
|
435
|
+
|
|
436
|
+
return response
|
|
437
|
+
|
|
438
|
+
def get_raw_cmd_string(self):
|
|
439
|
+
return self._cmd
|
|
440
|
+
|
|
441
|
+
def get_cmd_string(self, *args, **kwargs):
|
|
442
|
+
nargs = len(args)
|
|
443
|
+
nkwargs = len(kwargs)
|
|
444
|
+
|
|
445
|
+
if self._tot_n_args != nargs + nkwargs:
|
|
446
|
+
raise CommandError(
|
|
447
|
+
f"Expected {self._tot_n_args} arguments for command {self._name}, "
|
|
448
|
+
f"got {nargs + nkwargs} arguments."
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if self._tot_n_args == 0:
|
|
452
|
+
cmd_string = self._cmd or self._name
|
|
453
|
+
elif nargs and nargs == self._n_args:
|
|
454
|
+
cmd_string = self._create_command_string_from_args(*args)
|
|
455
|
+
elif nkwargs and nkwargs == self._n_kwargs:
|
|
456
|
+
cmd_string = self._create_command_string_from_kwargs(**kwargs)
|
|
457
|
+
elif nargs == self._n_kwargs and nkwargs == 0:
|
|
458
|
+
cmd_string = self._create_command_string_from_args_with_kw(*args)
|
|
459
|
+
else:
|
|
460
|
+
raise CommandError(
|
|
461
|
+
f"Expected {self._n_args} positional arguments and {self._n_kwargs} "
|
|
462
|
+
f"keyword arguments for command {self._name}, got {nargs} positional "
|
|
463
|
+
f"and {nkwargs} keyword arguments instead."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
return cmd_string
|
|
467
|
+
|
|
468
|
+
def _create_command_string_from_args(self, *args):
|
|
469
|
+
full_command = self._cmd.format(*args)
|
|
470
|
+
return full_command
|
|
471
|
+
|
|
472
|
+
def _create_command_string_from_args_with_kw(self, *args):
|
|
473
|
+
full_command = self._cmd.format(**{k: v for k, v in zip(self._keys, args)})
|
|
474
|
+
return full_command
|
|
475
|
+
|
|
476
|
+
def _create_command_string_from_kwargs(self, **kwargs):
|
|
477
|
+
full_command = self._cmd.format(**kwargs)
|
|
478
|
+
return full_command
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class ClientServerCommand(Command):
|
|
482
|
+
@dry_run
|
|
483
|
+
def client_call(self, other, *args, **kwargs):
|
|
484
|
+
"""
|
|
485
|
+
This method is called at the client side. It is used by the Proxy
|
|
486
|
+
as a generic command to send a command execution to the server.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
other: a sub-class of the Proxy class
|
|
490
|
+
args: arguments that will be passed on to this command when executed
|
|
491
|
+
kwargs: keyword arguments that will be passed on to this command when executed
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
the response that is returned by calling the command (at the server side).
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
self.validate_arguments(*args, **kwargs)
|
|
499
|
+
except CommandError as e_ce:
|
|
500
|
+
logger.error(str(e_ce))
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
ce = CommandExecution(self, *args, **kwargs)
|
|
504
|
+
rc = other.send(ce)
|
|
505
|
+
|
|
506
|
+
# FIXME:
|
|
507
|
+
# not sure if I should do this, the Success/Failure returns from the CS should be
|
|
508
|
+
# re-designed. I have put this here for the following reason: when requesting an obsid
|
|
509
|
+
# from the CM_CS we don't want a Success message back, but we need the number!
|
|
510
|
+
|
|
511
|
+
# if isinstance(rc, Success):
|
|
512
|
+
# logger.info(rc)
|
|
513
|
+
# rc = rc.return_code
|
|
514
|
+
|
|
515
|
+
return rc
|
|
516
|
+
|
|
517
|
+
def server_call(self, other, *args, **kwargs):
|
|
518
|
+
"""
|
|
519
|
+
This method is called at the server side. It is used by the CommandProtocol class in the
|
|
520
|
+
``execute`` method.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
other: a sub-class of the CommandProtocol
|
|
524
|
+
args: arguments are passed on to the response method
|
|
525
|
+
kwargs: keyword arguments are passed on to the response method
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
0 on success and -1 on failure.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
if self._response is None:
|
|
532
|
+
logger.warning(f"No response defined for {other} command {self.get_name()}")
|
|
533
|
+
return -1
|
|
534
|
+
|
|
535
|
+
# Note that `_response` is of type 'function' because it was loaded from
|
|
536
|
+
# a class object and therefore not bound to a class instance. The reason
|
|
537
|
+
# that we could not use a bound method for `_response` is that we pass the
|
|
538
|
+
# command objects back and forth from the control server to the proxy and
|
|
539
|
+
# the class instances are not known at the other side.
|
|
540
|
+
|
|
541
|
+
if self._response.__name__ == "handle_device_method":
|
|
542
|
+
|
|
543
|
+
# call the handle_device_method of the Protocol sub-class
|
|
544
|
+
|
|
545
|
+
logger.log(0,
|
|
546
|
+
f"Executing Command {self._response.__name__}({other!r}, "
|
|
547
|
+
f"{self!r}, {args}, {kwargs})")
|
|
548
|
+
|
|
549
|
+
rc = self._response(other, self, *args, **kwargs)
|
|
550
|
+
else:
|
|
551
|
+
logger.log(0,
|
|
552
|
+
f"Executing Command {self._response.__name__}({other!r}, {args}, {kwargs})")
|
|
553
|
+
|
|
554
|
+
rc = self._response(other, *args, **kwargs)
|
|
555
|
+
|
|
556
|
+
logger.log(0, f"Response is {rc}.")
|
|
557
|
+
|
|
558
|
+
return 0
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def get_method(parent_obj, method_name: str):
|
|
562
|
+
"""
|
|
563
|
+
Returns a bound method from a given class instance.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
parent_obj: the class instance that provides the method
|
|
567
|
+
method_name: name of the method that is requested
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
the method [type: method].
|
|
571
|
+
|
|
572
|
+
.. note::
|
|
573
|
+
The method returned is an bound class instance method and therefore
|
|
574
|
+
this method *does not* expects as its first argument the class
|
|
575
|
+
instance, i.e. self, when you call this as a function.
|
|
576
|
+
|
|
577
|
+
"""
|
|
578
|
+
if method_name is None or method_name == "None":
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
if hasattr(parent_obj, method_name):
|
|
582
|
+
method = getattr(parent_obj, method_name)
|
|
583
|
+
if inspect.ismethod(method):
|
|
584
|
+
return method
|
|
585
|
+
logger.warning(f"{method_name} is not a method, type={type(method)}")
|
|
586
|
+
else:
|
|
587
|
+
logger.warning(f"{parent_obj!r} has no method called {method_name}")
|
|
588
|
+
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def get_function(parent_class, method_name: str):
|
|
593
|
+
"""
|
|
594
|
+
Returns a function (unbound method) from a given class.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
parent_class: the class that provides the method
|
|
598
|
+
method_name: name of the method that is requested
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
the method [type: function].
|
|
602
|
+
|
|
603
|
+
.. note::
|
|
604
|
+
The function returned is an unbound class instance method and
|
|
605
|
+
therefore this function expects as its first argument the class
|
|
606
|
+
instance, i.e. self, when you call it as a function.
|
|
607
|
+
|
|
608
|
+
"""
|
|
609
|
+
if method_name is None or method_name == "None":
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
if hasattr(parent_class, method_name):
|
|
613
|
+
func = getattr(parent_class, method_name)
|
|
614
|
+
if inspect.isfunction(func):
|
|
615
|
+
return func
|
|
616
|
+
logger.warning(f"{method_name} is not a function, type={type(func)}")
|
|
617
|
+
else:
|
|
618
|
+
logger.warning(
|
|
619
|
+
f"{parent_class.__module__}.{parent_class.__name__} has no method called {method_name}"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
return None
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def load_commands(protocol_class, command_settings, command_class, device_class):
|
|
626
|
+
"""
|
|
627
|
+
Loads the command definitions from the given ``command_settings`` and builds an internal
|
|
628
|
+
dictionary containing the command names as keys and the corresponding ``Command`` class objects
|
|
629
|
+
as values.
|
|
630
|
+
|
|
631
|
+
The ``command_settings`` is usually loaded from a YAML configuration file containing the
|
|
632
|
+
command definitions for the device.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
protocol_class: the CommandProtocol or a sub-class
|
|
636
|
+
command_settings: a dictionary containing the command definitions for this device
|
|
637
|
+
command_class: the type of command to create, a subclass of Command
|
|
638
|
+
device_class: the type of the base device class from which the methods are loaded
|
|
639
|
+
"""
|
|
640
|
+
commands = {}
|
|
641
|
+
|
|
642
|
+
for name in command_settings:
|
|
643
|
+
command_settings_name = command_settings[name]
|
|
644
|
+
if "cmd" in command_settings_name:
|
|
645
|
+
cmd = command_settings_name["cmd"]
|
|
646
|
+
else:
|
|
647
|
+
cmd = ""
|
|
648
|
+
|
|
649
|
+
if "description" in command_settings_name:
|
|
650
|
+
description = command_settings_name["description"]
|
|
651
|
+
else:
|
|
652
|
+
description = None
|
|
653
|
+
|
|
654
|
+
# The response field is the name of a function from the CommandProtocol class
|
|
655
|
+
# or a sub-class. This function shall send a response back to the client (Proxy). That's
|
|
656
|
+
# why this field is called response.
|
|
657
|
+
# By convention we like that this method name would start with `handle_` so that we can
|
|
658
|
+
# make a distinction between response commands and normal methods in Protocol. Remember
|
|
659
|
+
# that response methods should send a reply back to the client (which will be waiting for
|
|
660
|
+
# it..). If no response field is given, then the `handle_device_method` will be called.
|
|
661
|
+
|
|
662
|
+
if "response" in command_settings_name:
|
|
663
|
+
response_method = get_function(protocol_class, command_settings_name["response"])
|
|
664
|
+
else:
|
|
665
|
+
response_method = get_function(protocol_class, "handle_device_method")
|
|
666
|
+
response_method = None
|
|
667
|
+
|
|
668
|
+
# The device_method field is used in the `handle_device_method` to call the method on the
|
|
669
|
+
# device class. That is the class that implements the DeviceInterface and is usually called
|
|
670
|
+
# a Controller or a Simulator.
|
|
671
|
+
#
|
|
672
|
+
# If no device_name field is given, the name from the command_settings is used.
|
|
673
|
+
|
|
674
|
+
if "device_method" in command_settings_name:
|
|
675
|
+
device_method_name = command_settings_name["device_method"]
|
|
676
|
+
else:
|
|
677
|
+
device_method_name = name
|
|
678
|
+
|
|
679
|
+
# check if the device_method exists in the device base class
|
|
680
|
+
|
|
681
|
+
if device_method_name == "None":
|
|
682
|
+
device_method = None
|
|
683
|
+
else:
|
|
684
|
+
device_method = get_function(device_class, device_method_name)
|
|
685
|
+
|
|
686
|
+
logger.debug(
|
|
687
|
+
f"Creating {command_class.__module__}.{command_class.__name__}(name='{name}', "
|
|
688
|
+
f"cmd='{cmd}', response={response_method}, device_method={device_method})"
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
commands[name] = command_class(
|
|
692
|
+
name=name,
|
|
693
|
+
cmd=cmd,
|
|
694
|
+
response=response_method,
|
|
695
|
+
description=description,
|
|
696
|
+
device_method=device_method,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
return commands
|