scirodev 0.5.5__tar.gz → 0.7.3__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.
- {scirodev-0.5.5/scirodev.egg-info → scirodev-0.7.3}/PKG-INFO +34 -4
- {scirodev-0.5.5 → scirodev-0.7.3}/README.md +33 -3
- {scirodev-0.5.5 → scirodev-0.7.3}/pyproject.toml +1 -1
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/_common.py +3 -0
- scirodev-0.7.3/sciro/canbus.py +86 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/hubs.py +95 -2
- scirodev-0.7.3/scirodev/__init__.py +8 -0
- scirodev-0.7.3/scirodev/cli.py +146 -0
- scirodev-0.7.3/scirodev/rename.py +112 -0
- {scirodev-0.5.5 → scirodev-0.7.3/scirodev.egg-info}/PKG-INFO +34 -4
- {scirodev-0.5.5 → scirodev-0.7.3}/scirodev.egg-info/SOURCES.txt +2 -0
- scirodev-0.5.5/scirodev/__init__.py +0 -3
- scirodev-0.5.5/scirodev/cli.py +0 -17
- {scirodev-0.5.5 → scirodev-0.7.3}/LICENSE +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/__init__.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/floorpro.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/iodevices.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/parameters.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/pump.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/py.typed +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/robotics.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/sciro/tools.py +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/scirodev.egg-info/dependency_links.txt +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/scirodev.egg-info/entry_points.txt +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/scirodev.egg-info/requires.txt +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/scirodev.egg-info/top_level.txt +0 -0
- {scirodev-0.5.5 → scirodev-0.7.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev
|
|
5
5
|
Author: Thomas Schank
|
|
6
6
|
License: MIT
|
|
@@ -23,9 +23,14 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
# scirodev
|
|
25
25
|
|
|
26
|
-
Host-side companion to
|
|
27
|
-
namespace that PeakHub programs import, plus the
|
|
28
|
-
`
|
|
26
|
+
Host-side companion to Sciurus Robotics hubs (PeakHub so far): **typed API
|
|
27
|
+
stubs** for the `sciro` namespace that PeakHub programs import, plus the
|
|
28
|
+
`scirodev` command line tool.
|
|
29
|
+
|
|
30
|
+
The tool is built on `pybricksdev`. It offers pybricksdev's own tools
|
|
31
|
+
unchanged (`scirodev run ble prog.py` works exactly like `pybricksdev run`),
|
|
32
|
+
and adds tools that only exist for sciro hubs, such as `scirodev rename`.
|
|
33
|
+
`scirodev -h` lists both and marks which is which.
|
|
29
34
|
|
|
30
35
|
```
|
|
31
36
|
pip install scirodev # from PyPI
|
|
@@ -46,6 +51,7 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
46
51
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
47
52
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
48
53
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
54
|
+
from sciro.canbus import CAN # beta: raw classic-CAN frames on connector 1/2
|
|
49
55
|
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
50
56
|
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
51
57
|
|
|
@@ -92,6 +98,30 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
92
98
|
stub files here mirror the frozen modules; a release of this package matches
|
|
93
99
|
the PeakHub firmware of the same date.
|
|
94
100
|
|
|
101
|
+
## Hub names
|
|
102
|
+
|
|
103
|
+
Every PeakHub advertises under its own name, `Peak-XXXX` out of the box (the
|
|
104
|
+
first four characters of its board ID), so several hubs on one table can be
|
|
105
|
+
told apart:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
scirodev run ble --name Peak-BYN9 prog.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Give a hub a name of your choice (1 to 16 printable ASCII characters); it is
|
|
112
|
+
stored on the hub and survives power cycles and firmware updates:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
scirodev rename usb MyRobot # over USB: applied at once
|
|
116
|
+
scirodev rename ble MyRobot -n Peak-BYN9 # over Bluetooth: confirm on the hub
|
|
117
|
+
scirodev rename usb --default # back to Peak-XXXX
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Over Bluetooth the hub spells the new name on its display, then shows `?`.
|
|
121
|
+
Press the centre button to accept; any other button, or 10 seconds without
|
|
122
|
+
one, rejects. This keeps somebody else in radio range from renaming your hub.
|
|
123
|
+
`hub.system.name()` returns the name.
|
|
124
|
+
|
|
95
125
|
## API reference (HTML)
|
|
96
126
|
|
|
97
127
|
The stubs double as the source of the API reference, built with Sphinx the way
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# scirodev
|
|
2
2
|
|
|
3
|
-
Host-side companion to
|
|
4
|
-
namespace that PeakHub programs import, plus the
|
|
5
|
-
`
|
|
3
|
+
Host-side companion to Sciurus Robotics hubs (PeakHub so far): **typed API
|
|
4
|
+
stubs** for the `sciro` namespace that PeakHub programs import, plus the
|
|
5
|
+
`scirodev` command line tool.
|
|
6
|
+
|
|
7
|
+
The tool is built on `pybricksdev`. It offers pybricksdev's own tools
|
|
8
|
+
unchanged (`scirodev run ble prog.py` works exactly like `pybricksdev run`),
|
|
9
|
+
and adds tools that only exist for sciro hubs, such as `scirodev rename`.
|
|
10
|
+
`scirodev -h` lists both and marks which is which.
|
|
6
11
|
|
|
7
12
|
```
|
|
8
13
|
pip install scirodev # from PyPI
|
|
@@ -23,6 +28,7 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
23
28
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
24
29
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
25
30
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
31
|
+
from sciro.canbus import CAN # beta: raw classic-CAN frames on connector 1/2
|
|
26
32
|
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
27
33
|
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
28
34
|
|
|
@@ -69,6 +75,30 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
69
75
|
stub files here mirror the frozen modules; a release of this package matches
|
|
70
76
|
the PeakHub firmware of the same date.
|
|
71
77
|
|
|
78
|
+
## Hub names
|
|
79
|
+
|
|
80
|
+
Every PeakHub advertises under its own name, `Peak-XXXX` out of the box (the
|
|
81
|
+
first four characters of its board ID), so several hubs on one table can be
|
|
82
|
+
told apart:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
scirodev run ble --name Peak-BYN9 prog.py
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Give a hub a name of your choice (1 to 16 printable ASCII characters); it is
|
|
89
|
+
stored on the hub and survives power cycles and firmware updates:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
scirodev rename usb MyRobot # over USB: applied at once
|
|
93
|
+
scirodev rename ble MyRobot -n Peak-BYN9 # over Bluetooth: confirm on the hub
|
|
94
|
+
scirodev rename usb --default # back to Peak-XXXX
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Over Bluetooth the hub spells the new name on its display, then shows `?`.
|
|
98
|
+
Press the centre button to accept; any other button, or 10 seconds without
|
|
99
|
+
one, rejects. This keeps somebody else in radio range from renaming your hub.
|
|
100
|
+
`hub.system.name()` returns the name.
|
|
101
|
+
|
|
72
102
|
## API reference (HTML)
|
|
73
103
|
|
|
74
104
|
The stubs double as the source of the API reference, built with Sphinx the way
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scirodev"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
description = "PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
|
|
|
21
21
|
_Calibration = Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int], Tuple[int, int, int], bool, bool, int]
|
|
22
22
|
_Pixels = Tuple[_RGB, ...]
|
|
23
23
|
_Bools = Tuple[bool, ...]
|
|
24
|
+
_Frame = Tuple[int, bytes, bool]
|
|
24
25
|
|
|
25
26
|
class MaybeAwaitableLine(_Line, Awaitable[_Line]): ...
|
|
26
27
|
|
|
@@ -43,3 +44,5 @@ if TYPE_CHECKING:
|
|
|
43
44
|
class MaybeAwaitableStr(str, Awaitable[str]): ...
|
|
44
45
|
|
|
45
46
|
class MaybeAwaitableBools(_Bools, Awaitable[_Bools]): ...
|
|
47
|
+
|
|
48
|
+
class MaybeAwaitableFrame(_Frame, Awaitable[_Frame]): ...
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""sciro.canbus -- the PeakHub CAN connectors (beta).
|
|
2
|
+
|
|
3
|
+
Classic CAN (11/29-bit identifiers, up to 8 data bytes) on CAN connector 1 or 2.
|
|
4
|
+
Both transceivers are powered while a program holds a ``CAN`` object and are
|
|
5
|
+
switched off when the program ends.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pybricks._common import MaybeAwaitable
|
|
14
|
+
|
|
15
|
+
from ._common import MaybeAwaitableFrame
|
|
16
|
+
|
|
17
|
+
Frame = Tuple[int, bytes, bool]
|
|
18
|
+
"""A received frame: ``(id, data, extended)``."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CAN:
|
|
22
|
+
"""One of the hub's CAN connectors."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, bus: int, bitrate: int = 500000):
|
|
25
|
+
"""CAN(bus, bitrate=500000)
|
|
26
|
+
|
|
27
|
+
Arguments:
|
|
28
|
+
bus (int): Connector number, 1 or 2.
|
|
29
|
+
bitrate (int): Bits per second: 125000, 250000, 500000 or 1000000.
|
|
30
|
+
Raises ``OSError`` (``ENOTSUP``) on a board whose CAN
|
|
31
|
+
transceivers must stay off.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def bitrate(self, bitrate: Optional[int] = None) -> Optional[int]:
|
|
35
|
+
"""bitrate() -> int
|
|
36
|
+
bitrate(bitrate)
|
|
37
|
+
|
|
38
|
+
Gets or sets the bit rate. Setting it restarts the controller: frames
|
|
39
|
+
waiting to be sent are dropped, received frames are kept.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def send(self, id: int, data: bytes = b"", extended: bool = False, timeout: Optional[int] = 100) -> MaybeAwaitable:
|
|
43
|
+
"""send(id, data=b"", extended=False, timeout=100)
|
|
44
|
+
|
|
45
|
+
Queues one frame. Waits up to ``timeout`` ms for room in the transmit
|
|
46
|
+
queue (it only fills when nobody on the bus acknowledges) and raises
|
|
47
|
+
``OSError`` (``ETIMEDOUT``) after that.
|
|
48
|
+
|
|
49
|
+
Arguments:
|
|
50
|
+
id (int): Identifier, 11 bits or 29 bits with ``extended=True``.
|
|
51
|
+
data (bytes): 0 to 8 bytes.
|
|
52
|
+
extended (bool): Use a 29-bit identifier.
|
|
53
|
+
timeout (int): Milliseconds, ``None`` to wait forever.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def recv(self, timeout: Optional[int] = 1000) -> Optional[MaybeAwaitableFrame]:
|
|
57
|
+
"""recv(timeout=1000) -> Tuple[int, bytes, bool] | None
|
|
58
|
+
|
|
59
|
+
Takes the oldest received frame as ``(id, data, extended)``, or ``None``
|
|
60
|
+
when nothing arrived within ``timeout`` ms (``None`` = wait forever,
|
|
61
|
+
``0`` = just poll). Under multitasking ``await`` the result; it is then
|
|
62
|
+
never ``None`` at the call site, so narrow the type (e.g. ``assert``).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def pending(self) -> int:
|
|
66
|
+
"""pending() -> int
|
|
67
|
+
|
|
68
|
+
Number of received frames waiting (up to 32 are queued; beyond that,
|
|
69
|
+
frames are counted as lost).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def clear(self) -> None:
|
|
73
|
+
"""clear()
|
|
74
|
+
|
|
75
|
+
Drops the queued received frames and zeroes the counters.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def stats(self) -> Dict[str, Union[int, bool]]:
|
|
79
|
+
"""stats() -> Dict
|
|
80
|
+
|
|
81
|
+
Counters and controller state: ``tx_queued``, ``tx_done`` (acknowledged
|
|
82
|
+
on the bus), ``rx``, ``rx_lost``, ``bus_off_count``, ``tec``, ``rec``
|
|
83
|
+
(CAN error counters), ``lec`` (last error code, 0 none, 1 stuff, 2 form,
|
|
84
|
+
3 ack, 4 bit1, 5 bit0, 6 crc, 7 unchanged), ``warning``, ``passive``,
|
|
85
|
+
``bus_off``.
|
|
86
|
+
"""
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""sciro.hubs -- the PeakHub.
|
|
2
2
|
|
|
3
3
|
On the hub this module re-exports ``pybricks.hubs`` unchanged; the stub adds what
|
|
4
|
-
the upstream stubs lack: the ``PeakHub`` class
|
|
4
|
+
the upstream stubs lack: the ``PeakHub`` class, its ``display.device()``, and the
|
|
5
|
+
``ThisHub`` alias.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
|
-
from typing import Any, Optional, Protocol, Tuple
|
|
10
|
+
from typing import Any, Dict, Optional, Protocol, Tuple, Union
|
|
10
11
|
|
|
11
12
|
from pybricks import _common
|
|
12
13
|
from pybricks.hubs import PrimeHub as PrimeHub # noqa: F401 (re-export)
|
|
@@ -121,6 +122,68 @@ class Battery(_common.Battery):
|
|
|
121
122
|
"""
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
class UsbPd:
|
|
126
|
+
"""**Experimental.** The hub's USB-PD input.
|
|
127
|
+
|
|
128
|
+
Reached as :attr:`PeakHub.usb_pd`. Lets a program read the measured input
|
|
129
|
+
voltage and ask the USB-PD sink to renegotiate to a different one.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def voltage(self, volts: Optional[int] = None) -> Optional[int]:
|
|
133
|
+
"""voltage(volts=None) -> int
|
|
134
|
+
|
|
135
|
+
Without an argument, returns the **measured** USB-PD input voltage in
|
|
136
|
+
millivolts. The sense sits before the ideal diode, so it reads the
|
|
137
|
+
supply itself and is not masked by a connected battery. Returns 0 if
|
|
138
|
+
the ADC has not sampled yet (only in the first few ms after boot).
|
|
139
|
+
|
|
140
|
+
With an argument, asks the sink to renegotiate to that voltage.
|
|
141
|
+
|
|
142
|
+
Arguments:
|
|
143
|
+
volts (int): 5, 9, 12 or 15. Any other value raises ``ValueError``.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The measured voltage in mV when called with no argument,
|
|
147
|
+
otherwise ``None``.
|
|
148
|
+
|
|
149
|
+
.. warning::
|
|
150
|
+
|
|
151
|
+
**Experimental, and it is a request rather than a command.**
|
|
152
|
+
|
|
153
|
+
A supply that does not offer the requested voltage simply refuses.
|
|
154
|
+
Nothing is raised in that case and the hub stays where it was, so
|
|
155
|
+
**read the voltage back** to see what actually happened rather than
|
|
156
|
+
assuming the call took effect.
|
|
157
|
+
|
|
158
|
+
Higher PD gears (20 V, 28 V) are deliberately unreachable: the
|
|
159
|
+
hub's voltage sense saturates at 18.81 V, so the firmware could not
|
|
160
|
+
even measure what it had asked for.
|
|
161
|
+
|
|
162
|
+
Renegotiating itself is well behaved in practice: measured working even
|
|
163
|
+
while driving motors on a small (2x18650-class) power bank, and on a
|
|
164
|
+
bench supply switching 9 V to 12 V and back with the hub up throughout.
|
|
165
|
+
|
|
166
|
+
Some power banks that were already switched on when the cable went
|
|
167
|
+
in feed plain 5 V first and then cycle VBUS by themselves a few seconds
|
|
168
|
+
later to start PD; a hub running from USB alone restarts at 9 V then.
|
|
169
|
+
That is the bank's doing, not the request's. Switching such a bank off
|
|
170
|
+
and on before pressing PWR avoids it.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def status(self) -> Dict[str, Union[bool, int]]:
|
|
174
|
+
"""status() -> Dict
|
|
175
|
+
|
|
176
|
+
What the USB-PD sink negotiated with the supply:
|
|
177
|
+
``{"pd": bool, "bc": bool, "qc2": bool, "qc3": bool, "epr": bool,
|
|
178
|
+
"max_ma": int, "raw": int, "mv": int}``. ``pd`` is ``True`` under a PD
|
|
179
|
+
contract (the normal 9 V case). ``max_ma`` is the contract's current
|
|
180
|
+
limit (0 without PD), ``raw`` the sink's status register, ``mv`` the
|
|
181
|
+
measured input voltage.
|
|
182
|
+
|
|
183
|
+
Diagnostic: lets a program see whether a contract exists at all.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
|
|
124
187
|
class PeakHub:
|
|
125
188
|
"""LEGO-compatible hub by Sciurus Robotics: 8 ports, 5x5 RGB matrix, IMU."""
|
|
126
189
|
|
|
@@ -134,6 +197,7 @@ class PeakHub:
|
|
|
134
197
|
imu = _common.IMU()
|
|
135
198
|
speaker = _common.Speaker()
|
|
136
199
|
system = System()
|
|
200
|
+
usb_pd = UsbPd()
|
|
137
201
|
|
|
138
202
|
def __init__(
|
|
139
203
|
self,
|
|
@@ -150,3 +214,32 @@ class PeakHub:
|
|
|
150
214
|
broadcast_channel: Channel for broadcasting data (0 .. 255).
|
|
151
215
|
observe_channels: Channels to observe.
|
|
152
216
|
"""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
#: The hub this program is running on.
|
|
220
|
+
#:
|
|
221
|
+
#: Every Pybricks firmware compiles in exactly one hub class and exports it
|
|
222
|
+
#: under several names: the generic ``ThisHub``, the hub's own name, and any
|
|
223
|
+
#: aliases. On PeakHub firmware ``ThisHub``, :class:`PeakHub`, ``PrimeHub`` and
|
|
224
|
+
#: ``InventorHub`` are therefore all the *same* class object -- this is resolved
|
|
225
|
+
#: when the firmware is built, not by detecting anything at run time.
|
|
226
|
+
#:
|
|
227
|
+
#: Use it for code that should run unchanged on whichever hub is in front of
|
|
228
|
+
#: you, which is mostly bench and test scripts::
|
|
229
|
+
#:
|
|
230
|
+
#: from sciro.hubs import ThisHub
|
|
231
|
+
#:
|
|
232
|
+
#: hub = ThisHub()
|
|
233
|
+
#: print(hub.imu.heading(), hub.imu.tilt())
|
|
234
|
+
#:
|
|
235
|
+
#: Prefer :class:`PeakHub` when a program is specific to this hub anyway: it
|
|
236
|
+
#: says so, and it reads better than a generic name.
|
|
237
|
+
#:
|
|
238
|
+
#: Note the difference between the REPL and a downloaded program. The REPL is
|
|
239
|
+
#: started with everything auto-imported and with a ready-made ``hub`` instance
|
|
240
|
+
#: already created, so there ``hub.imu.heading()`` just works. A downloaded
|
|
241
|
+
#: program gets neither: it must import what it uses and construct the hub, as
|
|
242
|
+
#: above, and ``print(hub)`` there raises ``NameError``. (In the firmware this
|
|
243
|
+
#: is ``pb_package_pybricks_init(true)`` for the REPL versus ``false`` for
|
|
244
|
+
#: everything else.)
|
|
245
|
+
ThisHub = PeakHub
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""scirodev -- tooling for Sciurus Robotics hubs on top of pybricksdev, and the `sciro` API stubs."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("scirodev")
|
|
7
|
+
except PackageNotFoundError: # running from a source tree that is not installed
|
|
8
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""`scirodev` command line.
|
|
2
|
+
|
|
3
|
+
One tool for Sciurus Robotics hubs (PeakHub so far). It is built on
|
|
4
|
+
pybricksdev: pybricksdev's own tools (run, compile, flash, ...) are offered
|
|
5
|
+
unchanged, next to the tools that only exist for sciro hubs (rename).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
|
|
17
|
+
DESCRIPTION = """\
|
|
18
|
+
Command line tool for Sciurus Robotics hubs (PeakHub).
|
|
19
|
+
|
|
20
|
+
Built on pybricksdev. Tools marked [pybricksdev] are pybricksdev's own,
|
|
21
|
+
unchanged, and work with any hub running Pybricks firmware. The other tools
|
|
22
|
+
are extensions for sciro hubs.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
EPILOG = """\
|
|
26
|
+
Run `%(prog)s <tool> --help` for tool-specific arguments.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
%(prog)s run ble --name Peak-BYN9 program.py
|
|
30
|
+
%(prog)s rename ble MyRobot --name Peak-BYN9
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# pybricksdev tools to offer, by class name. Looked up by name so a pybricksdev
|
|
34
|
+
# release that adds or drops one does not break us.
|
|
35
|
+
PYBRICKSDEV_TOOLS = ("Compile", "Run", "Flash", "DFU", "OAD", "LWP3", "Udev")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _pybricksdev_version() -> str:
|
|
39
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
return version("pybricksdev")
|
|
43
|
+
except PackageNotFoundError:
|
|
44
|
+
return "?"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_parser() -> tuple[argparse.ArgumentParser, argparse._SubParsersAction]:
|
|
48
|
+
import pybricksdev.cli as pbcli
|
|
49
|
+
|
|
50
|
+
from .rename import Rename
|
|
51
|
+
|
|
52
|
+
parser = argparse.ArgumentParser(
|
|
53
|
+
prog="scirodev",
|
|
54
|
+
description=DESCRIPTION,
|
|
55
|
+
epilog=EPILOG,
|
|
56
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"-v",
|
|
60
|
+
"--version",
|
|
61
|
+
action="version",
|
|
62
|
+
version=f"scirodev v{__version__} (pybricksdev v{_pybricksdev_version()})",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("-d", "--debug", action="store_true", help="enable debug logging")
|
|
65
|
+
|
|
66
|
+
subparsers = parser.add_subparsers(metavar="<tool>", dest="tool", help="the tool to use")
|
|
67
|
+
|
|
68
|
+
# Our own tools first.
|
|
69
|
+
for tool in (Rename(),):
|
|
70
|
+
tool.add_parser(subparsers)
|
|
71
|
+
own = len(subparsers._choices_actions)
|
|
72
|
+
|
|
73
|
+
for name in PYBRICKSDEV_TOOLS:
|
|
74
|
+
cls = getattr(pbcli, name, None)
|
|
75
|
+
if cls is not None:
|
|
76
|
+
cls().add_parser(subparsers)
|
|
77
|
+
|
|
78
|
+
# Mark what comes from pybricksdev in the tool list.
|
|
79
|
+
for action in subparsers._choices_actions[own:]:
|
|
80
|
+
action.help = f"{action.help or ''} [pybricksdev]".strip()
|
|
81
|
+
|
|
82
|
+
return parser, subparsers
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def pairing_hint(error: str) -> str | None:
|
|
86
|
+
"""Plain words for the Bluetooth errors a PIN-protected hub can cause."""
|
|
87
|
+
e = error.lower()
|
|
88
|
+
if "peer removed pairing information" in e:
|
|
89
|
+
return (
|
|
90
|
+
"This computer was paired with the hub, but the hub no longer knows it\n"
|
|
91
|
+
"(its PIN was set or changed). Make the computer forget the hub in its\n"
|
|
92
|
+
"Bluetooth settings, then run the command again and enter the hub's PIN."
|
|
93
|
+
)
|
|
94
|
+
if "authentication" in e or "encryption is insufficient" in e or "pairing" in e:
|
|
95
|
+
return (
|
|
96
|
+
"The hub is protected with a PIN. Enter it in the dialog of your operating\n"
|
|
97
|
+
"system when it appears; if the command gave up meanwhile, run it again.\n"
|
|
98
|
+
"After 3 wrong PINs the hub refuses new computers until its Bluetooth\n"
|
|
99
|
+
"button is switched off and on."
|
|
100
|
+
)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> None:
|
|
105
|
+
if sys.platform == "win32":
|
|
106
|
+
# Same workaround as pybricksdev: bad side effects of pythoncom.
|
|
107
|
+
try:
|
|
108
|
+
from bleak_winrt._winrt import MTA, init_apartment
|
|
109
|
+
except ImportError:
|
|
110
|
+
from winrt._winrt import MTA, init_apartment
|
|
111
|
+
|
|
112
|
+
init_apartment(MTA)
|
|
113
|
+
|
|
114
|
+
parser, subparsers = build_parser()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
import argcomplete
|
|
118
|
+
|
|
119
|
+
argcomplete.autocomplete(parser)
|
|
120
|
+
except ImportError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
args = parser.parse_args()
|
|
124
|
+
|
|
125
|
+
logging.basicConfig(
|
|
126
|
+
format="%(asctime)s: %(levelname)s: %(name)s: %(message)s",
|
|
127
|
+
level=logging.DEBUG if args.debug else logging.WARNING,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not args.tool:
|
|
131
|
+
parser.error(f'Missing name of tool: {"|".join(subparsers.choices.keys())}')
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
result = asyncio.run(subparsers.choices[args.tool].tool.run(args))
|
|
135
|
+
except Exception as e:
|
|
136
|
+
hint = pairing_hint(str(e))
|
|
137
|
+
if hint is None or args.debug:
|
|
138
|
+
raise
|
|
139
|
+
print(f"error: {e}\n\n{hint}", file=sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
if isinstance(result, int) and result:
|
|
142
|
+
sys.exit(result)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""`scirodev rename`: give a PeakHub a name of its own (see DESCRIPTION)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
# PeakHub extension of the Pybricks command set (PBIO_PYBRICKS_COMMAND_SET_HUB_NAME).
|
|
10
|
+
SET_HUB_NAME = 0xA0
|
|
11
|
+
NAME_MAX = 16
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check_name(name: str) -> str:
|
|
15
|
+
if name == "":
|
|
16
|
+
return name
|
|
17
|
+
if len(name) > NAME_MAX:
|
|
18
|
+
raise argparse.ArgumentTypeError(f"at most {NAME_MAX} characters")
|
|
19
|
+
if any(not (0x20 <= ord(c) <= 0x7E) for c in name):
|
|
20
|
+
raise argparse.ArgumentTypeError("printable ASCII characters only")
|
|
21
|
+
if name != name.strip():
|
|
22
|
+
raise argparse.ArgumentTypeError("no leading or trailing spaces")
|
|
23
|
+
return name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
DESCRIPTION = """\
|
|
27
|
+
Give a PeakHub a name of its own.
|
|
28
|
+
|
|
29
|
+
The name is what the hub advertises over Bluetooth (so `scirodev run ble
|
|
30
|
+
--name <name>` finds exactly this hub) and what hub.system.name() returns. It
|
|
31
|
+
is stored on the hub across power cycles and firmware updates.
|
|
32
|
+
|
|
33
|
+
Over USB the hub takes the name at once. Over Bluetooth, where anybody in
|
|
34
|
+
range could send the command, the hub spells the new name on its display,
|
|
35
|
+
shows `?` and applies it only when the centre button is pressed (any other
|
|
36
|
+
button, or 10 s without one, rejects it). The hub must be idle: no program
|
|
37
|
+
running, not in power-save.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Rename:
|
|
42
|
+
"""The `rename` tool (same shape as pybricksdev's Tool classes)."""
|
|
43
|
+
|
|
44
|
+
def add_parser(self, subparsers: argparse._SubParsersAction) -> None:
|
|
45
|
+
p = subparsers.add_parser(
|
|
46
|
+
"rename",
|
|
47
|
+
help="set the name a PeakHub advertises under",
|
|
48
|
+
description=DESCRIPTION,
|
|
49
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
50
|
+
)
|
|
51
|
+
p.tool = self
|
|
52
|
+
self.parser = p
|
|
53
|
+
p.add_argument("conntype", choices=["ble", "usb"], help="how to reach the hub")
|
|
54
|
+
p.add_argument(
|
|
55
|
+
"new_name",
|
|
56
|
+
type=check_name,
|
|
57
|
+
nargs="?",
|
|
58
|
+
help=f"the new name: 1 to {NAME_MAX} printable ASCII characters",
|
|
59
|
+
)
|
|
60
|
+
p.add_argument("--default", action="store_true", help="go back to the default name (Peak-XXXX)")
|
|
61
|
+
p.add_argument("-n", "--name", help="current name of the hub to rename (ble; default: first hub found)")
|
|
62
|
+
|
|
63
|
+
async def run(self, args: argparse.Namespace) -> int:
|
|
64
|
+
if args.default == (args.new_name is not None):
|
|
65
|
+
self.parser.error("give either a new name or --default")
|
|
66
|
+
if args.default:
|
|
67
|
+
args.new_name = ""
|
|
68
|
+
elif args.new_name == "":
|
|
69
|
+
self.parser.error("the name must not be empty (use --default for the default name)")
|
|
70
|
+
return await rename(args)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def rename(args: argparse.Namespace) -> int:
|
|
74
|
+
from pybricksdev.ble.pybricks import PYBRICKS_COMMAND_EVENT_UUID
|
|
75
|
+
|
|
76
|
+
if args.conntype == "ble":
|
|
77
|
+
from pybricksdev.ble import find_device
|
|
78
|
+
from pybricksdev.connections.pybricks import PybricksHubBLE
|
|
79
|
+
|
|
80
|
+
print(f"Searching for {args.name or 'any hub with Pybricks service'}...")
|
|
81
|
+
hub = PybricksHubBLE(await find_device(args.name))
|
|
82
|
+
else:
|
|
83
|
+
from usb.core import find as find_usb
|
|
84
|
+
|
|
85
|
+
from pybricksdev.connections.pybricks import PybricksHubUSB
|
|
86
|
+
from pybricksdev.usb import LEGO_USB_VID
|
|
87
|
+
|
|
88
|
+
device = find_usb(custom_match=lambda d: d.idVendor == LEGO_USB_VID and d.product.endswith("Pybricks"))
|
|
89
|
+
if device is None:
|
|
90
|
+
print("Pybricks Hub not found.", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
hub = PybricksHubUSB(device)
|
|
93
|
+
|
|
94
|
+
shown = args.new_name or "the default name"
|
|
95
|
+
await hub.connect()
|
|
96
|
+
try:
|
|
97
|
+
await hub.write_gatt_char(
|
|
98
|
+
PYBRICKS_COMMAND_EVENT_UUID,
|
|
99
|
+
bytes([SET_HUB_NAME]) + args.new_name.encode("ascii"),
|
|
100
|
+
True,
|
|
101
|
+
)
|
|
102
|
+
if args.conntype == "ble":
|
|
103
|
+
print(f"The hub now shows the new name ({shown}) and then '?'.")
|
|
104
|
+
print("Press the centre button on the hub to accept. Any other button, or 10 s, rejects.")
|
|
105
|
+
# Stay connected while the user decides, so nobody else can slip in.
|
|
106
|
+
await asyncio.sleep(len(args.new_name) * 0.6 + 10.5)
|
|
107
|
+
print("Done. If accepted, the hub advertises under the new name from the next connection on.")
|
|
108
|
+
else:
|
|
109
|
+
print(f"Renamed to {shown}.")
|
|
110
|
+
finally:
|
|
111
|
+
await hub.disconnect()
|
|
112
|
+
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev
|
|
5
5
|
Author: Thomas Schank
|
|
6
6
|
License: MIT
|
|
@@ -23,9 +23,14 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
# scirodev
|
|
25
25
|
|
|
26
|
-
Host-side companion to
|
|
27
|
-
namespace that PeakHub programs import, plus the
|
|
28
|
-
`
|
|
26
|
+
Host-side companion to Sciurus Robotics hubs (PeakHub so far): **typed API
|
|
27
|
+
stubs** for the `sciro` namespace that PeakHub programs import, plus the
|
|
28
|
+
`scirodev` command line tool.
|
|
29
|
+
|
|
30
|
+
The tool is built on `pybricksdev`. It offers pybricksdev's own tools
|
|
31
|
+
unchanged (`scirodev run ble prog.py` works exactly like `pybricksdev run`),
|
|
32
|
+
and adds tools that only exist for sciro hubs, such as `scirodev rename`.
|
|
33
|
+
`scirodev -h` lists both and marks which is which.
|
|
29
34
|
|
|
30
35
|
```
|
|
31
36
|
pip install scirodev # from PyPI
|
|
@@ -46,6 +51,7 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
46
51
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
47
52
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
48
53
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
54
|
+
from sciro.canbus import CAN # beta: raw classic-CAN frames on connector 1/2
|
|
49
55
|
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
50
56
|
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
51
57
|
|
|
@@ -92,6 +98,30 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
92
98
|
stub files here mirror the frozen modules; a release of this package matches
|
|
93
99
|
the PeakHub firmware of the same date.
|
|
94
100
|
|
|
101
|
+
## Hub names
|
|
102
|
+
|
|
103
|
+
Every PeakHub advertises under its own name, `Peak-XXXX` out of the box (the
|
|
104
|
+
first four characters of its board ID), so several hubs on one table can be
|
|
105
|
+
told apart:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
scirodev run ble --name Peak-BYN9 prog.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Give a hub a name of your choice (1 to 16 printable ASCII characters); it is
|
|
112
|
+
stored on the hub and survives power cycles and firmware updates:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
scirodev rename usb MyRobot # over USB: applied at once
|
|
116
|
+
scirodev rename ble MyRobot -n Peak-BYN9 # over Bluetooth: confirm on the hub
|
|
117
|
+
scirodev rename usb --default # back to Peak-XXXX
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Over Bluetooth the hub spells the new name on its display, then shows `?`.
|
|
121
|
+
Press the centre button to accept; any other button, or 10 seconds without
|
|
122
|
+
one, rejects. This keeps somebody else in radio range from renaming your hub.
|
|
123
|
+
`hub.system.name()` returns the name.
|
|
124
|
+
|
|
95
125
|
## API reference (HTML)
|
|
96
126
|
|
|
97
127
|
The stubs double as the source of the API reference, built with Sphinx the way
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
sciro/__init__.py
|
|
5
5
|
sciro/_common.py
|
|
6
|
+
sciro/canbus.py
|
|
6
7
|
sciro/floorpro.py
|
|
7
8
|
sciro/hubs.py
|
|
8
9
|
sciro/iodevices.py
|
|
@@ -13,6 +14,7 @@ sciro/robotics.py
|
|
|
13
14
|
sciro/tools.py
|
|
14
15
|
scirodev/__init__.py
|
|
15
16
|
scirodev/cli.py
|
|
17
|
+
scirodev/rename.py
|
|
16
18
|
scirodev.egg-info/PKG-INFO
|
|
17
19
|
scirodev.egg-info/SOURCES.txt
|
|
18
20
|
scirodev.egg-info/dependency_links.txt
|
scirodev-0.5.5/scirodev/cli.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"""`scirodev` command line: pybricksdev's CLI (run, download, flash, ...) under
|
|
2
|
-
our name, so one tool covers PeakHub as well. PeakHub-specific commands can be
|
|
3
|
-
added here later; everything else is forwarded unchanged.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import sys
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main() -> None:
|
|
10
|
-
from pybricksdev.cli import main as pybricksdev_main
|
|
11
|
-
|
|
12
|
-
sys.argv[0] = "scirodev"
|
|
13
|
-
pybricksdev_main()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if __name__ == "__main__":
|
|
17
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|