motorcortex-python 1.0.0rc1__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.
- motorcortex/__init__.py +314 -0
- motorcortex/_connection_state.py +58 -0
- motorcortex/_request_builders.py +157 -0
- motorcortex/_request_utils.py +314 -0
- motorcortex/_subscribe_dispatch.py +90 -0
- motorcortex/exceptions.py +65 -0
- motorcortex/init_threads.py +103 -0
- motorcortex/message_types.py +387 -0
- motorcortex/motorcortex_hash.json +166 -0
- motorcortex/motorcortex_pb2.py +105 -0
- motorcortex/motorcortex_pb2.pyi +1961 -0
- motorcortex/nng_url.py +49 -0
- motorcortex/parameter_tree.py +86 -0
- motorcortex/py.typed +0 -0
- motorcortex/reply.py +108 -0
- motorcortex/request.py +668 -0
- motorcortex/session.py +194 -0
- motorcortex/setup_logger.py +10 -0
- motorcortex/state_callback_handler.py +92 -0
- motorcortex/subscribe.py +400 -0
- motorcortex/subscription.py +414 -0
- motorcortex/timespec.py +173 -0
- motorcortex/version.py +1 -0
- motorcortex_python-1.0.0rc1.dist-info/LICENSE +22 -0
- motorcortex_python-1.0.0rc1.dist-info/METADATA +171 -0
- motorcortex_python-1.0.0rc1.dist-info/RECORD +28 -0
- motorcortex_python-1.0.0rc1.dist-info/WHEEL +5 -0
- motorcortex_python-1.0.0rc1.dist-info/top_level.txt +1 -0
motorcortex/request.py
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Developer: Alexey Zakharov (alexey.zakharov@vectioneer.com)
|
|
5
|
+
# All rights reserved. Copyright (c) 2016-2026 VECTIONEER.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
motorcortex.request
|
|
10
|
+
|
|
11
|
+
Provides the Request class for managing request connections to a Motorcortex server,
|
|
12
|
+
including login, parameter retrieval, parameter updates, and group management.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import atexit
|
|
16
|
+
from threading import Event, Timer
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
18
|
+
from typing import Any, Callable, List, Optional, Union
|
|
19
|
+
from pynng import Req0, TLSConfig # type: ignore[import-untyped]
|
|
20
|
+
|
|
21
|
+
from motorcortex.reply import Reply
|
|
22
|
+
from motorcortex.setup_logger import logger
|
|
23
|
+
from motorcortex.state_callback_handler import StateCallbackHandler
|
|
24
|
+
from motorcortex.parameter_tree import ParameterTree
|
|
25
|
+
from motorcortex.message_types import MessageTypes
|
|
26
|
+
from motorcortex.nng_url import NngUrl
|
|
27
|
+
from motorcortex.exceptions import McxConnectionError
|
|
28
|
+
from motorcortex import _connection_state, _request_builders, _request_utils, motorcortex_pb2 as _pb
|
|
29
|
+
# ConnectionState lives in _connection_state now — re-exported here so
|
|
30
|
+
# ``from motorcortex.request import ConnectionState`` keeps working.
|
|
31
|
+
from motorcortex._connection_state import ConnectionState
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _close_at_exit(inst: "Request") -> None:
|
|
35
|
+
"""Shutdown hook: close a ``Request`` the user forgot to clean up.
|
|
36
|
+
|
|
37
|
+
Registered via :func:`_register_shutdown` at construction. See
|
|
38
|
+
motorcortex.subscribe for the rationale on using
|
|
39
|
+
``threading._register_atexit`` instead of plain ``atexit`` —
|
|
40
|
+
``concurrent.futures._python_exit`` runs there and would join the
|
|
41
|
+
pool workers before any regular atexit handler could close the
|
|
42
|
+
socket.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
if getattr(inst, "_closed", True):
|
|
46
|
+
return
|
|
47
|
+
inst.close()
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _register_shutdown(inst: "Request") -> None:
|
|
53
|
+
"""Register ``_close_at_exit(inst)`` on the ``threading`` atexit
|
|
54
|
+
registry so it fires before ``concurrent.futures._python_exit``.
|
|
55
|
+
|
|
56
|
+
Falls back to regular ``atexit`` if the private threading API is
|
|
57
|
+
absent.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
import threading
|
|
61
|
+
threading._register_atexit(_close_at_exit, inst) # type: ignore[attr-defined]
|
|
62
|
+
except (AttributeError, ImportError):
|
|
63
|
+
atexit.register(_close_at_exit, inst)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Request:
|
|
67
|
+
"""
|
|
68
|
+
Represents a request connection to a Motorcortex server.
|
|
69
|
+
|
|
70
|
+
The Request class allows you to:
|
|
71
|
+
- Establish and manage a connection to a Motorcortex server.
|
|
72
|
+
- Perform login authentication.
|
|
73
|
+
- Retrieve, set, and overwrite parameter values.
|
|
74
|
+
- Manage parameter groups for efficient batch operations.
|
|
75
|
+
- Save and load parameter trees.
|
|
76
|
+
- Chain asynchronous operations using a promise-like interface (`Reply`).
|
|
77
|
+
|
|
78
|
+
Methods:
|
|
79
|
+
url() -> Optional[str]
|
|
80
|
+
Returns the current connection URL.
|
|
81
|
+
|
|
82
|
+
connect(url: str, **kwargs) -> Reply
|
|
83
|
+
Establishes a connection to the server.
|
|
84
|
+
|
|
85
|
+
close() -> None
|
|
86
|
+
Closes the connection and cleans up resources.
|
|
87
|
+
|
|
88
|
+
send(encoded_msg: Any, do_not_decode_reply: bool = False) -> Optional[Reply]
|
|
89
|
+
Sends an encoded message to the server.
|
|
90
|
+
|
|
91
|
+
login(login: str, password: str) -> Reply
|
|
92
|
+
Sends a login request.
|
|
93
|
+
|
|
94
|
+
connectionState() -> ConnectionState
|
|
95
|
+
Returns the current connection state.
|
|
96
|
+
|
|
97
|
+
getParameterTreeHash() -> Reply
|
|
98
|
+
Requests the parameter tree hash from the server.
|
|
99
|
+
|
|
100
|
+
getParameterTree() -> Reply
|
|
101
|
+
Requests the parameter tree from the server.
|
|
102
|
+
|
|
103
|
+
save(path: str, file_name: str) -> Reply
|
|
104
|
+
Requests the server to save the parameter tree to a file.
|
|
105
|
+
|
|
106
|
+
setParameter(path: str, value: Any, type_name: Optional[str] = None, offset: int = 0, length: int = 0) -> Reply
|
|
107
|
+
Sets a new value for a parameter.
|
|
108
|
+
|
|
109
|
+
setParameterList(param_list: List[dict]) -> Reply
|
|
110
|
+
Sets new values for a list of parameters.
|
|
111
|
+
|
|
112
|
+
getParameter(path: str) -> Reply
|
|
113
|
+
Requests a parameter value and description.
|
|
114
|
+
|
|
115
|
+
getParameterList(path_list: List[str]) -> Reply
|
|
116
|
+
Requests values and descriptions for a list of parameters.
|
|
117
|
+
|
|
118
|
+
overwriteParameter(path: str, value: Any, force_activate: bool = False, type_name: Optional[str] = None) -> Reply
|
|
119
|
+
Overwrites a parameter value and optionally forces it to stay active.
|
|
120
|
+
|
|
121
|
+
releaseParameter(path: str) -> Reply
|
|
122
|
+
Releases the overwrite operation for a parameter.
|
|
123
|
+
|
|
124
|
+
createGroup(path_list: List[str], group_alias: str, frq_divider: int = 1) -> Reply
|
|
125
|
+
Creates a subscription group for a list of parameters.
|
|
126
|
+
|
|
127
|
+
removeGroup(group_alias: str) -> Reply
|
|
128
|
+
Unsubscribes from a group.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> # Establish a connection
|
|
132
|
+
>>> req = motorcortex.Request(protobuf_types, parameter_tree)
|
|
133
|
+
>>> reply = req.connect("tls+tcp://localhost:6501", certificate="path/to/ca.crt")
|
|
134
|
+
>>> if reply.get():
|
|
135
|
+
... print("Connected!")
|
|
136
|
+
>>> # Login
|
|
137
|
+
>>> login_reply = req.login("user", "password")
|
|
138
|
+
>>> if login_reply.get().status == motorcortex.OK:
|
|
139
|
+
... print("Login successful")
|
|
140
|
+
>>> # Get a parameter
|
|
141
|
+
>>> param_reply = req.getParameter("MyDevice.MyParam")
|
|
142
|
+
>>> param = param_reply.get()
|
|
143
|
+
>>> print("Value:", param.value)
|
|
144
|
+
>>> # Set a parameter
|
|
145
|
+
>>> req.setParameter("MyDevice.MyParam", 42)
|
|
146
|
+
>>> # Clean up
|
|
147
|
+
>>> req.close()
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, protobuf_types: "MessageTypes", parameter_tree: "ParameterTree", number_of_threads: int = 2) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Initialize a Request object.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
protobuf_types: Motorcortex message types module.
|
|
156
|
+
parameter_tree: ParameterTree instance.
|
|
157
|
+
number_of_threads (int): Thread pool size (minimum 1, None - use default (CPU-based)).
|
|
158
|
+
"""
|
|
159
|
+
self._socket: Optional[Req0] = None
|
|
160
|
+
self._url: Optional[str] = None
|
|
161
|
+
self._connected_event: Optional[Event] = None
|
|
162
|
+
self._connected: bool = False
|
|
163
|
+
self._protobuf_types: "MessageTypes" = protobuf_types
|
|
164
|
+
self._parameter_tree: "ParameterTree" = parameter_tree
|
|
165
|
+
self._connection_state: ConnectionState = ConnectionState.DISCONNECTED
|
|
166
|
+
self._pool: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=number_of_threads,
|
|
167
|
+
thread_name_prefix="mcx_req")
|
|
168
|
+
self._callback_handler: StateCallbackHandler = StateCallbackHandler()
|
|
169
|
+
self._token: Optional[str] = None
|
|
170
|
+
self._token_timer: Optional[Timer] = None
|
|
171
|
+
self._token_update_interval_sec: float = 30.0
|
|
172
|
+
# Net safeguard: if the caller forgets close(), the receive-loop
|
|
173
|
+
# worker stays blocked in recv() and Python's atexit
|
|
174
|
+
# ``_python_exit`` hook (concurrent.futures) joins it forever,
|
|
175
|
+
# hanging the interpreter. Register a weakref-based cleanup that
|
|
176
|
+
# fires ahead of that hook. Explicit close() makes this a no-op
|
|
177
|
+
# (``__closed`` flag is set and re-entry is guarded).
|
|
178
|
+
self._closed: bool = False
|
|
179
|
+
_register_shutdown(self)
|
|
180
|
+
|
|
181
|
+
def url(self) -> Optional[str]:
|
|
182
|
+
"""Return the current connection URL."""
|
|
183
|
+
return self._url
|
|
184
|
+
|
|
185
|
+
def __repr__(self) -> str:
|
|
186
|
+
return (
|
|
187
|
+
f"Request(url={self._url!r}, "
|
|
188
|
+
f"state={self._connection_state.name})"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def motorcortex_types(self) -> "MessageTypes":
|
|
193
|
+
"""The ``MessageTypes`` instance passed at construction.
|
|
194
|
+
|
|
195
|
+
Exposed so callers (tests, higher-level helpers) can reach the
|
|
196
|
+
bundled protobuf module without constructing a second
|
|
197
|
+
``MessageTypes`` — e.g. ``req.motorcortex_types.motorcortex().OK``.
|
|
198
|
+
"""
|
|
199
|
+
return self._protobuf_types
|
|
200
|
+
|
|
201
|
+
# -- NNG pipe callbacks -------------------------------------------
|
|
202
|
+
#
|
|
203
|
+
# These used to be nested closures inside ``connect()`` that
|
|
204
|
+
# captured ``self`` plus the local ``connected_event`` / ``connected``
|
|
205
|
+
# vars. Promoting them to methods makes their behavior unit-testable
|
|
206
|
+
# by direct invocation (no live pipe event needed) and shrinks
|
|
207
|
+
# ``connect()`` accordingly. The NNG library passes a ``pipe`` arg
|
|
208
|
+
# we don't inspect — prefixed with ``_``.
|
|
209
|
+
|
|
210
|
+
def _on_pipe_connect(self, _pipe: Any) -> None:
|
|
211
|
+
"""Socket accepted a peer pipe. Transition to ``CONNECTION_OK``
|
|
212
|
+
and wake anyone blocked in :func:`_request_utils.wait_for_connection`.
|
|
213
|
+
"""
|
|
214
|
+
old_state = self._connection_state.name
|
|
215
|
+
self._connected = True
|
|
216
|
+
self._connection_state = ConnectionState.CONNECTION_OK
|
|
217
|
+
logger.debug(
|
|
218
|
+
"[REQUEST-CALLBACK] PRE_CONNECT fired — %s -> %s",
|
|
219
|
+
old_state, self._connection_state.name,
|
|
220
|
+
)
|
|
221
|
+
self._callback_handler.notify(self, self.connectionState())
|
|
222
|
+
if self._connected_event is not None:
|
|
223
|
+
self._connected_event.set()
|
|
224
|
+
|
|
225
|
+
def _on_pipe_remove(self, _pipe: Any) -> None:
|
|
226
|
+
"""Socket pipe torn down. Pick the right terminal state via
|
|
227
|
+
:func:`_connection_state.next_state_after_pipe_remove`, then
|
|
228
|
+
wake waiters.
|
|
229
|
+
"""
|
|
230
|
+
old_state = self._connection_state.name
|
|
231
|
+
self._connection_state = _connection_state.next_state_after_pipe_remove(
|
|
232
|
+
self._connection_state
|
|
233
|
+
)
|
|
234
|
+
logger.debug(
|
|
235
|
+
"[REQUEST-CALLBACK] POST_REMOVE fired — %s -> %s",
|
|
236
|
+
old_state, self._connection_state.name,
|
|
237
|
+
)
|
|
238
|
+
self._connected = False
|
|
239
|
+
self._callback_handler.notify(self, self.connectionState())
|
|
240
|
+
if self._connected_event is not None:
|
|
241
|
+
self._connected_event.set()
|
|
242
|
+
|
|
243
|
+
def connect(self, url: str, **kwargs: Any) -> "Reply[bool]":
|
|
244
|
+
"""
|
|
245
|
+
Establish a connection to the Motorcortex server.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
url: Connection URL.
|
|
249
|
+
**kwargs: Additional connection parameters. Supported keys include:
|
|
250
|
+
certificate (str, optional): Path to a TLS certificate file for secure connections.
|
|
251
|
+
conn_timeout_ms (int, optional): Connection timeout in milliseconds (default: 1000).
|
|
252
|
+
recv_timeout_ms (int, optional): Receive timeout in milliseconds (default: 500).
|
|
253
|
+
login (str, optional): Username for authentication (if required by server).
|
|
254
|
+
password (str, optional): Password for authentication (if required by server).
|
|
255
|
+
state_update (Callable, optional): Callback function to be called on connection state changes.
|
|
256
|
+
timeout_ms (int, optional): Alternative timeout in milliseconds (used by Request.parse).
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Reply: A promise that resolves when the connection is established.
|
|
260
|
+
"""
|
|
261
|
+
self._connection_state = ConnectionState.CONNECTING
|
|
262
|
+
conn_timeout_ms, recv_timeout_ms, certificate, state_update = _request_utils.parse_connect_kwargs(**kwargs)
|
|
263
|
+
|
|
264
|
+
if state_update:
|
|
265
|
+
self._callback_handler.start(state_update)
|
|
266
|
+
|
|
267
|
+
self._url = url
|
|
268
|
+
tls_config = None
|
|
269
|
+
if certificate:
|
|
270
|
+
parsed = NngUrl(url)
|
|
271
|
+
# auth_mode=OPTIONAL: motorcortex deployments ship a self-signed
|
|
272
|
+
# end-entity cert that clients pin directly. Modern mbedTLS (3.6+)
|
|
273
|
+
# refuses to use a non-CA cert as a trust anchor under the
|
|
274
|
+
# REQUIRED mode pynng defaults to, producing a cryptic
|
|
275
|
+
# "Cryptographic error" on handshake. OPTIONAL performs the
|
|
276
|
+
# verification best-effort (still catches the obvious
|
|
277
|
+
# wrong-server case) without aborting on the CA:FALSE check.
|
|
278
|
+
tls_config = TLSConfig(
|
|
279
|
+
TLSConfig.MODE_CLIENT,
|
|
280
|
+
ca_files=certificate,
|
|
281
|
+
server_name=parsed.hostname,
|
|
282
|
+
auth_mode=TLSConfig.AUTH_MODE_OPTIONAL,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
self._socket = Req0(recv_timeout=recv_timeout_ms, tls_config=tls_config)
|
|
286
|
+
self._connected_event = Event()
|
|
287
|
+
self._connected = False
|
|
288
|
+
|
|
289
|
+
self._socket.add_pre_pipe_connect_cb(self._on_pipe_connect)
|
|
290
|
+
self._socket.add_post_pipe_remove_cb(self._on_pipe_remove)
|
|
291
|
+
|
|
292
|
+
logger.debug(
|
|
293
|
+
"[REQUEST] dialing %s (timeout=%dms, tls=%s)",
|
|
294
|
+
url, conn_timeout_ms, bool(tls_config),
|
|
295
|
+
)
|
|
296
|
+
self._socket.dial(url, block=False)
|
|
297
|
+
|
|
298
|
+
return Reply(self._pool.submit(_request_utils.wait_for_connection, self._connected_event,
|
|
299
|
+
conn_timeout_ms / 1000.0, lambda: self._connected))
|
|
300
|
+
|
|
301
|
+
def close(self) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Close the request connection and clean up resources.
|
|
304
|
+
"""
|
|
305
|
+
if self._closed:
|
|
306
|
+
return
|
|
307
|
+
self._closed = True
|
|
308
|
+
logger.debug("[REQUEST] closing (state=%s)", self._connection_state.name)
|
|
309
|
+
|
|
310
|
+
self._connection_state = ConnectionState.DISCONNECTING
|
|
311
|
+
self._stopTokenRefresh()
|
|
312
|
+
if self._connected_event:
|
|
313
|
+
self._connected = False
|
|
314
|
+
self._connected_event.set()
|
|
315
|
+
|
|
316
|
+
if self._socket:
|
|
317
|
+
self._socket.close()
|
|
318
|
+
|
|
319
|
+
self._callback_handler.stop()
|
|
320
|
+
self._pool.shutdown(wait=True)
|
|
321
|
+
|
|
322
|
+
def send(self, encoded_msg: Any, do_not_decode_reply: bool = False) -> "Reply[Any]":
|
|
323
|
+
"""Send an encoded message to the server.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
encoded_msg: Encoded protobuf message.
|
|
327
|
+
do_not_decode_reply: If True, do not decode the reply.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Reply: A promise for the server's reply.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
McxConnectionError: the underlying socket is closed
|
|
334
|
+
or was never opened. Protocol-level failures (bad
|
|
335
|
+
path, permission denied, etc.) do **not** raise —
|
|
336
|
+
they arrive on ``reply.get().status``. Transport
|
|
337
|
+
failures do.
|
|
338
|
+
"""
|
|
339
|
+
if self._socket is None:
|
|
340
|
+
raise McxConnectionError(
|
|
341
|
+
"Cannot send: Request is not connected (socket is None). "
|
|
342
|
+
"Call Request.connect(...) first."
|
|
343
|
+
)
|
|
344
|
+
return Reply(self._pool.submit(
|
|
345
|
+
_request_utils.send_and_recv, self._socket, encoded_msg,
|
|
346
|
+
None if do_not_decode_reply else self._protobuf_types,
|
|
347
|
+
))
|
|
348
|
+
|
|
349
|
+
def login(self, login: str, password: str) -> "Reply[_pb.StatusMsg]":
|
|
350
|
+
"""
|
|
351
|
+
Send a login request to the server.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
login: User login.
|
|
355
|
+
password: User password.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Reply: A promise for the login reply.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
login_msg = self._protobuf_types.createType('motorcortex.LoginMsg')
|
|
362
|
+
login_msg.password = password
|
|
363
|
+
login_msg.login = login
|
|
364
|
+
|
|
365
|
+
return self.send(self._protobuf_types.encode(login_msg))
|
|
366
|
+
|
|
367
|
+
def connectionState(self) -> ConnectionState:
|
|
368
|
+
"""
|
|
369
|
+
Get the current connection state.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
ConnectionState: The current state.
|
|
373
|
+
"""
|
|
374
|
+
return self._connection_state
|
|
375
|
+
|
|
376
|
+
def getParameterTreeHash(self) -> "Reply[_pb.ParameterTreeHashMsg]":
|
|
377
|
+
"""
|
|
378
|
+
Request a parameter tree hash from the server.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Reply: A promise for the parameter tree hash.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
# getting and instantiating data type from the loaded dict
|
|
385
|
+
param_tree_hash_msg = self._protobuf_types.createType('motorcortex.GetParameterTreeHashMsg')
|
|
386
|
+
|
|
387
|
+
# encoding and sending data
|
|
388
|
+
return self.send(self._protobuf_types.encode(param_tree_hash_msg))
|
|
389
|
+
|
|
390
|
+
def getParameterTree(self) -> "Reply[_pb.ParameterTreeMsg]":
|
|
391
|
+
"""
|
|
392
|
+
Request a parameter tree from the server.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Reply: A promise for the parameter tree.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
return Reply(self._pool.submit(
|
|
399
|
+
self._getParameterTree,
|
|
400
|
+
self.getParameterTreeHash(), self._protobuf_types, self._socket, self._url,
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
def save(self, path: str, file_name: str) -> "Reply[_pb.StatusMsg]":
|
|
404
|
+
"""
|
|
405
|
+
Request the server to save a parameter tree to a file.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
path: Path to save the file.
|
|
409
|
+
file_name: Name of the file.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Reply: A promise for the save operation.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
param_save_msg = self._protobuf_types.createType('motorcortex.SaveMsg')
|
|
416
|
+
param_save_msg.path = path
|
|
417
|
+
param_save_msg.file_name = file_name
|
|
418
|
+
|
|
419
|
+
return self.send(self._protobuf_types.encode(param_save_msg))
|
|
420
|
+
|
|
421
|
+
def setParameter(self, path: str, value: Any, type_name: Optional[str] = None, offset: int = 0,
|
|
422
|
+
length: int = 0) -> "Reply[_pb.StatusMsg]":
|
|
423
|
+
"""
|
|
424
|
+
Set a new value for a parameter.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
path: Parameter path.
|
|
428
|
+
value: New value.
|
|
429
|
+
type_name: Type name (optional).
|
|
430
|
+
offset: Offset in array (optional).
|
|
431
|
+
length: Number of elements to update (optional).
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Reply: A promise for the set operation.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
if (offset == 0) and (length == 0):
|
|
438
|
+
return self.send(self._protobuf_types.encode(_request_builders.build_set_parameter_msg(
|
|
439
|
+
path, value, type_name, self._protobuf_types, self._parameter_tree)))
|
|
440
|
+
else:
|
|
441
|
+
return self.send(self._protobuf_types.encode(_request_builders.build_set_parameter_with_offset_msg(
|
|
442
|
+
offset, length, path, value, type_name, self._protobuf_types, self._parameter_tree)))
|
|
443
|
+
|
|
444
|
+
def setParameterList(self, param_list: List[dict]) -> "Reply[_pb.StatusMsg]":
|
|
445
|
+
"""
|
|
446
|
+
Set new values to a parameter list
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
param_list([{'path'-`str`,'value'-`any`, 'offset', 'length'}]): a list of the parameters which values update
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Reply(StatusMsg): A Promise, which resolves when parameters from the list are updated,
|
|
453
|
+
otherwise fails.
|
|
454
|
+
|
|
455
|
+
Examples:
|
|
456
|
+
>>> req.setParameterList([
|
|
457
|
+
>>> {'path': 'root/Control/generator/enable', 'value': False},
|
|
458
|
+
>>> {'path': 'root/Control/generator/amplitude', 'value': 1.4}])
|
|
459
|
+
>>> {'path': 'root/Control/myArray6', 'value': [1.4, 1.5], 'offset': 1, 'length': 2}])
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
# instantiating message type
|
|
463
|
+
set_param_list_msg = self._protobuf_types.createType("motorcortex.SetParameterListMsg")
|
|
464
|
+
# filling with sub messages
|
|
465
|
+
for param in param_list:
|
|
466
|
+
type_name = param.get("type_name", None)
|
|
467
|
+
offset = param.get("offset", 0)
|
|
468
|
+
length = param.get("length", 0)
|
|
469
|
+
if (offset == 0) and (length == 0):
|
|
470
|
+
set_param_list_msg.params.extend([_request_builders.build_set_parameter_msg(
|
|
471
|
+
param["path"], param["value"], type_name, self._protobuf_types, self._parameter_tree)])
|
|
472
|
+
else:
|
|
473
|
+
set_param_list_msg.params.extend([_request_builders.build_set_parameter_with_offset_msg(
|
|
474
|
+
offset, length, param["path"], param["value"],
|
|
475
|
+
type_name, self._protobuf_types, self._parameter_tree)])
|
|
476
|
+
|
|
477
|
+
# encoding and sending data
|
|
478
|
+
return self.send(self._protobuf_types.encode(set_param_list_msg))
|
|
479
|
+
|
|
480
|
+
def getParameter(self, path: str) -> "Reply[_pb.ParameterMsg]":
|
|
481
|
+
"""
|
|
482
|
+
Request a parameter value and description from the server.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
path: Parameter path.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Reply: A promise for the parameter value.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
return self.send(self._protobuf_types.encode(
|
|
492
|
+
_request_builders.build_get_parameter_msg(path, self._protobuf_types)))
|
|
493
|
+
|
|
494
|
+
def getParameterList(self, path_list: List[str]) -> "Reply[_pb.ParameterListMsg]":
|
|
495
|
+
"""
|
|
496
|
+
Request values and descriptions for a list of parameters.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
path_list: List of parameter paths.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Reply: A promise for the parameter list.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
# instantiating message type
|
|
506
|
+
get_param_list_msg = self._protobuf_types.createType('motorcortex.GetParameterListMsg')
|
|
507
|
+
# filling with sub messages
|
|
508
|
+
for path in path_list:
|
|
509
|
+
get_param_list_msg.params.extend([
|
|
510
|
+
_request_builders.build_get_parameter_msg(path, self._protobuf_types)])
|
|
511
|
+
|
|
512
|
+
# encoding and sending data
|
|
513
|
+
return self.send(self._protobuf_types.encode(get_param_list_msg))
|
|
514
|
+
|
|
515
|
+
def overwriteParameter(self, path: str, value: Any, force_activate: bool = False,
|
|
516
|
+
type_name: Optional[str] = None) -> "Reply[_pb.StatusMsg]":
|
|
517
|
+
"""
|
|
518
|
+
Overwrite a parameter value and optionally force it to stay active.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
path: Parameter path.
|
|
522
|
+
value: New value.
|
|
523
|
+
force_activate: Force value to stay active.
|
|
524
|
+
type_name: Type name (optional).
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Reply: A promise for the overwrite operation.
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
return self.send(self._protobuf_types.encode(_request_builders.build_overwrite_parameter_msg(
|
|
531
|
+
path, value, force_activate, type_name, self._protobuf_types, self._parameter_tree)))
|
|
532
|
+
|
|
533
|
+
def releaseParameter(self, path: str) -> "Reply[_pb.StatusMsg]":
|
|
534
|
+
"""
|
|
535
|
+
Release the overwrite operation for a parameter.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
path: Parameter path.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Reply: A promise for the release operation.
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
return self.send(self._protobuf_types.encode(
|
|
545
|
+
_request_builders.build_release_parameter_msg(path, self._protobuf_types)))
|
|
546
|
+
|
|
547
|
+
def createGroup(self, path_list: List[str], group_alias: str, frq_divider: int = 1) -> "Reply[_pb.GroupStatusMsg]":
|
|
548
|
+
"""
|
|
549
|
+
Create a subscription group for a list of parameters.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
path_list: List of parameter paths.
|
|
553
|
+
group_alias: Group alias.
|
|
554
|
+
frq_divider: Frequency divider.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Reply: A promise for the group creation.
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
# instantiating message type
|
|
561
|
+
create_group_msg = self._protobuf_types.createType('motorcortex.CreateGroupMsg')
|
|
562
|
+
create_group_msg.alias = group_alias
|
|
563
|
+
create_group_msg.paths.extend(path_list if type(path_list) is list else [path_list])
|
|
564
|
+
create_group_msg.frq_divider = frq_divider if frq_divider > 1 else 1
|
|
565
|
+
# encoding and sending data
|
|
566
|
+
return self.send(self._protobuf_types.encode(create_group_msg))
|
|
567
|
+
|
|
568
|
+
def removeGroup(self, group_alias: str) -> "Reply[_pb.StatusMsg]":
|
|
569
|
+
"""
|
|
570
|
+
Unsubscribe from a group.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
group_alias: Group alias.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Reply: A promise for the unsubscribe operation.
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
# instantiating message type
|
|
580
|
+
remove_group_msg = self._protobuf_types.createType('motorcortex.RemoveGroupMsg')
|
|
581
|
+
remove_group_msg.alias = group_alias
|
|
582
|
+
# encoding and sending data
|
|
583
|
+
return self.send(self._protobuf_types.encode(remove_group_msg))
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def token(self) -> Optional[str]:
|
|
587
|
+
"""Return the current session token."""
|
|
588
|
+
return self._token
|
|
589
|
+
|
|
590
|
+
def getSessionToken(self) -> "Reply[_pb.SessionTokenMsg]":
|
|
591
|
+
"""Request a session token from the server."""
|
|
592
|
+
msg = self._protobuf_types.createType('motorcortex.GetSessionTokenMsg')
|
|
593
|
+
return self.send(self._protobuf_types.encode(msg))
|
|
594
|
+
|
|
595
|
+
def restoreSession(self, token: str) -> "Reply[_pb.SessionTokenMsg]":
|
|
596
|
+
"""Restore a session using a saved token."""
|
|
597
|
+
msg = self._protobuf_types.createType('motorcortex.RestoreSessionMsg')
|
|
598
|
+
msg.token = token
|
|
599
|
+
return self.send(self._protobuf_types.encode(msg))
|
|
600
|
+
|
|
601
|
+
def _startTokenRefresh(self, interval_sec: float = 30.0) -> None:
|
|
602
|
+
"""Start periodic session token refresh.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
interval_sec: Interval between token refreshes in seconds.
|
|
606
|
+
"""
|
|
607
|
+
self._token_update_interval_sec = interval_sec
|
|
608
|
+
self._stopTokenRefresh()
|
|
609
|
+
# Fetch token immediately, then schedule periodic refresh
|
|
610
|
+
self._fetchToken()
|
|
611
|
+
self._scheduleTokenRefresh()
|
|
612
|
+
|
|
613
|
+
def _stopTokenRefresh(self) -> None:
|
|
614
|
+
"""Stop periodic session token refresh."""
|
|
615
|
+
if self._token_timer:
|
|
616
|
+
self._token_timer.cancel()
|
|
617
|
+
self._token_timer = None
|
|
618
|
+
|
|
619
|
+
def _scheduleTokenRefresh(self) -> None:
|
|
620
|
+
"""Schedule the next token refresh."""
|
|
621
|
+
self._stopTokenRefresh()
|
|
622
|
+
self._token_timer = Timer(self._token_update_interval_sec, self._onTokenTimer)
|
|
623
|
+
self._token_timer.daemon = True
|
|
624
|
+
self._token_timer.start()
|
|
625
|
+
|
|
626
|
+
def _onTokenTimer(self) -> None:
|
|
627
|
+
"""Timer callback: fetch token and reschedule."""
|
|
628
|
+
self._fetchToken()
|
|
629
|
+
# Always reschedule if not disconnecting/disconnected
|
|
630
|
+
if self._connection_state not in (ConnectionState.DISCONNECTING, ConnectionState.DISCONNECTED):
|
|
631
|
+
self._scheduleTokenRefresh()
|
|
632
|
+
|
|
633
|
+
def _fetchToken(self) -> None:
|
|
634
|
+
"""Fetch a session token from the server."""
|
|
635
|
+
if self._connection_state != ConnectionState.CONNECTION_OK:
|
|
636
|
+
return
|
|
637
|
+
try:
|
|
638
|
+
reply = self.getSessionToken()
|
|
639
|
+
if reply:
|
|
640
|
+
msg = reply.get(timeout_ms=5000)
|
|
641
|
+
if msg and hasattr(msg, 'token'):
|
|
642
|
+
self._token = msg.token
|
|
643
|
+
except Exception as e:
|
|
644
|
+
logger.debug("[REQUEST] token refresh failed: %s", e)
|
|
645
|
+
|
|
646
|
+
# Pure protobuf message builders live in ``motorcortex._request_builders``.
|
|
647
|
+
# They used to be private ``__buildXxxMsg`` static methods on this class;
|
|
648
|
+
# moving them out lets them be unit-tested without spinning up a socket.
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def _getParameterTree(
|
|
652
|
+
hash_reply: Reply,
|
|
653
|
+
protobuf_types: MessageTypes,
|
|
654
|
+
socket: Req0,
|
|
655
|
+
url: Optional[str] = None,
|
|
656
|
+
) -> Any:
|
|
657
|
+
"""Thunk that unwraps ``hash_reply`` and delegates to
|
|
658
|
+
:func:`motorcortex._request_utils.fetch_parameter_tree`.
|
|
659
|
+
|
|
660
|
+
Runs inside the request-pool worker (see ``getParameterTree``),
|
|
661
|
+
which is why it takes a pre-queued :class:`Reply` — we can't do
|
|
662
|
+
the ``.get()`` on the caller thread or we'd block the pool-submit.
|
|
663
|
+
"""
|
|
664
|
+
tree_hash = hash_reply.get()
|
|
665
|
+
return _request_utils.fetch_parameter_tree(
|
|
666
|
+
tree_hash.hash, protobuf_types, socket, url,
|
|
667
|
+
)
|
|
668
|
+
|