motorcortex-python 1.0.0__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 +362 -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.0.dist-info/LICENSE +22 -0
- motorcortex_python-1.0.0.dist-info/METADATA +171 -0
- motorcortex_python-1.0.0.dist-info/RECORD +28 -0
- motorcortex_python-1.0.0.dist-info/WHEEL +5 -0
- motorcortex_python-1.0.0.dist-info/top_level.txt +1 -0
motorcortex/__init__.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
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-python
|
|
10
|
+
|
|
11
|
+
Motorcortex is a Python library for connecting to Motorcortex servers, handling requests, subscriptions, and parameter trees.
|
|
12
|
+
Provides high-level APIs for communication, login, and data exchange using protocol buffers.
|
|
13
|
+
|
|
14
|
+
See documentation for usage examples.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from enum import IntEnum as _IntEnum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from motorcortex.version import __version__
|
|
21
|
+
from motorcortex.parameter_tree import ParameterTree
|
|
22
|
+
from motorcortex.message_types import MessageTypes
|
|
23
|
+
from motorcortex.request import Request, ConnectionState
|
|
24
|
+
from motorcortex.reply import Reply
|
|
25
|
+
from motorcortex.subscribe import Subscribe
|
|
26
|
+
from motorcortex.subscription import Subscription
|
|
27
|
+
from motorcortex.timespec import (
|
|
28
|
+
Timespec,
|
|
29
|
+
compare_timespec,
|
|
30
|
+
timespec_to_sec,
|
|
31
|
+
timespec_to_msec,
|
|
32
|
+
timespec_to_usec,
|
|
33
|
+
timespec_to_nsec,
|
|
34
|
+
)
|
|
35
|
+
from motorcortex.init_threads import init_nng_threads
|
|
36
|
+
from motorcortex.session import Session
|
|
37
|
+
from motorcortex.exceptions import (
|
|
38
|
+
McxError,
|
|
39
|
+
McxConnectionError,
|
|
40
|
+
McxLoginError,
|
|
41
|
+
McxTimeout,
|
|
42
|
+
)
|
|
43
|
+
from motorcortex.motorcortex_pb2 import StatusCode as _PbStatusCode
|
|
44
|
+
from motorcortex.setup_logger import logger # not re-exported — ``__all__`` gates package surface
|
|
45
|
+
|
|
46
|
+
# Build an ``IntEnum`` facade over the protobuf ``StatusCode`` enum so
|
|
47
|
+
# callers can write ``motorcortex.StatusCode.OK`` without reaching into
|
|
48
|
+
# the generated protobuf module. Subclassing ``int`` means comparisons
|
|
49
|
+
# like ``reply.status == StatusCode.OK`` work against the raw ints that
|
|
50
|
+
# come back on the wire.
|
|
51
|
+
StatusCode = _IntEnum(
|
|
52
|
+
"StatusCode",
|
|
53
|
+
{v.name: v.number for v in _PbStatusCode.DESCRIPTOR.values},
|
|
54
|
+
)
|
|
55
|
+
StatusCode.__doc__ = (
|
|
56
|
+
"Motorcortex response status codes. Members mirror the ``StatusCode`` "
|
|
57
|
+
"enum in the wire protocol. Use ``motorcortex.StatusCode.OK`` (or the "
|
|
58
|
+
"flat re-exports such as ``motorcortex.OK``) to compare against "
|
|
59
|
+
"``reply.status``."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Flat re-exports for the conventional ``motorcortex.OK`` idiom. Values
|
|
63
|
+
# are members of :class:`StatusCode` and therefore also ``int``.
|
|
64
|
+
OK = StatusCode.OK
|
|
65
|
+
READ_ONLY_MODE = StatusCode.READ_ONLY_MODE
|
|
66
|
+
FAILED = StatusCode.FAILED
|
|
67
|
+
FAILED_TO_DECODE = StatusCode.FAILED_TO_DECODE
|
|
68
|
+
SUB_LIST_IS_FULL = StatusCode.SUB_LIST_IS_FULL
|
|
69
|
+
WRONG_PARAMETER_PATH = StatusCode.WRONG_PARAMETER_PATH
|
|
70
|
+
FAILED_TO_SET_REQUESTED_FRQ = StatusCode.FAILED_TO_SET_REQUESTED_FRQ
|
|
71
|
+
FAILED_TO_OPEN_FILE = StatusCode.FAILED_TO_OPEN_FILE
|
|
72
|
+
GROUP_LIST_IS_FULL = StatusCode.GROUP_LIST_IS_FULL
|
|
73
|
+
WRONG_PASSWORD = StatusCode.WRONG_PASSWORD
|
|
74
|
+
USER_NOT_LOGGED_IN = StatusCode.USER_NOT_LOGGED_IN
|
|
75
|
+
PERMISSION_DENIED = StatusCode.PERMISSION_DENIED
|
|
76
|
+
|
|
77
|
+
# ``__all__`` is the 1.0 public API surface. Anything not listed here —
|
|
78
|
+
# including module-level helpers defined below (``parseUrl``,
|
|
79
|
+
# ``makeUrl``, ``statusToStr``) and submodule imports like
|
|
80
|
+
# ``motorcortex.setup_logger`` — is implicitly private and may change
|
|
81
|
+
# between minor releases. For logging, do
|
|
82
|
+
# ``logging.getLogger("mcx")`` instead of reaching into the package.
|
|
83
|
+
__all__ = [
|
|
84
|
+
"__version__",
|
|
85
|
+
# Entry points
|
|
86
|
+
"connect",
|
|
87
|
+
"Session",
|
|
88
|
+
# Connection / protocol classes
|
|
89
|
+
"Request",
|
|
90
|
+
"Subscribe",
|
|
91
|
+
"Subscription",
|
|
92
|
+
"Reply",
|
|
93
|
+
"ConnectionState",
|
|
94
|
+
# Protobuf / parameters
|
|
95
|
+
"MessageTypes",
|
|
96
|
+
"ParameterTree",
|
|
97
|
+
# Time
|
|
98
|
+
"Timespec",
|
|
99
|
+
"compare_timespec",
|
|
100
|
+
"timespec_to_sec",
|
|
101
|
+
"timespec_to_msec",
|
|
102
|
+
"timespec_to_usec",
|
|
103
|
+
"timespec_to_nsec",
|
|
104
|
+
# Exceptions
|
|
105
|
+
"McxError",
|
|
106
|
+
"McxConnectionError",
|
|
107
|
+
"McxLoginError",
|
|
108
|
+
"McxTimeout",
|
|
109
|
+
# Status codes
|
|
110
|
+
"StatusCode",
|
|
111
|
+
"OK",
|
|
112
|
+
"READ_ONLY_MODE",
|
|
113
|
+
"FAILED",
|
|
114
|
+
"FAILED_TO_DECODE",
|
|
115
|
+
"SUB_LIST_IS_FULL",
|
|
116
|
+
"WRONG_PARAMETER_PATH",
|
|
117
|
+
"FAILED_TO_SET_REQUESTED_FRQ",
|
|
118
|
+
"FAILED_TO_OPEN_FILE",
|
|
119
|
+
"GROUP_LIST_IS_FULL",
|
|
120
|
+
"WRONG_PASSWORD",
|
|
121
|
+
"USER_NOT_LOGGED_IN",
|
|
122
|
+
"PERMISSION_DENIED",
|
|
123
|
+
# Tuning
|
|
124
|
+
"init_nng_threads",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
init_nng_threads()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def parseUrl(url: str) -> tuple[str, str, int | None, int | None]:
|
|
131
|
+
"""
|
|
132
|
+
Parses a Motorcortex connection URL to extract request and subscribe addresses and ports.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
url (str): The connection URL, expected in the format 'address:req_port:sub_port'.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
tuple: (req_address, sub_address, req_port, sub_port)
|
|
139
|
+
- req_address (str): Address for request connection.
|
|
140
|
+
- sub_address (str): Address for subscribe connection.
|
|
141
|
+
- req_port (int or None): Port for request connection.
|
|
142
|
+
- sub_port (int or None): Port for subscribe connection.
|
|
143
|
+
|
|
144
|
+
If the URL does not contain ports, default endpoints '/mcx_req' and '/mcx_sub' are appended.
|
|
145
|
+
"""
|
|
146
|
+
# IPv6 host literals embed colons (``wss://[::1]``), so the rfind-based
|
|
147
|
+
# port scan has to start *after* the closing bracket of the host
|
|
148
|
+
# literal — otherwise it walks into the address bytes and either
|
|
149
|
+
# mis-parses the ports or (in the no-ports case) tries to int() an
|
|
150
|
+
# empty slice. For IPv4 / hostname URLs there is no bracket, so
|
|
151
|
+
# ``port_start`` stays at 0 and behavior matches the pre-fix code.
|
|
152
|
+
host_end = url.rfind(']')
|
|
153
|
+
port_start = host_end + 1 if host_end != -1 else 0
|
|
154
|
+
|
|
155
|
+
end = url.rfind(':')
|
|
156
|
+
if end < port_start:
|
|
157
|
+
return url + '/mcx_req', url + '/mcx_sub', None, None
|
|
158
|
+
start = url.rfind(':', port_start, end)
|
|
159
|
+
if start == -1:
|
|
160
|
+
return url + '/mcx_req', url + '/mcx_sub', None, None
|
|
161
|
+
req_port = int(url[start + 1:end])
|
|
162
|
+
sub_port = int(url[end + 1:])
|
|
163
|
+
address = url[:start]
|
|
164
|
+
return address, address, req_port, sub_port
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def makeUrl(address: str, port: int | None) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Constructs a URL string from an address and port.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
address (str): The base address.
|
|
173
|
+
port (int or None): The port number.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
str: The combined address and port in the format 'address:port', or just 'address' if port is None.
|
|
177
|
+
"""
|
|
178
|
+
if port:
|
|
179
|
+
return "{}:{}".format(address, port)
|
|
180
|
+
|
|
181
|
+
return address
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def connect(
|
|
185
|
+
url: str,
|
|
186
|
+
motorcortex_types: "MessageTypes",
|
|
187
|
+
param_tree: "ParameterTree",
|
|
188
|
+
reconnect: bool = True,
|
|
189
|
+
**kwargs: Any,
|
|
190
|
+
) -> tuple["Request", "Subscribe"]:
|
|
191
|
+
"""
|
|
192
|
+
Establishes connections to Motorcortex request and subscribe endpoints, performs login, and loads the parameter tree.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
url (str): Connection URL in the format 'address:req_port:sub_port' or 'wss://address'.
|
|
196
|
+
motorcortex_types (MessageTypes): Motorcortex message types instance.
|
|
197
|
+
param_tree (ParameterTree): ParameterTree instance to load parameters into.
|
|
198
|
+
reconnect (bool, optional): Whether to enable automatic reconnection. Defaults to True.
|
|
199
|
+
**kwargs: Additional keyword arguments:
|
|
200
|
+
login (str): Username for authentication.
|
|
201
|
+
password (str): Password for authentication.
|
|
202
|
+
certificate (str, optional): Path to a TLS certificate file for secure connections.
|
|
203
|
+
timeout_ms (int, optional): Connection timeout in milliseconds. Defaults to 1000.
|
|
204
|
+
recv_timeout_ms (int, optional): Receive timeout in milliseconds. Defaults to 500.
|
|
205
|
+
token_update_interval_ms (int, optional): Session token refresh interval in milliseconds. Defaults to 30000.
|
|
206
|
+
state_update (Callable, optional): Custom callback for connection state changes.
|
|
207
|
+
If provided, disables the built-in reconnect logic.
|
|
208
|
+
req_number_of_threads (int, optional): Thread pool size for request connection. Defaults to 2.
|
|
209
|
+
sub_number_of_threads (int, optional): Thread pool size for subscribe connection. Defaults to 2.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
tuple: (req, sub)
|
|
213
|
+
- req (Request): Established request connection.
|
|
214
|
+
- sub (Subscribe): Established subscribe connection.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
RuntimeError: If connection or login fails.
|
|
218
|
+
|
|
219
|
+
Examples:
|
|
220
|
+
>>> from motorcortex import connect, MessageTypes, ParameterTree
|
|
221
|
+
>>> types = MessageTypes()
|
|
222
|
+
>>> tree = ParameterTree()
|
|
223
|
+
>>> req, sub = connect("wss://192.168.2.100", types, tree,
|
|
224
|
+
... certificate="mcx.cert.crt", timeout_ms=1000,
|
|
225
|
+
... login="admin", password="admin",
|
|
226
|
+
... token_update_interval_ms=15000)
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
token_interval_sec = kwargs.get("token_update_interval_ms", 30000) / 1000.0
|
|
230
|
+
|
|
231
|
+
initial_connect_done = [False] # Now reconnections will trigger callback login
|
|
232
|
+
|
|
233
|
+
if reconnect and not kwargs.get("state_update"):
|
|
234
|
+
|
|
235
|
+
def stateUpdate(req, sub, state):
|
|
236
|
+
if state == ConnectionState.CONNECTION_OK and initial_connect_done[0]:
|
|
237
|
+
# Try to restore session using token, fall back to login
|
|
238
|
+
restored = False
|
|
239
|
+
if req.token:
|
|
240
|
+
try:
|
|
241
|
+
restore_reply = req.restoreSession(req.token)
|
|
242
|
+
restore_msg = restore_reply.get(timeout_ms=5000)
|
|
243
|
+
motorcortex_msg = motorcortex_types.motorcortex()
|
|
244
|
+
if restore_msg and restore_msg.status == motorcortex_msg.OK:
|
|
245
|
+
restored = True
|
|
246
|
+
logger.debug("[SESSION] Session restored using token")
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.debug(f"[SESSION] Token restore failed: {e}")
|
|
249
|
+
|
|
250
|
+
if not restored:
|
|
251
|
+
logger.debug("[SESSION] Falling back to login")
|
|
252
|
+
req.login(kwargs.get("login"), kwargs.get("password")).get()
|
|
253
|
+
|
|
254
|
+
req._startTokenRefresh(token_interval_sec)
|
|
255
|
+
sub.resubscribe()
|
|
256
|
+
|
|
257
|
+
kwargs.update(state_update=stateUpdate)
|
|
258
|
+
|
|
259
|
+
# Parse address
|
|
260
|
+
req_address, sub_address, req_port, sub_port = parseUrl(url)
|
|
261
|
+
# Open request connection
|
|
262
|
+
req = Request(motorcortex_types, param_tree, kwargs.get("req_number_of_threads", 2))
|
|
263
|
+
sub = None
|
|
264
|
+
# Wrap every post-construction failure point so half-opened sockets
|
|
265
|
+
# get closed; otherwise the Subscribe receive thread stays blocked in
|
|
266
|
+
# recv() forever and pins the Python interpreter on exit (an atexit
|
|
267
|
+
# ``_python_exit`` handler then joins the worker indefinitely).
|
|
268
|
+
try:
|
|
269
|
+
kwargs_copy = kwargs.copy()
|
|
270
|
+
kwargs_copy.update(state_update=None)
|
|
271
|
+
if not req.connect(makeUrl(req_address, req_port), **kwargs_copy).get():
|
|
272
|
+
raise McxConnectionError(
|
|
273
|
+
"Failed to establish request connection: {}:{}".format(req_address, req_port)
|
|
274
|
+
)
|
|
275
|
+
# Open subscribe connection
|
|
276
|
+
sub = Subscribe(req, motorcortex_types, kwargs.get("sub_number_of_threads", 2))
|
|
277
|
+
if not sub.connect(makeUrl(sub_address, sub_port), **kwargs).get():
|
|
278
|
+
raise McxConnectionError(
|
|
279
|
+
"Failed to establish subscribe connection: {}:{}".format(sub_address, sub_port)
|
|
280
|
+
)
|
|
281
|
+
# Login. With ``Login: disable`` servers the credentials can legitimately
|
|
282
|
+
# be omitted — default to empty strings so the helper does not raise
|
|
283
|
+
# ``KeyError`` on valid callers.
|
|
284
|
+
login_reply = req.login(kwargs.get('login', ''), kwargs.get('password', ''))
|
|
285
|
+
login_reply_msg = login_reply.get()
|
|
286
|
+
|
|
287
|
+
motorcortex_msg = motorcortex_types.motorcortex()
|
|
288
|
+
if not login_reply_msg.status == motorcortex_msg.OK:
|
|
289
|
+
raise McxLoginError(
|
|
290
|
+
"Login failed, status: {}".format(login_reply_msg.status),
|
|
291
|
+
status=login_reply_msg.status,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Requesting a parameter tree
|
|
295
|
+
param_tree_reply = req.getParameterTree()
|
|
296
|
+
tree = param_tree_reply.get()
|
|
297
|
+
param_tree.load(tree)
|
|
298
|
+
except Exception:
|
|
299
|
+
if sub is not None:
|
|
300
|
+
try:
|
|
301
|
+
sub.close()
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
try:
|
|
305
|
+
req.close()
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
raise
|
|
309
|
+
|
|
310
|
+
# Start session token refresh
|
|
311
|
+
req._startTokenRefresh(token_interval_sec)
|
|
312
|
+
|
|
313
|
+
initial_connect_done[0] = True # Now reconnections will trigger callback login
|
|
314
|
+
|
|
315
|
+
return req, sub
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def statusToStr(motorcortex_msg: Any, code: int) -> str:
|
|
319
|
+
"""Converts status codes to a readable message.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
motorcortex_msg(Module): reference to a motorcortex module
|
|
323
|
+
code(int): status code
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
str: status message
|
|
327
|
+
|
|
328
|
+
Examples:
|
|
329
|
+
>>> login_reply = req.login("admin", "iddqd")
|
|
330
|
+
>>> login_reply_msg = login_reply.get()
|
|
331
|
+
>>> if login_reply_msg.status != motorcortex_msg.OK:
|
|
332
|
+
>>> print(motorcortex.statusToStr(motorcortex_msg, login_reply_msg.status))
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
status = 'Unknown code'
|
|
337
|
+
if code == motorcortex_msg.OK:
|
|
338
|
+
status = 'Success'
|
|
339
|
+
elif code == motorcortex_msg.FAILED:
|
|
340
|
+
status = 'Failed'
|
|
341
|
+
elif code == motorcortex_msg.FAILED_TO_DECODE:
|
|
342
|
+
status = 'Failed to decode request'
|
|
343
|
+
elif code == motorcortex_msg.SUB_LIST_IS_FULL:
|
|
344
|
+
status = 'Failed to subscribe, subscription list is full'
|
|
345
|
+
elif code == motorcortex_msg.WRONG_PARAMETER_PATH:
|
|
346
|
+
status = 'Failed to find parameter'
|
|
347
|
+
elif code == motorcortex_msg.FAILED_TO_SET_REQUESTED_FRQ:
|
|
348
|
+
status = 'Failed to set requested frequency'
|
|
349
|
+
elif code == motorcortex_msg.FAILED_TO_OPEN_FILE:
|
|
350
|
+
status = 'Failed to open file'
|
|
351
|
+
elif code == motorcortex_msg.READ_ONLY_MODE:
|
|
352
|
+
status = 'Logged in, read-only mode'
|
|
353
|
+
elif code == motorcortex_msg.WRONG_PASSWORD:
|
|
354
|
+
status = 'Wrong login or password'
|
|
355
|
+
elif code == motorcortex_msg.USER_NOT_LOGGED_IN:
|
|
356
|
+
status = 'Operation is not permitted, user is not logged in'
|
|
357
|
+
elif code == motorcortex_msg.PERMISSION_DENIED:
|
|
358
|
+
status = 'Operation is not permitted, user has no rights'
|
|
359
|
+
|
|
360
|
+
status += str(' (%s)' % hex(code))
|
|
361
|
+
|
|
362
|
+
return status
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Developer: Alexey Zakharov (alexey.zakharov@vectioneer.com)
|
|
3
|
+
# All rights reserved. Copyright (c) 2026 VECTIONEER.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
"""Connection-state enum + pure state-transition helpers.
|
|
7
|
+
|
|
8
|
+
``ConnectionState`` was historically defined in ``motorcortex.request`` and
|
|
9
|
+
is still re-exported from there (and from ``motorcortex``) for backward
|
|
10
|
+
compatibility. It lives here so pure helpers can depend on it without
|
|
11
|
+
dragging the whole ``request`` module — which would cause a circular
|
|
12
|
+
import.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConnectionState(Enum):
|
|
21
|
+
"""Enumeration of connection states.
|
|
22
|
+
|
|
23
|
+
- CONNECTING: Connection is being established.
|
|
24
|
+
- CONNECTION_OK: Connection is successfully established.
|
|
25
|
+
- CONNECTION_LOST: Connection was lost.
|
|
26
|
+
- CONNECTION_FAILED: Connection attempt failed.
|
|
27
|
+
- DISCONNECTING: Connection is being closed.
|
|
28
|
+
- DISCONNECTED: Connection is closed.
|
|
29
|
+
"""
|
|
30
|
+
CONNECTING = 0
|
|
31
|
+
CONNECTION_OK = 1
|
|
32
|
+
CONNECTION_LOST = 2
|
|
33
|
+
CONNECTION_FAILED = 3
|
|
34
|
+
DISCONNECTING = 4
|
|
35
|
+
DISCONNECTED = 5
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def next_state_after_pipe_remove(current: ConnectionState) -> ConnectionState:
|
|
39
|
+
"""Map the current state to the one we enter when the remote pipe goes away.
|
|
40
|
+
|
|
41
|
+
nng fires its post-remove callback when a peer socket is torn down, but
|
|
42
|
+
the callback itself can't tell *why*: we might have called
|
|
43
|
+
``close()`` (clean shutdown) or the remote might have died (lost /
|
|
44
|
+
failed). The only disambiguator is the state we were already in.
|
|
45
|
+
|
|
46
|
+
Transitions:
|
|
47
|
+
DISCONNECTING → DISCONNECTED (clean close we initiated)
|
|
48
|
+
CONNECTING → CONNECTION_FAILED (never finished handshake)
|
|
49
|
+
CONNECTION_OK → CONNECTION_LOST (remote went away mid-session)
|
|
50
|
+
anything else → unchanged (idempotent / re-entry safe)
|
|
51
|
+
"""
|
|
52
|
+
if current == ConnectionState.DISCONNECTING:
|
|
53
|
+
return ConnectionState.DISCONNECTED
|
|
54
|
+
if current == ConnectionState.CONNECTING:
|
|
55
|
+
return ConnectionState.CONNECTION_FAILED
|
|
56
|
+
if current == ConnectionState.CONNECTION_OK:
|
|
57
|
+
return ConnectionState.CONNECTION_LOST
|
|
58
|
+
return current
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Developer: Alexey Zakharov (alexey.zakharov@vectioneer.com)
|
|
3
|
+
# All rights reserved. Copyright (c) 2026 VECTIONEER.
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
"""Pure protobuf message builders used by :class:`motorcortex.Request`.
|
|
7
|
+
|
|
8
|
+
These used to be private static methods on ``Request``. Moving them to
|
|
9
|
+
a free-function module so they can be unit-tested without spinning up a
|
|
10
|
+
socket or stubbing the class.
|
|
11
|
+
|
|
12
|
+
Everything here is side-effect-free w.r.t. the wire — the builders just
|
|
13
|
+
produce protobuf messages that the caller later hands to
|
|
14
|
+
``Request.send()``. They may log an error when no encoder resolves, but
|
|
15
|
+
they do not touch sockets, files, or global state.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
from motorcortex.setup_logger import logger
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from motorcortex.message_types import MessageTypes
|
|
26
|
+
from motorcortex.parameter_tree import ParameterTree
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_encoder(
|
|
30
|
+
path: str,
|
|
31
|
+
type_name: Optional[str],
|
|
32
|
+
protobuf_types: "MessageTypes",
|
|
33
|
+
parameter_tree: "ParameterTree",
|
|
34
|
+
) -> Any:
|
|
35
|
+
"""Find the encoder for ``path`` — by explicit ``type_name`` first,
|
|
36
|
+
otherwise by hash looked up in the parameter tree.
|
|
37
|
+
|
|
38
|
+
Returns ``None`` when neither resolution succeeds. Callers (see
|
|
39
|
+
``build_set_parameter_msg`` / ``build_overwrite_parameter_msg``)
|
|
40
|
+
log and fall back to sending an empty-value message, which the
|
|
41
|
+
server rejects with a clean status code — beats raising
|
|
42
|
+
``AttributeError`` at the client on ``.encode()``.
|
|
43
|
+
"""
|
|
44
|
+
if type_name:
|
|
45
|
+
return protobuf_types.createType(type_name)
|
|
46
|
+
try:
|
|
47
|
+
type_id = parameter_tree.getDataType(path)
|
|
48
|
+
except KeyError:
|
|
49
|
+
return None
|
|
50
|
+
return protobuf_types.getTypeByHash(type_id)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_set_parameter_msg(
|
|
54
|
+
path: str,
|
|
55
|
+
value: Any,
|
|
56
|
+
type_name: Optional[str],
|
|
57
|
+
protobuf_types: "MessageTypes",
|
|
58
|
+
parameter_tree: "ParameterTree",
|
|
59
|
+
) -> Any:
|
|
60
|
+
"""``SetParameterMsg`` for a single parameter (whole-value write).
|
|
61
|
+
|
|
62
|
+
When the encoder cannot be resolved (unknown path, no ``type_name``),
|
|
63
|
+
the message is built with an empty ``value`` byte string and sent as-is
|
|
64
|
+
— the server rejects it with a proper error status, which beats
|
|
65
|
+
raising ``AttributeError`` at the client.
|
|
66
|
+
"""
|
|
67
|
+
param_value = _resolve_encoder(path, type_name, protobuf_types, parameter_tree)
|
|
68
|
+
msg = protobuf_types.createType("motorcortex.SetParameterMsg")
|
|
69
|
+
msg.path = path
|
|
70
|
+
if param_value is None:
|
|
71
|
+
logger.error("No encoder for path %s (type=%s) — sending empty value", path, type_name)
|
|
72
|
+
msg.value = b""
|
|
73
|
+
else:
|
|
74
|
+
msg.value = param_value.encode(value)
|
|
75
|
+
return msg
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_set_parameter_with_offset_msg(
|
|
79
|
+
offset: int,
|
|
80
|
+
length: int,
|
|
81
|
+
path: str,
|
|
82
|
+
value: Any,
|
|
83
|
+
type_name: Optional[str],
|
|
84
|
+
protobuf_types: "MessageTypes",
|
|
85
|
+
parameter_tree: "ParameterTree",
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""``SetParameterMsg`` with an ``offset`` + ``length`` slice window.
|
|
88
|
+
|
|
89
|
+
Negative offsets are clamped to zero. ``length == 0`` means "write
|
|
90
|
+
all of ``value``" and is inferred from ``len(value)`` (or 1 for
|
|
91
|
+
scalars). Unknown encoder → empty ``value``, see
|
|
92
|
+
:func:`build_set_parameter_msg`.
|
|
93
|
+
"""
|
|
94
|
+
param_value = _resolve_encoder(path, type_name, protobuf_types, parameter_tree)
|
|
95
|
+
|
|
96
|
+
if offset < 0:
|
|
97
|
+
offset = 0
|
|
98
|
+
if length < 0:
|
|
99
|
+
length = 0
|
|
100
|
+
if length == 0:
|
|
101
|
+
length = len(value) if hasattr(value, "__len__") else 1
|
|
102
|
+
|
|
103
|
+
msg = protobuf_types.createType("motorcortex.SetParameterMsg")
|
|
104
|
+
msg.offset.type = 1
|
|
105
|
+
msg.offset.offset = offset
|
|
106
|
+
msg.offset.length = length
|
|
107
|
+
msg.path = path
|
|
108
|
+
if param_value is None:
|
|
109
|
+
logger.error("No encoder for path %s (type=%s) — sending empty value", path, type_name)
|
|
110
|
+
msg.value = b""
|
|
111
|
+
else:
|
|
112
|
+
msg.value = param_value.encode(value)
|
|
113
|
+
return msg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_get_parameter_msg(
|
|
117
|
+
path: str,
|
|
118
|
+
protobuf_types: "MessageTypes",
|
|
119
|
+
) -> Any:
|
|
120
|
+
"""``GetParameterMsg`` — trivial, just carries the path."""
|
|
121
|
+
msg = protobuf_types.createType("motorcortex.GetParameterMsg")
|
|
122
|
+
msg.path = path
|
|
123
|
+
return msg
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_overwrite_parameter_msg(
|
|
127
|
+
path: str,
|
|
128
|
+
value: Any,
|
|
129
|
+
activate: bool,
|
|
130
|
+
type_name: Optional[str],
|
|
131
|
+
protobuf_types: "MessageTypes",
|
|
132
|
+
parameter_tree: "ParameterTree",
|
|
133
|
+
) -> Any:
|
|
134
|
+
"""``OverwriteParameterMsg`` — pins a value until release or deactivate.
|
|
135
|
+
|
|
136
|
+
Unknown encoder → empty ``value``, see :func:`build_set_parameter_msg`.
|
|
137
|
+
"""
|
|
138
|
+
param_value = _resolve_encoder(path, type_name, protobuf_types, parameter_tree)
|
|
139
|
+
msg = protobuf_types.createType("motorcortex.OverwriteParameterMsg")
|
|
140
|
+
msg.path = path
|
|
141
|
+
msg.activate = activate
|
|
142
|
+
if param_value is None:
|
|
143
|
+
logger.error("No encoder for path %s (type=%s) — sending empty value", path, type_name)
|
|
144
|
+
msg.value = b""
|
|
145
|
+
else:
|
|
146
|
+
msg.value = param_value.encode(value)
|
|
147
|
+
return msg
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_release_parameter_msg(
|
|
151
|
+
path: str,
|
|
152
|
+
protobuf_types: "MessageTypes",
|
|
153
|
+
) -> Any:
|
|
154
|
+
"""``ReleaseParameterMsg`` — drop any active overwrite on ``path``."""
|
|
155
|
+
msg = protobuf_types.createType("motorcortex.ReleaseParameterMsg")
|
|
156
|
+
msg.path = path
|
|
157
|
+
return msg
|