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
|
@@ -0,0 +1,414 @@
|
|
|
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
|
+
import warnings
|
|
9
|
+
from collections import namedtuple
|
|
10
|
+
from struct import unpack_from
|
|
11
|
+
from concurrent.futures import Future
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable, List, Optional, Union
|
|
14
|
+
from motorcortex.timespec import Timespec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Default wait when the caller passes neither ``timeout_sec`` nor
|
|
18
|
+
# ``timeout_ms``. Kept at 1 s for backwards compatibility with the
|
|
19
|
+
# pre-T1.2 signature ``def get(self, timeout_sec: float = 1.0)``.
|
|
20
|
+
_DEFAULT_TIMEOUT_SEC = 1.0
|
|
21
|
+
|
|
22
|
+
# timespec = namedtuple('timespec', 'sec, nsec')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Parameter = namedtuple('Parameter', 'timestamp, value')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Subscription(object):
|
|
29
|
+
"""
|
|
30
|
+
Represents a subscription to a group of parameters in Motorcortex.
|
|
31
|
+
|
|
32
|
+
The Subscription class allows you to:
|
|
33
|
+
- Access the latest values and timestamps for a group of parameters.
|
|
34
|
+
- Poll for updates or use observer callbacks for real-time notifications.
|
|
35
|
+
- Chain asynchronous operations using `then` and `catch` (promise-like interface).
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
group_alias (str): Alias for the parameter group.
|
|
39
|
+
protobuf_types (Any): Protobuf type definitions.
|
|
40
|
+
frq_divider (int): Frequency divider for the group.
|
|
41
|
+
pool (Any): Thread or process pool for observer callbacks.
|
|
42
|
+
|
|
43
|
+
Methods:
|
|
44
|
+
id() -> int
|
|
45
|
+
Returns the subscription identifier.
|
|
46
|
+
|
|
47
|
+
alias() -> str
|
|
48
|
+
Returns the group alias.
|
|
49
|
+
|
|
50
|
+
frqDivider() -> int
|
|
51
|
+
Returns the frequency divider of the group.
|
|
52
|
+
|
|
53
|
+
read() -> Optional[List[Parameter]]
|
|
54
|
+
Returns the latest values of the parameters in the group.
|
|
55
|
+
|
|
56
|
+
layout() -> Optional[List[str]]
|
|
57
|
+
Returns the ordered list of parameter paths in the group.
|
|
58
|
+
|
|
59
|
+
done() -> bool
|
|
60
|
+
Returns True if the subscription is finished or cancelled.
|
|
61
|
+
|
|
62
|
+
get(timeout_sec: float = 1.0) -> Optional[Any]
|
|
63
|
+
Waits for the subscription to complete, returns the result or None on timeout.
|
|
64
|
+
|
|
65
|
+
then(subscribed_clb: Callable[[Any], None]) -> Subscription
|
|
66
|
+
Registers a callback for successful subscription completion.
|
|
67
|
+
|
|
68
|
+
catch(failed: Callable[[], None]) -> Subscription
|
|
69
|
+
Registers a callback for subscription failure.
|
|
70
|
+
|
|
71
|
+
notify(observer_list: Union[Callable, List[Callable]]) -> None
|
|
72
|
+
Registers observer(s) to be notified on every group update.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
>>> # Make sure you have a valid connection
|
|
76
|
+
>>> subscription = sub.subscribe(paths, "group1", 100)
|
|
77
|
+
>>> result = subscription.get()
|
|
78
|
+
>>> if result is not None and result.status == motorcortex.OK:
|
|
79
|
+
... print(f"Subscription successful, layout: {subscription.layout()}")
|
|
80
|
+
... else:
|
|
81
|
+
... print(f"Subscription failed. Check parameter paths: {paths}")
|
|
82
|
+
... sub.close()
|
|
83
|
+
... exit()
|
|
84
|
+
>>> # Use promise-like interface
|
|
85
|
+
>>> subscription.then(lambda res: print("Subscribed:", res)).catch(lambda: print("Failed"))
|
|
86
|
+
>>> # Use observer for real-time updates
|
|
87
|
+
>>> def on_update(parameters):
|
|
88
|
+
... for param in parameters:
|
|
89
|
+
... timestamp = param.timestamp.sec + param.timestamp.nsec * 1e-9
|
|
90
|
+
... print(f"Update: {timestamp:.6f}, {param.value}")
|
|
91
|
+
>>> subscription.notify(on_update)
|
|
92
|
+
>>> print("Waiting for parameter updates...")
|
|
93
|
+
>>> import time
|
|
94
|
+
>>> while True:
|
|
95
|
+
... time.sleep(1)
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
group_alias: str,
|
|
101
|
+
protobuf_types: Any,
|
|
102
|
+
frq_divider: int,
|
|
103
|
+
pool: Any
|
|
104
|
+
) -> None:
|
|
105
|
+
# ``_info`` is the server's reply message (typed as ``Any`` since
|
|
106
|
+
# it's an opaque protobuf). Starts ``None``; populated by
|
|
107
|
+
# ``_complete()`` once the subscribe handshake lands. Use sites
|
|
108
|
+
# only fire after ``_is_complete`` is True, so the value is safe
|
|
109
|
+
# to access without a guard — the typed-as-Any gives up the mypy
|
|
110
|
+
# Optional check, which is fine here (the field's real type is
|
|
111
|
+
# dynamic anyway).
|
|
112
|
+
self._info: Any = None
|
|
113
|
+
self._group_alias: str = group_alias
|
|
114
|
+
self._protobuf_types: Any = protobuf_types
|
|
115
|
+
self._decoder: List[Any] = []
|
|
116
|
+
self._future: Future = Future()
|
|
117
|
+
# ``_values`` / ``_layout`` default to empty lists so indexing
|
|
118
|
+
# and slicing are type-safe. ``read()`` / ``layout()`` still
|
|
119
|
+
# return ``Optional`` by gating on ``_is_complete``, preserving
|
|
120
|
+
# the public "None until ready" contract.
|
|
121
|
+
self._values: List["Parameter"] = []
|
|
122
|
+
self._layout: List[str] = []
|
|
123
|
+
self._is_complete: bool = False
|
|
124
|
+
self._observer_list: List[Callable[[List["Parameter"]], None]] = []
|
|
125
|
+
self._pool: Any = pool
|
|
126
|
+
self._frq_divider: int = frq_divider
|
|
127
|
+
self._dropped_frames: int = 0
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return (
|
|
131
|
+
f"Subscription(alias={self._group_alias!r}, "
|
|
132
|
+
f"frq_divider={self._frq_divider!r}, "
|
|
133
|
+
f"layout_size={len(self._layout)}, "
|
|
134
|
+
f"done={self._future.done()}, "
|
|
135
|
+
f"dropped={self._dropped_frames})"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def id(self) -> int:
|
|
139
|
+
"""
|
|
140
|
+
Returns:
|
|
141
|
+
int: subscription identifier
|
|
142
|
+
"""
|
|
143
|
+
return self._info.id
|
|
144
|
+
|
|
145
|
+
def alias(self) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Returns:
|
|
148
|
+
str: group alias
|
|
149
|
+
"""
|
|
150
|
+
return self._group_alias
|
|
151
|
+
|
|
152
|
+
def frqDivider(self) -> int:
|
|
153
|
+
"""
|
|
154
|
+
Returns:
|
|
155
|
+
int: frequency divider of the group
|
|
156
|
+
"""
|
|
157
|
+
return self._frq_divider
|
|
158
|
+
|
|
159
|
+
def read(self) -> Optional[List["Parameter"]]:
|
|
160
|
+
"""Read the latest values of the parameters in the group.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
list of Parameter: list of parameters
|
|
164
|
+
"""
|
|
165
|
+
return self._values[:] if self._is_complete else None
|
|
166
|
+
|
|
167
|
+
def layout(self) -> Optional[List[str]]:
|
|
168
|
+
"""Get a layout of the group.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
list of str: ordered list of the parameters in the group
|
|
172
|
+
"""
|
|
173
|
+
return self._layout[:] if self._is_complete else None
|
|
174
|
+
|
|
175
|
+
def done(self) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Returns:
|
|
178
|
+
bool: True if the call was successfully canceled or finished running.
|
|
179
|
+
|
|
180
|
+
Examples:
|
|
181
|
+
>>> subscription = sub.subscribe("root/logger/logOut", "log")
|
|
182
|
+
>>> while not subscription.done():
|
|
183
|
+
>>> time.sleep(0.1)
|
|
184
|
+
"""
|
|
185
|
+
return self._future.done()
|
|
186
|
+
|
|
187
|
+
def get(
|
|
188
|
+
self,
|
|
189
|
+
timeout_sec: Optional[float] = None,
|
|
190
|
+
*,
|
|
191
|
+
timeout_ms: Optional[int] = None,
|
|
192
|
+
) -> Optional[Any]:
|
|
193
|
+
"""Wait for the subscribe handshake to resolve.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
timeout_sec: **Deprecated** (since 0.28.0, removal
|
|
197
|
+
planned for 2.0). Legacy seconds-based timeout,
|
|
198
|
+
kept as the first positional arg so existing code
|
|
199
|
+
(``subscription.get(5.0)``) keeps working. Prefer
|
|
200
|
+
``timeout_ms`` for new code — the rest of the
|
|
201
|
+
library is ms-based (see ARCHITECTURE.md §2a).
|
|
202
|
+
Explicit use emits a ``DeprecationWarning``.
|
|
203
|
+
timeout_ms: Timeout in milliseconds (keyword-only).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The subscribe-reply message on success, ``None`` on timeout.
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
>>> subscription = sub.subscribe("root/logger/logOut", "log")
|
|
210
|
+
>>> msg = subscription.get(timeout_ms=2000)
|
|
211
|
+
"""
|
|
212
|
+
if timeout_ms is not None:
|
|
213
|
+
return self._future.result(timeout_ms / 1000.0)
|
|
214
|
+
|
|
215
|
+
if timeout_sec is not None:
|
|
216
|
+
warnings.warn(
|
|
217
|
+
"Subscription.get(timeout_sec=...) is deprecated; "
|
|
218
|
+
"use timeout_ms= instead. timeout_sec will be removed "
|
|
219
|
+
"in motorcortex-python 2.0.",
|
|
220
|
+
DeprecationWarning,
|
|
221
|
+
stacklevel=2,
|
|
222
|
+
)
|
|
223
|
+
return self._future.result(timeout_sec)
|
|
224
|
+
|
|
225
|
+
return self._future.result(_DEFAULT_TIMEOUT_SEC)
|
|
226
|
+
|
|
227
|
+
def then(self, subscribed_clb: Callable[[Any], None]) -> "Subscription":
|
|
228
|
+
"""JavaScript-like promise, which is resolved when the subscription is completed.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
subscribed_clb: callback which is resolved when the subscription is completed.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
self pointer to add 'catch' callback
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
>>> subscription = sub.subscribe("root/logger/logOut", "log")
|
|
238
|
+
>>> subscription.then(lambda val: print("got: %s"%val)).catch(lambda d: print("failed"))
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
self._future.add_done_callback(
|
|
242
|
+
lambda msg: subscribed_clb(msg.result()) if msg.result() else None
|
|
243
|
+
)
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def catch(self, failed: Callable[[], None]) -> "Subscription":
|
|
247
|
+
"""JavaScript-like promise, which is resolved when subscription has failed.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
failed: callback which is resolved when the subscription has failed
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
self pointer to add 'then' callback
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
>>> subscription = sub.subscribe("root/logger/logOut", "log")
|
|
257
|
+
>>> subscription.catch(lambda d: print("failed")).then(lambda val: print("got: %s"%val))
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
self._future.add_done_callback(
|
|
261
|
+
lambda msg: failed() if not msg.result() else None
|
|
262
|
+
)
|
|
263
|
+
return self
|
|
264
|
+
|
|
265
|
+
def notify(self, observer_list: Union[
|
|
266
|
+
Callable[[List["Parameter"]], None], List[Callable[[List["Parameter"]], None]]]) -> None:
|
|
267
|
+
"""Set an observer, which is notified on every group update.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
observer_list: a callback function (or list of callback functions)
|
|
271
|
+
to notify when new values are available
|
|
272
|
+
|
|
273
|
+
Examples:
|
|
274
|
+
>>> def update(parameters):
|
|
275
|
+
>>> print(parameters) #list of Parameter tuples
|
|
276
|
+
>>> ...
|
|
277
|
+
>>> data_sub.notify(update)
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
# Narrow the union properly so mypy can pick the right branch
|
|
281
|
+
# — ``type(x) is list`` is opaque to the type checker.
|
|
282
|
+
if isinstance(observer_list, list):
|
|
283
|
+
self._observer_list = observer_list
|
|
284
|
+
else:
|
|
285
|
+
self._observer_list = [observer_list]
|
|
286
|
+
|
|
287
|
+
def _complete(self, msg: Any) -> bool:
|
|
288
|
+
"""Marks the subscription as complete and initializes decoders.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
msg: Protobuf message containing subscription info.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
bool: True if completed successfully, False otherwise.
|
|
295
|
+
"""
|
|
296
|
+
self._decoder = []
|
|
297
|
+
self._values = []
|
|
298
|
+
self._layout = []
|
|
299
|
+
if msg.status == 0:
|
|
300
|
+
self._info = msg
|
|
301
|
+
for param in msg.params:
|
|
302
|
+
data_type = param.info.data_type
|
|
303
|
+
self._decoder.append(self._protobuf_types.getTypeByHash(data_type))
|
|
304
|
+
self._values.append(Parameter(Timespec(0, 0), [0] * param.info.number_of_elements))
|
|
305
|
+
self._layout.append(param.info.path)
|
|
306
|
+
|
|
307
|
+
self._is_complete = True
|
|
308
|
+
self._future.set_result(msg)
|
|
309
|
+
return True
|
|
310
|
+
else:
|
|
311
|
+
self._failed()
|
|
312
|
+
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
def _updateId(self, new_id: int) -> None:
|
|
316
|
+
"""Updates the subscription identifier.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
new_id: New subscription identifier.
|
|
320
|
+
"""
|
|
321
|
+
self._info.id = new_id
|
|
322
|
+
|
|
323
|
+
def _decode_frame(self, counter: int, value_bytes: bytes, timestamp: Timespec) -> None:
|
|
324
|
+
"""Decode one parameter's value bytes and store the result.
|
|
325
|
+
|
|
326
|
+
Shared tail of both protocol paths — the only difference
|
|
327
|
+
between protocol 0 and protocol 1 is where ``timestamp`` and
|
|
328
|
+
``value_bytes`` come from; from here on the work is identical.
|
|
329
|
+
"""
|
|
330
|
+
param = self._info.params[counter]
|
|
331
|
+
value = self._decoder[counter].decode(
|
|
332
|
+
value_bytes, len(value_bytes) / param.info.data_size,
|
|
333
|
+
)
|
|
334
|
+
self._values[counter] = Parameter(timestamp, value)
|
|
335
|
+
|
|
336
|
+
def _notify_observers(self) -> None:
|
|
337
|
+
"""Snapshot current values and fan out to observers via the pool."""
|
|
338
|
+
if not self._observer_list:
|
|
339
|
+
return
|
|
340
|
+
snapshot = self._values[:]
|
|
341
|
+
for observer in self._observer_list:
|
|
342
|
+
self._pool.submit(observer, snapshot)
|
|
343
|
+
|
|
344
|
+
def _updateProtocol0(self, sub_msg: bytes, length: int) -> None:
|
|
345
|
+
"""Updates the subscription values using protocol version 0
|
|
346
|
+
(per-param 16-byte timestamp prepended to every payload).
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
sub_msg: Subscription message bytes.
|
|
350
|
+
length: Length of the subscription message.
|
|
351
|
+
"""
|
|
352
|
+
if not sub_msg:
|
|
353
|
+
return
|
|
354
|
+
n_params = len(self._info.params)
|
|
355
|
+
for counter, param in enumerate(self._info.params):
|
|
356
|
+
offset = param.offset
|
|
357
|
+
size = param.size
|
|
358
|
+
|
|
359
|
+
# the last element in the group may have a variable size
|
|
360
|
+
if counter + 1 == n_params and (offset + size) > length:
|
|
361
|
+
size = length - offset
|
|
362
|
+
|
|
363
|
+
# Each frame is 16B timestamp + payload. Require both the
|
|
364
|
+
# full frame AND enough bytes for the timestamp itself —
|
|
365
|
+
# size can be trimmed to 0 by the last-param branch above,
|
|
366
|
+
# in which case there's nothing to unpack.
|
|
367
|
+
if size < 16 or (offset + size) > length:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
timestamp = Timespec.from_iterable(unpack_from('QQ', sub_msg, offset))
|
|
371
|
+
self._decode_frame(counter, sub_msg[offset + 16: offset + size], timestamp)
|
|
372
|
+
|
|
373
|
+
self._notify_observers()
|
|
374
|
+
|
|
375
|
+
def _updateProtocol1(self, sub_msg: bytes, length: int) -> None:
|
|
376
|
+
"""Updates the subscription values using protocol version 1
|
|
377
|
+
(single leading 16-byte timestamp, shared by all params)."""
|
|
378
|
+
if not sub_msg or length < 16:
|
|
379
|
+
return
|
|
380
|
+
timestamp = Timespec.from_iterable(unpack_from('QQ', sub_msg, 0))
|
|
381
|
+
n_params = len(self._info.params)
|
|
382
|
+
for counter, param in enumerate(self._info.params):
|
|
383
|
+
offset = param.offset + 16
|
|
384
|
+
size = param.size
|
|
385
|
+
|
|
386
|
+
# the last element in the group may have a variable size
|
|
387
|
+
if counter + 1 == n_params and (offset + size) > length:
|
|
388
|
+
size = length - offset
|
|
389
|
+
|
|
390
|
+
if (offset + size) > length:
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
self._decode_frame(counter, sub_msg[offset: offset + size], timestamp)
|
|
394
|
+
|
|
395
|
+
self._notify_observers()
|
|
396
|
+
|
|
397
|
+
def _failed(self) -> None:
|
|
398
|
+
self._future.set_result(None)
|
|
399
|
+
|
|
400
|
+
def droppedFrameCount(self) -> int:
|
|
401
|
+
"""Number of wire frames dropped by the dispatcher for this group.
|
|
402
|
+
|
|
403
|
+
Incremented when a frame carries an unrecognised protocol version
|
|
404
|
+
or is otherwise undeliverable. Exposed for diagnostics — a
|
|
405
|
+
subscription that is silently missing updates will show a rising
|
|
406
|
+
count here while its callback stays quiet.
|
|
407
|
+
"""
|
|
408
|
+
return self._dropped_frames
|
|
409
|
+
|
|
410
|
+
def _recordDroppedFrame(self) -> None:
|
|
411
|
+
"""Called by the dispatcher when a frame for this subscription
|
|
412
|
+
can't be decoded. Keeps the count visible to ``droppedFrameCount()``.
|
|
413
|
+
"""
|
|
414
|
+
self._dropped_frames += 1
|
motorcortex/timespec.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
class Timespec:
|
|
2
|
+
"""
|
|
3
|
+
Represents a timestamp with seconds and nanoseconds.
|
|
4
|
+
|
|
5
|
+
Provides conversion properties for seconds, milliseconds, microseconds, and nanoseconds.
|
|
6
|
+
Prefer using this class for new code instead of standalone functions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, sec: int, nsec: int):
|
|
10
|
+
self._sec = sec
|
|
11
|
+
self._nsec = nsec
|
|
12
|
+
|
|
13
|
+
def __eq__(self, other: object) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Compare two Timespec objects for equality.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
other (object): Another Timespec instance.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
bool: True if both timestamps are equal, False otherwise.
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(other, Timespec):
|
|
24
|
+
return NotImplemented
|
|
25
|
+
return self._sec == other._sec and self._nsec == other._nsec
|
|
26
|
+
|
|
27
|
+
def __lt__(self, other: "Timespec") -> bool:
|
|
28
|
+
return (self._sec, self._nsec) < (other._sec, other._nsec)
|
|
29
|
+
|
|
30
|
+
def __le__(self, other: "Timespec") -> bool:
|
|
31
|
+
return (self._sec, self._nsec) <= (other._sec, other._nsec)
|
|
32
|
+
|
|
33
|
+
def __add__(self, other: "Timespec") -> "Timespec":
|
|
34
|
+
total_nsec = self._nsec + other._nsec
|
|
35
|
+
total_sec = self._sec + other._sec + total_nsec // 1_000_000_000
|
|
36
|
+
total_nsec = total_nsec % 1_000_000_000
|
|
37
|
+
return Timespec(total_sec, total_nsec)
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return f"{self._sec}s {self._nsec}ns"
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"Timespec(sec={self._sec}, nsec={self._nsec})"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_iterable(cls, iterable):
|
|
47
|
+
"""
|
|
48
|
+
Create a Timespec instance from an iterable containing seconds and nanoseconds.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
iterable: An iterable with two elements: (sec, nsec).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Timespec: A new Timespec instance.
|
|
55
|
+
"""
|
|
56
|
+
sec, nsec = iterable
|
|
57
|
+
return cls(sec, nsec)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def from_msec(msec: float) -> "Timespec":
|
|
61
|
+
"""
|
|
62
|
+
Create a Timespec instance from milliseconds.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
msec (float): Time in milliseconds.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Timespec: A new Timespec instance.
|
|
69
|
+
"""
|
|
70
|
+
sec = int(msec // 1000)
|
|
71
|
+
nsec = int((msec % 1000) * 1_000_000)
|
|
72
|
+
return Timespec(sec, nsec)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def sec(self) -> int:
|
|
76
|
+
"""
|
|
77
|
+
Returns the seconds component of the timestamp.
|
|
78
|
+
"""
|
|
79
|
+
return self._sec
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def nsec(self) -> int:
|
|
83
|
+
"""
|
|
84
|
+
Returns the nanoseconds component of the timestamp.
|
|
85
|
+
"""
|
|
86
|
+
return self._nsec
|
|
87
|
+
|
|
88
|
+
def to_tuple(self) -> tuple[int, int]:
|
|
89
|
+
"""
|
|
90
|
+
Returns the timestamp as a (sec, nsec) tuple.
|
|
91
|
+
"""
|
|
92
|
+
return self._sec, self._nsec
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def compare_timespec(timestamp1: Timespec, timestamp2: Timespec) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Compare two timestamps.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
timestamp1 (timespec): First timestamp to compare.
|
|
101
|
+
timestamp2 (timespec): Second timestamp to compare.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
bool: True if both timestamps are equal, False otherwise.
|
|
105
|
+
|
|
106
|
+
Note:
|
|
107
|
+
For new code, use Timespec.__eq__ instead.
|
|
108
|
+
"""
|
|
109
|
+
return True if (timestamp1.sec == timestamp2.sec) and (timestamp1.nsec == timestamp2.nsec) else False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def timespec_to_sec(timestamp: Timespec) -> float:
|
|
113
|
+
"""
|
|
114
|
+
Convert a timestamp to seconds.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
timestamp (timespec): Timestamp to convert.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
float: Time in seconds.
|
|
121
|
+
|
|
122
|
+
Note:
|
|
123
|
+
For new code, use Timespec.sec_value instead.
|
|
124
|
+
"""
|
|
125
|
+
return timestamp.sec + timestamp.nsec / 1000000000.0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def timespec_to_msec(timestamp: Timespec) -> float:
|
|
129
|
+
"""
|
|
130
|
+
Convert a timestamp to milliseconds.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
timestamp (timespec): Timestamp to convert.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
float: Time in milliseconds.
|
|
137
|
+
|
|
138
|
+
Note:
|
|
139
|
+
For new code, use Timespec.msec instead.
|
|
140
|
+
"""
|
|
141
|
+
return timestamp.sec * 1000 + timestamp.nsec / 1000000
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def timespec_to_usec(timestamp: Timespec) -> float:
|
|
145
|
+
"""
|
|
146
|
+
Convert a timestamp to microseconds.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
timestamp (timespec): Timestamp to convert.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
float: Time in microseconds.
|
|
153
|
+
|
|
154
|
+
Note:
|
|
155
|
+
For new code, use Timespec.usec instead.
|
|
156
|
+
"""
|
|
157
|
+
return timestamp.sec * 1000000 + timestamp.nsec / 1000
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def timespec_to_nsec(timestamp: Timespec) -> int:
|
|
161
|
+
"""
|
|
162
|
+
Convert a timestamp to nanoseconds.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
timestamp (timespec): Timestamp to convert.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
int: Time in nanoseconds.
|
|
169
|
+
|
|
170
|
+
Note:
|
|
171
|
+
For new code, use Timespec.nsec_value instead.
|
|
172
|
+
"""
|
|
173
|
+
return timestamp.sec * 1000000000 + timestamp.nsec
|
motorcortex/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.0rc1'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright 2018 Vectioneer B.V. <info@vectioneer.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
"motorcortex" is a trademark of Vectioneer (www.vectioneer.com)
|