motorcortex-python 0.25.5__tar.gz → 1.0.0rc1__tar.gz
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_python-1.0.0rc1/PKG-INFO +171 -0
- motorcortex_python-1.0.0rc1/README.md +146 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/__init__.py +116 -30
- motorcortex_python-1.0.0rc1/motorcortex/_connection_state.py +58 -0
- motorcortex_python-1.0.0rc1/motorcortex/_request_builders.py +157 -0
- motorcortex_python-1.0.0rc1/motorcortex/_request_utils.py +314 -0
- motorcortex_python-1.0.0rc1/motorcortex/_subscribe_dispatch.py +90 -0
- motorcortex_python-1.0.0rc1/motorcortex/exceptions.py +65 -0
- motorcortex_python-1.0.0rc1/motorcortex/init_threads.py +103 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/message_types.py +91 -76
- motorcortex_python-1.0.0rc1/motorcortex/motorcortex_hash.json +166 -0
- motorcortex_python-1.0.0rc1/motorcortex/motorcortex_pb2.py +105 -0
- motorcortex_python-1.0.0rc1/motorcortex/motorcortex_pb2.pyi +1961 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/nng_url.py +12 -6
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/parameter_tree.py +22 -13
- motorcortex_python-1.0.0rc1/motorcortex/py.typed +0 -0
- motorcortex_python-1.0.0rc1/motorcortex/reply.py +108 -0
- motorcortex_python-1.0.0rc1/motorcortex/request.py +668 -0
- motorcortex_python-1.0.0rc1/motorcortex/session.py +194 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/state_callback_handler.py +10 -5
- motorcortex_python-1.0.0rc1/motorcortex/subscribe.py +400 -0
- motorcortex_python-1.0.0rc1/motorcortex/subscription.py +414 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/timespec.py +3 -0
- motorcortex_python-1.0.0rc1/motorcortex/version.py +1 -0
- motorcortex_python-1.0.0rc1/motorcortex_python.egg-info/PKG-INFO +171 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex_python.egg-info/SOURCES.txt +8 -0
- motorcortex_python-1.0.0rc1/motorcortex_python.egg-info/requires.txt +2 -0
- motorcortex_python-1.0.0rc1/pyproject.toml +51 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/setup.py +21 -1
- motorcortex_python-0.25.5/PKG-INFO +0 -33
- motorcortex_python-0.25.5/README.md +0 -11
- motorcortex_python-0.25.5/motorcortex/init_threads.py +0 -20
- motorcortex_python-0.25.5/motorcortex/motorcortex_hash.json +0 -166
- motorcortex_python-0.25.5/motorcortex/motorcortex_pb2.py +0 -103
- motorcortex_python-0.25.5/motorcortex/reply.py +0 -74
- motorcortex_python-0.25.5/motorcortex/request.py +0 -980
- motorcortex_python-0.25.5/motorcortex/subscribe.py +0 -373
- motorcortex_python-0.25.5/motorcortex/subscription.py +0 -319
- motorcortex_python-0.25.5/motorcortex/version.py +0 -1
- motorcortex_python-0.25.5/motorcortex_python.egg-info/PKG-INFO +0 -33
- motorcortex_python-0.25.5/motorcortex_python.egg-info/requires.txt +0 -2
- motorcortex_python-0.25.5/pyproject.toml +0 -3
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/LICENSE +0 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/MANIFEST.in +0 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex/setup_logger.py +0 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex_python.egg-info/dependency_links.txt +0 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/motorcortex_python.egg-info/top_level.txt +0 -0
- {motorcortex_python-0.25.5 → motorcortex_python-1.0.0rc1}/setup.cfg +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: motorcortex-python
|
|
3
|
+
Version: 1.0.0rc1
|
|
4
|
+
Summary: Python bindings for Motorcortex Engine
|
|
5
|
+
Home-page: https://www.motorcortex.io
|
|
6
|
+
Author: Alexey Zakharov
|
|
7
|
+
Author-email: alexey.zakharov@vectioneer.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pynng<2,>=0.9.0
|
|
24
|
+
Requires-Dist: protobuf>=3.20
|
|
25
|
+
|
|
26
|
+
# motorcortex-python
|
|
27
|
+
|
|
28
|
+
[](https://git.vectioneer.com/pub/motorcortex-python/-/commits/master)
|
|
29
|
+
[](https://git.vectioneer.com/pub/motorcortex-python/-/commits/master)
|
|
30
|
+
[](https://pypi.org/project/motorcortex-python/)
|
|
31
|
+
[](https://pypi.org/project/motorcortex-python/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Python bindings for the [Motorcortex](https://www.motorcortex.io) real-time control engine. Connect to a Motorcortex server, read and write parameters, and stream live telemetry over a single TLS-secured websocket.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install motorcortex-python
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python ≥ 3.10. Runtime dependencies (`pynng`, `protobuf`) are installed automatically.
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
Recommended — `Session` context manager (close is automatic, even on exceptions):
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import motorcortex
|
|
50
|
+
|
|
51
|
+
with motorcortex.Session(
|
|
52
|
+
"wss://192.168.2.100",
|
|
53
|
+
certificate="mcx.cert.crt",
|
|
54
|
+
login="admin", password="admin",
|
|
55
|
+
timeout_ms=1000,
|
|
56
|
+
) as s:
|
|
57
|
+
# Read a single parameter
|
|
58
|
+
reply = s.req.getParameter("root/Control/dummyDouble").get()
|
|
59
|
+
print(reply.value)
|
|
60
|
+
|
|
61
|
+
# Write a parameter
|
|
62
|
+
s.req.setParameter("root/Control/dummyDouble", 3.14).get()
|
|
63
|
+
|
|
64
|
+
# Subscribe to a streamed update (every 10th cycle)
|
|
65
|
+
subscription = s.sub.subscribe(
|
|
66
|
+
["root/Control/dummyDouble"], "myGroup", frq_divider=10,
|
|
67
|
+
)
|
|
68
|
+
subscription.get()
|
|
69
|
+
subscription.notify(lambda result: print(result[0].value))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Explicit-objects form — the original API, still supported:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
types = motorcortex.MessageTypes()
|
|
76
|
+
tree = motorcortex.ParameterTree()
|
|
77
|
+
|
|
78
|
+
req, sub = motorcortex.connect(
|
|
79
|
+
"wss://192.168.2.100", types, tree,
|
|
80
|
+
certificate="mcx.cert.crt",
|
|
81
|
+
login="admin", password="admin",
|
|
82
|
+
)
|
|
83
|
+
try:
|
|
84
|
+
reply = req.getParameter("root/Control/dummyDouble").get()
|
|
85
|
+
print(reply.value)
|
|
86
|
+
finally:
|
|
87
|
+
sub.close()
|
|
88
|
+
req.close()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The URL grammar accepts IPv4, hostnames, and IPv6 literals both with and without explicit ports — e.g. `wss://host`, `wss://host:5568:5567`, `wss://[::1]`, `wss://[fe80::1]:5568:5567`.
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
- **[API reference — `docs/_index.md`](docs/_index.md)** — flat, method-by-method reference for every public class and function. Regenerated from the docstrings via `pydoc-markdown`.
|
|
96
|
+
- **[`ARCHITECTURE.md`](ARCHITECTURE.md)** — internals tour: module layout, connection lifecycle, subscribe frame format, parameter-tree cache, threading model, error contract, type conventions.
|
|
97
|
+
- **[`examples/README.md`](examples/README.md)** — runnable scripts (`quickstart.py`, `error_handling.py`) that demonstrate the canonical usage patterns.
|
|
98
|
+
- **[`CHANGELOG.md`](CHANGELOG.md)** — version history.
|
|
99
|
+
|
|
100
|
+
## Repository layout
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
motorcortex/ Python package
|
|
104
|
+
test/
|
|
105
|
+
unit/ Offline unit tests (no server)
|
|
106
|
+
integration/ Live tests against the vendored test_server
|
|
107
|
+
server/ Vendored C++ test_server (CMake project)
|
|
108
|
+
docs/ pydoc-markdown + stub generation scripts
|
|
109
|
+
benchmark/ Throughput scripts (not part of the test suite)
|
|
110
|
+
sandbox/ Ad-hoc repro scripts
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Testing
|
|
114
|
+
|
|
115
|
+
Unit tests run offline and require only the package itself:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install -e .
|
|
119
|
+
pip install "coverage[toml]>=7.4"
|
|
120
|
+
python -m unittest discover -s test/unit -t .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Integration tests spawn the vendored `test_server`. Build it once, then run the suite:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cmake -S test/server -B test/server/build -DCMAKE_BUILD_TYPE=Release
|
|
127
|
+
cmake --build test/server/build
|
|
128
|
+
|
|
129
|
+
python -m unittest discover -s test/integration -t .
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Coverage (line + branch). `pyproject.toml` sets `parallel = true`, so each
|
|
133
|
+
`coverage run` writes a per-process data shard; use `coverage combine` to
|
|
134
|
+
merge them before `coverage report`:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
coverage erase
|
|
138
|
+
coverage run -m unittest discover -s test/unit -t .
|
|
139
|
+
coverage run -m unittest discover -s test/integration -t .
|
|
140
|
+
coverage combine
|
|
141
|
+
coverage report
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
See [`test/README.md`](test/README.md) for the full testing walkthrough.
|
|
145
|
+
|
|
146
|
+
## Regenerating the API reference
|
|
147
|
+
|
|
148
|
+
[`docs/_index.md`](docs/_index.md) is committed and should be refreshed
|
|
149
|
+
before each release so the rendered reference matches the code at
|
|
150
|
+
the tag. The regen is two commands — `pydoc-markdown` first, then
|
|
151
|
+
`format_api.sh` to wrap `>>>` examples as fenced Python blocks and
|
|
152
|
+
prepend the front matter:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install pydoc-markdown
|
|
156
|
+
cd docs
|
|
157
|
+
pydoc-markdown pydoc-markdown.yml > _index.md
|
|
158
|
+
./format_api.sh
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The hook-based one-shot version was dropped — it races with the
|
|
162
|
+
shell redirect and silently loses output. See `docs/readme.md` and
|
|
163
|
+
the comment in `docs/pydoc-markdown.yml` for the full rationale.
|
|
164
|
+
|
|
165
|
+
## Release process
|
|
166
|
+
|
|
167
|
+
See [`PIPHOWTO.md`](PIPHOWTO.md) for PyPI release steps and [`CHANGELOG.md`](CHANGELOG.md) for version history.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# motorcortex-python
|
|
2
|
+
|
|
3
|
+
[](https://git.vectioneer.com/pub/motorcortex-python/-/commits/master)
|
|
4
|
+
[](https://git.vectioneer.com/pub/motorcortex-python/-/commits/master)
|
|
5
|
+
[](https://pypi.org/project/motorcortex-python/)
|
|
6
|
+
[](https://pypi.org/project/motorcortex-python/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Python bindings for the [Motorcortex](https://www.motorcortex.io) real-time control engine. Connect to a Motorcortex server, read and write parameters, and stream live telemetry over a single TLS-secured websocket.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install motorcortex-python
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python ≥ 3.10. Runtime dependencies (`pynng`, `protobuf`) are installed automatically.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Recommended — `Session` context manager (close is automatic, even on exceptions):
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import motorcortex
|
|
25
|
+
|
|
26
|
+
with motorcortex.Session(
|
|
27
|
+
"wss://192.168.2.100",
|
|
28
|
+
certificate="mcx.cert.crt",
|
|
29
|
+
login="admin", password="admin",
|
|
30
|
+
timeout_ms=1000,
|
|
31
|
+
) as s:
|
|
32
|
+
# Read a single parameter
|
|
33
|
+
reply = s.req.getParameter("root/Control/dummyDouble").get()
|
|
34
|
+
print(reply.value)
|
|
35
|
+
|
|
36
|
+
# Write a parameter
|
|
37
|
+
s.req.setParameter("root/Control/dummyDouble", 3.14).get()
|
|
38
|
+
|
|
39
|
+
# Subscribe to a streamed update (every 10th cycle)
|
|
40
|
+
subscription = s.sub.subscribe(
|
|
41
|
+
["root/Control/dummyDouble"], "myGroup", frq_divider=10,
|
|
42
|
+
)
|
|
43
|
+
subscription.get()
|
|
44
|
+
subscription.notify(lambda result: print(result[0].value))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Explicit-objects form — the original API, still supported:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
types = motorcortex.MessageTypes()
|
|
51
|
+
tree = motorcortex.ParameterTree()
|
|
52
|
+
|
|
53
|
+
req, sub = motorcortex.connect(
|
|
54
|
+
"wss://192.168.2.100", types, tree,
|
|
55
|
+
certificate="mcx.cert.crt",
|
|
56
|
+
login="admin", password="admin",
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
reply = req.getParameter("root/Control/dummyDouble").get()
|
|
60
|
+
print(reply.value)
|
|
61
|
+
finally:
|
|
62
|
+
sub.close()
|
|
63
|
+
req.close()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The URL grammar accepts IPv4, hostnames, and IPv6 literals both with and without explicit ports — e.g. `wss://host`, `wss://host:5568:5567`, `wss://[::1]`, `wss://[fe80::1]:5568:5567`.
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
- **[API reference — `docs/_index.md`](docs/_index.md)** — flat, method-by-method reference for every public class and function. Regenerated from the docstrings via `pydoc-markdown`.
|
|
71
|
+
- **[`ARCHITECTURE.md`](ARCHITECTURE.md)** — internals tour: module layout, connection lifecycle, subscribe frame format, parameter-tree cache, threading model, error contract, type conventions.
|
|
72
|
+
- **[`examples/README.md`](examples/README.md)** — runnable scripts (`quickstart.py`, `error_handling.py`) that demonstrate the canonical usage patterns.
|
|
73
|
+
- **[`CHANGELOG.md`](CHANGELOG.md)** — version history.
|
|
74
|
+
|
|
75
|
+
## Repository layout
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
motorcortex/ Python package
|
|
79
|
+
test/
|
|
80
|
+
unit/ Offline unit tests (no server)
|
|
81
|
+
integration/ Live tests against the vendored test_server
|
|
82
|
+
server/ Vendored C++ test_server (CMake project)
|
|
83
|
+
docs/ pydoc-markdown + stub generation scripts
|
|
84
|
+
benchmark/ Throughput scripts (not part of the test suite)
|
|
85
|
+
sandbox/ Ad-hoc repro scripts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Testing
|
|
89
|
+
|
|
90
|
+
Unit tests run offline and require only the package itself:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -e .
|
|
94
|
+
pip install "coverage[toml]>=7.4"
|
|
95
|
+
python -m unittest discover -s test/unit -t .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Integration tests spawn the vendored `test_server`. Build it once, then run the suite:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cmake -S test/server -B test/server/build -DCMAKE_BUILD_TYPE=Release
|
|
102
|
+
cmake --build test/server/build
|
|
103
|
+
|
|
104
|
+
python -m unittest discover -s test/integration -t .
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Coverage (line + branch). `pyproject.toml` sets `parallel = true`, so each
|
|
108
|
+
`coverage run` writes a per-process data shard; use `coverage combine` to
|
|
109
|
+
merge them before `coverage report`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
coverage erase
|
|
113
|
+
coverage run -m unittest discover -s test/unit -t .
|
|
114
|
+
coverage run -m unittest discover -s test/integration -t .
|
|
115
|
+
coverage combine
|
|
116
|
+
coverage report
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See [`test/README.md`](test/README.md) for the full testing walkthrough.
|
|
120
|
+
|
|
121
|
+
## Regenerating the API reference
|
|
122
|
+
|
|
123
|
+
[`docs/_index.md`](docs/_index.md) is committed and should be refreshed
|
|
124
|
+
before each release so the rendered reference matches the code at
|
|
125
|
+
the tag. The regen is two commands — `pydoc-markdown` first, then
|
|
126
|
+
`format_api.sh` to wrap `>>>` examples as fenced Python blocks and
|
|
127
|
+
prepend the front matter:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install pydoc-markdown
|
|
131
|
+
cd docs
|
|
132
|
+
pydoc-markdown pydoc-markdown.yml > _index.md
|
|
133
|
+
./format_api.sh
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The hook-based one-shot version was dropped — it races with the
|
|
137
|
+
shell redirect and silently loses output. See `docs/readme.md` and
|
|
138
|
+
the comment in `docs/pydoc-markdown.yml` for the full rationale.
|
|
139
|
+
|
|
140
|
+
## Release process
|
|
141
|
+
|
|
142
|
+
See [`PIPHOWTO.md`](PIPHOWTO.md) for PyPI release steps and [`CHANGELOG.md`](CHANGELOG.md) for version history.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#
|
|
4
4
|
# Developer: Alexey Zakharov (alexey.zakharov@vectioneer.com)
|
|
5
|
-
# All rights reserved. Copyright (c) 2016 -
|
|
5
|
+
# All rights reserved. Copyright (c) 2016 - 2026 VECTIONEER.
|
|
6
6
|
#
|
|
7
7
|
|
|
8
8
|
"""
|
|
@@ -14,6 +14,8 @@ Provides high-level APIs for communication, login, and data exchange using proto
|
|
|
14
14
|
See documentation for usage examples.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
17
19
|
from motorcortex.version import __version__
|
|
18
20
|
from motorcortex.parameter_tree import ParameterTree
|
|
19
21
|
from motorcortex.message_types import MessageTypes
|
|
@@ -21,11 +23,58 @@ from motorcortex.request import Request, ConnectionState
|
|
|
21
23
|
from motorcortex.reply import Reply
|
|
22
24
|
from motorcortex.subscribe import Subscribe
|
|
23
25
|
from motorcortex.subscription import Subscription
|
|
24
|
-
from motorcortex.timespec import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
from motorcortex.timespec import (
|
|
27
|
+
Timespec,
|
|
28
|
+
compare_timespec,
|
|
29
|
+
timespec_to_sec,
|
|
30
|
+
timespec_to_msec,
|
|
31
|
+
timespec_to_usec,
|
|
32
|
+
timespec_to_nsec,
|
|
33
|
+
)
|
|
28
34
|
from motorcortex.init_threads import init_nng_threads
|
|
35
|
+
from motorcortex.session import Session
|
|
36
|
+
from motorcortex.exceptions import (
|
|
37
|
+
McxError,
|
|
38
|
+
McxConnectionError,
|
|
39
|
+
McxLoginError,
|
|
40
|
+
McxTimeout,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ``__all__`` is the 1.0 public API surface. Anything not listed here —
|
|
44
|
+
# including module-level helpers defined below (``parseUrl``,
|
|
45
|
+
# ``makeUrl``, ``statusToStr``) and submodule imports like
|
|
46
|
+
# ``motorcortex.setup_logger`` — is implicitly private and may change
|
|
47
|
+
# between minor releases. For logging, do
|
|
48
|
+
# ``logging.getLogger("mcx")`` instead of reaching into the package.
|
|
49
|
+
__all__ = [
|
|
50
|
+
"__version__",
|
|
51
|
+
# Entry points
|
|
52
|
+
"connect",
|
|
53
|
+
"Session",
|
|
54
|
+
# Connection / protocol classes
|
|
55
|
+
"Request",
|
|
56
|
+
"Subscribe",
|
|
57
|
+
"Subscription",
|
|
58
|
+
"Reply",
|
|
59
|
+
"ConnectionState",
|
|
60
|
+
# Protobuf / parameters
|
|
61
|
+
"MessageTypes",
|
|
62
|
+
"ParameterTree",
|
|
63
|
+
# Time
|
|
64
|
+
"Timespec",
|
|
65
|
+
"compare_timespec",
|
|
66
|
+
"timespec_to_sec",
|
|
67
|
+
"timespec_to_msec",
|
|
68
|
+
"timespec_to_usec",
|
|
69
|
+
"timespec_to_nsec",
|
|
70
|
+
# Exceptions
|
|
71
|
+
"McxError",
|
|
72
|
+
"McxConnectionError",
|
|
73
|
+
"McxLoginError",
|
|
74
|
+
"McxTimeout",
|
|
75
|
+
# Tuning
|
|
76
|
+
"init_nng_threads",
|
|
77
|
+
]
|
|
29
78
|
|
|
30
79
|
init_nng_threads()
|
|
31
80
|
|
|
@@ -46,9 +95,20 @@ def parseUrl(url: str) -> tuple[str, str, int | None, int | None]:
|
|
|
46
95
|
|
|
47
96
|
If the URL does not contain ports, default endpoints '/mcx_req' and '/mcx_sub' are appended.
|
|
48
97
|
"""
|
|
98
|
+
# IPv6 host literals embed colons (``wss://[::1]``), so the rfind-based
|
|
99
|
+
# port scan has to start *after* the closing bracket of the host
|
|
100
|
+
# literal — otherwise it walks into the address bytes and either
|
|
101
|
+
# mis-parses the ports or (in the no-ports case) tries to int() an
|
|
102
|
+
# empty slice. For IPv4 / hostname URLs there is no bracket, so
|
|
103
|
+
# ``port_start`` stays at 0 and behavior matches the pre-fix code.
|
|
104
|
+
host_end = url.rfind(']')
|
|
105
|
+
port_start = host_end + 1 if host_end != -1 else 0
|
|
106
|
+
|
|
49
107
|
end = url.rfind(':')
|
|
50
|
-
|
|
51
|
-
|
|
108
|
+
if end < port_start:
|
|
109
|
+
return url + '/mcx_req', url + '/mcx_sub', None, None
|
|
110
|
+
start = url.rfind(':', port_start, end)
|
|
111
|
+
if start == -1:
|
|
52
112
|
return url + '/mcx_req', url + '/mcx_sub', None, None
|
|
53
113
|
req_port = int(url[start + 1:end])
|
|
54
114
|
sub_port = int(url[end + 1:])
|
|
@@ -75,10 +135,10 @@ def makeUrl(address: str, port: int | None) -> str:
|
|
|
75
135
|
|
|
76
136
|
def connect(
|
|
77
137
|
url: str,
|
|
78
|
-
motorcortex_types:
|
|
138
|
+
motorcortex_types: "MessageTypes",
|
|
79
139
|
param_tree: "ParameterTree",
|
|
80
140
|
reconnect: bool = True,
|
|
81
|
-
**kwargs
|
|
141
|
+
**kwargs: Any,
|
|
82
142
|
) -> tuple["Request", "Subscribe"]:
|
|
83
143
|
"""
|
|
84
144
|
Establishes connections to Motorcortex request and subscribe endpoints, performs login, and loads the parameter tree.
|
|
@@ -152,26 +212,52 @@ def connect(
|
|
|
152
212
|
req_address, sub_address, req_port, sub_port = parseUrl(url)
|
|
153
213
|
# Open request connection
|
|
154
214
|
req = Request(motorcortex_types, param_tree, kwargs.get("req_number_of_threads", 2))
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
215
|
+
sub = None
|
|
216
|
+
# Wrap every post-construction failure point so half-opened sockets
|
|
217
|
+
# get closed; otherwise the Subscribe receive thread stays blocked in
|
|
218
|
+
# recv() forever and pins the Python interpreter on exit (an atexit
|
|
219
|
+
# ``_python_exit`` handler then joins the worker indefinitely).
|
|
220
|
+
try:
|
|
221
|
+
kwargs_copy = kwargs.copy()
|
|
222
|
+
kwargs_copy.update(state_update=None)
|
|
223
|
+
if not req.connect(makeUrl(req_address, req_port), **kwargs_copy).get():
|
|
224
|
+
raise McxConnectionError(
|
|
225
|
+
"Failed to establish request connection: {}:{}".format(req_address, req_port)
|
|
226
|
+
)
|
|
227
|
+
# Open subscribe connection
|
|
228
|
+
sub = Subscribe(req, motorcortex_types, kwargs.get("sub_number_of_threads", 2))
|
|
229
|
+
if not sub.connect(makeUrl(sub_address, sub_port), **kwargs).get():
|
|
230
|
+
raise McxConnectionError(
|
|
231
|
+
"Failed to establish subscribe connection: {}:{}".format(sub_address, sub_port)
|
|
232
|
+
)
|
|
233
|
+
# Login. With ``Login: disable`` servers the credentials can legitimately
|
|
234
|
+
# be omitted — default to empty strings so the helper does not raise
|
|
235
|
+
# ``KeyError`` on valid callers.
|
|
236
|
+
login_reply = req.login(kwargs.get('login', ''), kwargs.get('password', ''))
|
|
237
|
+
login_reply_msg = login_reply.get()
|
|
238
|
+
|
|
239
|
+
motorcortex_msg = motorcortex_types.motorcortex()
|
|
240
|
+
if not login_reply_msg.status == motorcortex_msg.OK:
|
|
241
|
+
raise McxLoginError(
|
|
242
|
+
"Login failed, status: {}".format(login_reply_msg.status),
|
|
243
|
+
status=login_reply_msg.status,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Requesting a parameter tree
|
|
247
|
+
param_tree_reply = req.getParameterTree()
|
|
248
|
+
tree = param_tree_reply.get()
|
|
249
|
+
param_tree.load(tree)
|
|
250
|
+
except Exception:
|
|
251
|
+
if sub is not None:
|
|
252
|
+
try:
|
|
253
|
+
sub.close()
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
try:
|
|
257
|
+
req.close()
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
raise
|
|
175
261
|
|
|
176
262
|
# Start session token refresh
|
|
177
263
|
req._startTokenRefresh(token_interval_sec)
|
|
@@ -181,7 +267,7 @@ def connect(
|
|
|
181
267
|
return req, sub
|
|
182
268
|
|
|
183
269
|
|
|
184
|
-
def statusToStr(motorcortex_msg:
|
|
270
|
+
def statusToStr(motorcortex_msg: Any, code: int) -> str:
|
|
185
271
|
"""Converts status codes to a readable message.
|
|
186
272
|
|
|
187
273
|
Args:
|
|
@@ -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
|