scirodev 0.5.0__tar.gz → 0.5.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.0/scirodev.egg-info → scirodev-0.5.3}/PKG-INFO +16 -1
- {scirodev-0.5.0 → scirodev-0.5.3}/README.md +12 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/pyproject.toml +4 -1
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/_common.py +3 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/iodevices.py +1 -1
- scirodev-0.5.3/sciro/parameters.py +62 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/pump.py +54 -9
- scirodev-0.5.3/sciro/robotics.py +156 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/tools.py +52 -2
- {scirodev-0.5.0 → scirodev-0.5.3/scirodev.egg-info}/PKG-INFO +16 -1
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/requires.txt +4 -0
- scirodev-0.5.0/sciro/parameters.py +0 -41
- scirodev-0.5.0/sciro/robotics.py +0 -84
- {scirodev-0.5.0 → scirodev-0.5.3}/LICENSE +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/__init__.py +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/floorpro.py +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/hubs.py +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/sciro/py.typed +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev/__init__.py +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev/cli.py +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/SOURCES.txt +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/dependency_links.txt +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/entry_points.txt +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/top_level.txt +0 -0
- {scirodev-0.5.0 → scirodev-0.5.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -16,6 +16,9 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: pybricksdev>=2.3
|
|
18
18
|
Requires-Dist: pybricks>=3.6
|
|
19
|
+
Provides-Extra: docs
|
|
20
|
+
Requires-Dist: sphinx>=8; extra == "docs"
|
|
21
|
+
Requires-Dist: furo; extra == "docs"
|
|
19
22
|
Dynamic: license-file
|
|
20
23
|
|
|
21
24
|
# scirodev
|
|
@@ -89,6 +92,18 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
89
92
|
stub files here mirror the frozen modules; a release of this package matches
|
|
90
93
|
the PeakHub firmware of the same date.
|
|
91
94
|
|
|
95
|
+
## API reference (HTML)
|
|
96
|
+
|
|
97
|
+
The stubs double as the source of the API reference, built with Sphinx the way
|
|
98
|
+
docs.pybricks.com is (cross-links into it for the upstream types):
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
pip install -e ".[docs]"
|
|
102
|
+
scripts/build-docs # -> docs/_build/html/index.html
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The build output is static HTML; a website can copy that folder as is.
|
|
106
|
+
|
|
92
107
|
## Releasing
|
|
93
108
|
|
|
94
109
|
Bump `version` in `pyproject.toml`, commit, tag `vX.Y.Z` and push the tag. The
|
|
@@ -69,6 +69,18 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
69
69
|
stub files here mirror the frozen modules; a release of this package matches
|
|
70
70
|
the PeakHub firmware of the same date.
|
|
71
71
|
|
|
72
|
+
## API reference (HTML)
|
|
73
|
+
|
|
74
|
+
The stubs double as the source of the API reference, built with Sphinx the way
|
|
75
|
+
docs.pybricks.com is (cross-links into it for the upstream types):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
pip install -e ".[docs]"
|
|
79
|
+
scripts/build-docs # -> docs/_build/html/index.html
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The build output is static HTML; a website can copy that folder as is.
|
|
83
|
+
|
|
72
84
|
## Releasing
|
|
73
85
|
|
|
74
86
|
Bump `version` in `pyproject.toml`, commit, tag `vX.Y.Z` and push the tag. The
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scirodev"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.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"
|
|
@@ -24,6 +24,9 @@ dependencies = [
|
|
|
24
24
|
"pybricks>=3.6",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
docs = ["sphinx>=8", "furo"]
|
|
29
|
+
|
|
27
30
|
[project.urls]
|
|
28
31
|
Homepage = "https://github.com/sciurus-robotics/scirodev"
|
|
29
32
|
Repository = "https://github.com/sciurus-robotics/scirodev"
|
|
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
|
|
|
20
20
|
_RGB = Tuple[int, int, int]
|
|
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
|
+
_Bools = Tuple[bool, ...]
|
|
23
24
|
|
|
24
25
|
class MaybeAwaitableLine(_Line, Awaitable[_Line]): ...
|
|
25
26
|
|
|
@@ -40,3 +41,5 @@ if TYPE_CHECKING:
|
|
|
40
41
|
class MaybeAwaitableCalibration(_Calibration, Awaitable[_Calibration]): ...
|
|
41
42
|
|
|
42
43
|
class MaybeAwaitableStr(str, Awaitable[str]): ...
|
|
44
|
+
|
|
45
|
+
class MaybeAwaitableBools(_Bools, Awaitable[_Bools]): ...
|
|
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
|
|
12
12
|
from ._common import MaybeAwaitableStateData
|
|
13
13
|
|
|
14
|
-
from .parameters import Port as _Port
|
|
14
|
+
from pybricks.parameters import Port as _Port # base type: sciro.parameters.Port is a subclass
|
|
15
15
|
|
|
16
16
|
# (stream id, url, ext_port, state length)
|
|
17
17
|
StreamInfo = Tuple[int, str, int, int]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""sciro.parameters -- the Pybricks parameter types, with the PeakHub port set.
|
|
2
|
+
|
|
3
|
+
On the hub this module re-exports ``pybricks.parameters`` unchanged (the firmware
|
|
4
|
+
has always had ``Port.G`` and ``Port.H``); this stub exists because the upstream
|
|
5
|
+
stubs declare ``Port.A`` .. ``Port.F`` only.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pybricks.parameters import ( # noqa: F401 (re-exports)
|
|
9
|
+
Axis as Axis,
|
|
10
|
+
Button as Button,
|
|
11
|
+
Color as Color,
|
|
12
|
+
Direction as Direction,
|
|
13
|
+
Icon as Icon,
|
|
14
|
+
Side as Side,
|
|
15
|
+
Stop as Stop,
|
|
16
|
+
)
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from pybricks.parameters import _PybricksEnum
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pybricks.parameters import Port as _Port
|
|
23
|
+
|
|
24
|
+
class Port(_Port):
|
|
25
|
+
"""Port on the PeakHub. Eight LPF2/PUP ports, A .. H.
|
|
26
|
+
|
|
27
|
+
For the type checker this is a subclass of ``pybricks.parameters.Port``
|
|
28
|
+
(on the hub it is the same object), so a ``sciro`` port is accepted
|
|
29
|
+
wherever the upstream stubs expect a ``pybricks.parameters.Port``
|
|
30
|
+
(``Motor``, ``DriveBase``, ...). A .. F are inherited; G and H are
|
|
31
|
+
the two extra PeakHub ports.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
G: Port
|
|
35
|
+
H: Port
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
# At runtime the upstream Port is a real Enum, which cannot be subclassed
|
|
39
|
+
# once it has members; tooling that imports the stubs (docs, tests) gets
|
|
40
|
+
# an equivalent enum with all eight ports instead.
|
|
41
|
+
class Port(_PybricksEnum):
|
|
42
|
+
"""Port on the PeakHub. Eight LPF2/PUP ports, A .. H."""
|
|
43
|
+
|
|
44
|
+
A = ord("A")
|
|
45
|
+
B = ord("B")
|
|
46
|
+
C = ord("C")
|
|
47
|
+
D = ord("D")
|
|
48
|
+
E = ord("E")
|
|
49
|
+
F = ord("F")
|
|
50
|
+
G = ord("G")
|
|
51
|
+
H = ord("H")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ExtPort:
|
|
55
|
+
"""Extension port of a PUMP device (1-based, as in its stream table)."""
|
|
56
|
+
|
|
57
|
+
EXT1: int = 1
|
|
58
|
+
EXT2: int = 2
|
|
59
|
+
EXT3: int = 3
|
|
60
|
+
EXT4: int = 4
|
|
61
|
+
EXT5: int = 5
|
|
62
|
+
EXT6: int = 6
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
from typing import TYPE_CHECKING, Iterable, Optional, Tuple, Union
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from pybricks._common import MaybeAwaitable, MaybeAwaitableColor, MaybeAwaitableFloat
|
|
14
|
+
from pybricks._common import MaybeAwaitable, MaybeAwaitableBool, MaybeAwaitableColor, MaybeAwaitableFloat
|
|
15
15
|
|
|
16
16
|
from ._common import (
|
|
17
17
|
MaybeAwaitableEuler,
|
|
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
|
|
|
22
22
|
MaybeAwaitableRGB8,
|
|
23
23
|
MaybeAwaitableCalibration,
|
|
24
24
|
MaybeAwaitableStr,
|
|
25
|
+
MaybeAwaitableBools,
|
|
25
26
|
MaybeAwaitableRGBC,
|
|
26
27
|
)
|
|
27
28
|
|
|
@@ -29,7 +30,7 @@ from .iodevices import PUMPDevice as PUMPDevice # noqa: F401 (re-export)
|
|
|
29
30
|
from .iodevices import StreamInfo
|
|
30
31
|
from pybricks.parameters import Color
|
|
31
32
|
|
|
32
|
-
from .parameters import Port as _Port
|
|
33
|
+
from pybricks.parameters import Port as _Port # base type: sciro.parameters.Port is a subclass
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class _Stream:
|
|
@@ -50,12 +51,32 @@ class Line(_Stream):
|
|
|
50
51
|
"""read() -> Tuple
|
|
51
52
|
|
|
52
53
|
Returns ``(cog_dark, cog_bright, brightness, darkness, mask, calibrating)``:
|
|
53
|
-
|
|
54
|
+
center of gravity of the dark / bright pixels in sensor pitches from the
|
|
54
55
|
middle sensor (-7 .. +7), overall brightness / darkness (0 .. 1), a
|
|
55
56
|
15-bit bright-pixel mask (bit i = sensor i), and whether calibration is
|
|
56
57
|
active.
|
|
57
58
|
"""
|
|
58
59
|
|
|
60
|
+
def dark_centroid(self) -> MaybeAwaitableFloat:
|
|
61
|
+
"""dark_centroid() -> float -- center of gravity of the dark pixels, in
|
|
62
|
+
sensor pitches from the middle sensor (-7 .. +7)."""
|
|
63
|
+
|
|
64
|
+
def bright_centroid(self) -> MaybeAwaitableFloat:
|
|
65
|
+
"""bright_centroid() -> float -- center of gravity of the bright pixels (-7 .. +7)."""
|
|
66
|
+
|
|
67
|
+
def brightness(self) -> MaybeAwaitableFloat:
|
|
68
|
+
"""brightness() -> float -- overall brightness 0 .. 1."""
|
|
69
|
+
|
|
70
|
+
def darkness(self) -> MaybeAwaitableFloat:
|
|
71
|
+
"""darkness() -> float -- overall darkness 0 .. 1."""
|
|
72
|
+
|
|
73
|
+
def line_sensors_binary(self) -> MaybeAwaitableBools:
|
|
74
|
+
"""line_sensors_binary() -> Tuple[bool, ...] -- 15 flags, sensor 0 first,
|
|
75
|
+
True where the sensor sees bright."""
|
|
76
|
+
|
|
77
|
+
def calibrating(self) -> MaybeAwaitableBool:
|
|
78
|
+
"""calibrating() -> bool -- True while the min-max calibration runs."""
|
|
79
|
+
|
|
59
80
|
def calibrate(self, enable: bool = True) -> MaybeAwaitable:
|
|
60
81
|
"""calibrate(enable=True) -- start/stop min-max calibration of the array."""
|
|
61
82
|
|
|
@@ -99,7 +120,7 @@ class Pixels(_Stream):
|
|
|
99
120
|
|
|
100
121
|
|
|
101
122
|
class ColorSensor(_Stream):
|
|
102
|
-
"""TCS3400
|
|
123
|
+
"""TCS3400 color sensor on an extension port of a PUMP device.
|
|
103
124
|
|
|
104
125
|
``ColorSensor(port, ext_port)`` opens it directly; :meth:`FloorPro.color_sensor`
|
|
105
126
|
returns the same class. ``hsv()`` and ``color()`` follow
|
|
@@ -119,7 +140,7 @@ class ColorSensor(_Stream):
|
|
|
119
140
|
port (Port): Hub port of the PUMP device carrying the sensor (or the
|
|
120
141
|
opened ``PUMPDevice`` itself).
|
|
121
142
|
ext_port (ExtPort): Extension port the sensor is plugged into.
|
|
122
|
-
Raises ``OSError`` if there is no
|
|
143
|
+
Raises ``OSError`` if there is no color sensor there.
|
|
123
144
|
"""
|
|
124
145
|
|
|
125
146
|
def read(self) -> MaybeAwaitableRGBC:
|
|
@@ -137,7 +158,7 @@ class ColorSensor(_Stream):
|
|
|
137
158
|
"""hsv() -> Color
|
|
138
159
|
|
|
139
160
|
Hue (0 .. 359), saturation (0 .. 100) and value (0 .. 100) of the
|
|
140
|
-
surface, as a ``Color``: standard HSV of the calibrated
|
|
161
|
+
surface, as a ``Color``: standard HSV of the calibrated color when a
|
|
141
162
|
valid calibration applies, of the raw reading otherwise.
|
|
142
163
|
"""
|
|
143
164
|
|
|
@@ -159,7 +180,7 @@ class ColorSensor(_Stream):
|
|
|
159
180
|
"""
|
|
160
181
|
|
|
161
182
|
def clear_calibration(self) -> MaybeAwaitable:
|
|
162
|
-
"""clear_calibration() -- drop the stored calibration (back to full-scale
|
|
183
|
+
"""clear_calibration() -- drop the stored calibration (back to full-scale color)."""
|
|
163
184
|
|
|
164
185
|
def calibration(self) -> MaybeAwaitableCalibration:
|
|
165
186
|
"""calibration() -> Tuple
|
|
@@ -172,12 +193,12 @@ class ColorSensor(_Stream):
|
|
|
172
193
|
def color(self) -> MaybeAwaitableColor:
|
|
173
194
|
"""color() -> Color
|
|
174
195
|
|
|
175
|
-
The nearest of the detectable
|
|
196
|
+
The nearest of the detectable colors (default: red, yellow, green,
|
|
176
197
|
blue, white, none), matched like ``pybricks.pupdevices.ColorSensor``.
|
|
177
198
|
"""
|
|
178
199
|
|
|
179
200
|
def detectable_colors(self, colors: Optional[Iterable[Color]] = None) -> Optional[Tuple[Color, ...]]:
|
|
180
|
-
"""detectable_colors(colors) -- set the
|
|
201
|
+
"""detectable_colors(colors) -- set the colors color() chooses from; with no argument, get them."""
|
|
181
202
|
|
|
182
203
|
def settings(self) -> Tuple[int, int, int]:
|
|
183
204
|
"""settings() -> Tuple[int, int, int] -- (led_percent, gain_x, atime) in effect."""
|
|
@@ -259,6 +280,30 @@ class FloorPro:
|
|
|
259
280
|
def imu(self, ext_port: int = 2) -> Gyro:
|
|
260
281
|
"""imu(ext_port=2) -> Gyro -- alias of :meth:`gyro`."""
|
|
261
282
|
|
|
283
|
+
# Line conveniences at device level (the names of the LUMP driver class):
|
|
284
|
+
|
|
285
|
+
def all_sensor_data(self) -> MaybeAwaitableLine:
|
|
286
|
+
"""all_sensor_data() -> Tuple -- the same as ``line.read()``."""
|
|
287
|
+
|
|
288
|
+
def dark_centroid(self) -> MaybeAwaitableFloat:
|
|
289
|
+
"""dark_centroid() -> float -- see :meth:`Line.dark_centroid`."""
|
|
290
|
+
|
|
291
|
+
def bright_centroid(self) -> MaybeAwaitableFloat:
|
|
292
|
+
"""bright_centroid() -> float -- see :meth:`Line.bright_centroid`."""
|
|
293
|
+
|
|
294
|
+
def brightness(self) -> MaybeAwaitableFloat:
|
|
295
|
+
"""brightness() -> float -- overall brightness 0 .. 1."""
|
|
296
|
+
|
|
297
|
+
def darkness(self) -> MaybeAwaitableFloat:
|
|
298
|
+
"""darkness() -> float -- overall darkness 0 .. 1."""
|
|
299
|
+
|
|
300
|
+
def line_sensors_binary(self) -> MaybeAwaitableBools:
|
|
301
|
+
"""line_sensors_binary() -> Tuple[bool, ...] -- 15 flags, True = bright."""
|
|
302
|
+
|
|
303
|
+
def line_sensors_analog(self) -> MaybeAwaitableInts:
|
|
304
|
+
"""line_sensors_analog() -> Tuple[int, ...] -- 15 raw ADC counts
|
|
305
|
+
(0 .. 4095), sensor 0 first; one ``ir_raw`` frame on demand."""
|
|
306
|
+
|
|
262
307
|
def streams(self) -> Tuple[StreamInfo, ...]:
|
|
263
308
|
"""streams() -> Tuple -- the enumerated streams ``(id, url, ext_port, state_len)``."""
|
|
264
309
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Stubs for ``sciro.robotics`` (PeakHub firmware) -- control helpers.
|
|
2
|
+
EXPERIMENTAL: the API may still change between releases.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
from pybricks.parameters import Stop
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pybricks._common import MaybeAwaitable
|
|
13
|
+
from pybricks.parameters import Number
|
|
14
|
+
else:
|
|
15
|
+
# Upstream defines these only for type checkers; runtime importers of the
|
|
16
|
+
# stubs (the docs build) need values. Annotations are strings here
|
|
17
|
+
# (from __future__ import annotations), so placeholders suffice.
|
|
18
|
+
Number = Union[int, float]
|
|
19
|
+
MaybeAwaitable = None
|
|
20
|
+
|
|
21
|
+
from pybricks import _common
|
|
22
|
+
from pybricks.robotics import DriveBase as _DriveBase
|
|
23
|
+
from pybricks.tools import StopWatch
|
|
24
|
+
|
|
25
|
+
from sciro.tools import Logger, RingBuffer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Control(_common.Control):
|
|
29
|
+
"""A drivebase / motor controller, with its built-in logger exposed."""
|
|
30
|
+
|
|
31
|
+
log: Logger
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DriveBase(_DriveBase):
|
|
35
|
+
"""``pybricks.robotics.DriveBase`` (the same class on the hub). The stub
|
|
36
|
+
adds the ``log`` on ``heading_control`` / ``distance_control`` and the
|
|
37
|
+
PeakHub extensions of :meth:`straight`."""
|
|
38
|
+
|
|
39
|
+
heading_control: Control # type: ignore[assignment]
|
|
40
|
+
distance_control: Control # type: ignore[assignment]
|
|
41
|
+
|
|
42
|
+
def straight( # type: ignore[override]
|
|
43
|
+
self,
|
|
44
|
+
distance: Number,
|
|
45
|
+
then: Stop = Stop.HOLD,
|
|
46
|
+
wait: bool = True,
|
|
47
|
+
speed: Optional[Number] = None,
|
|
48
|
+
heading: Optional[Number] = None,
|
|
49
|
+
exit_speed: Number = 0,
|
|
50
|
+
) -> MaybeAwaitable:
|
|
51
|
+
"""straight(distance, then=Stop.HOLD, wait=True, speed=None, heading=None, exit_speed=0)
|
|
52
|
+
|
|
53
|
+
Drives straight for ``distance`` mm, as in Pybricks, with three
|
|
54
|
+
PeakHub extensions (keyword use recommended):
|
|
55
|
+
|
|
56
|
+
``speed``: drive speed in mm/s for this move only; ``None`` uses the
|
|
57
|
+
``straight_speed`` setting.
|
|
58
|
+
|
|
59
|
+
``heading``: absolute heading in degrees to hold during the move
|
|
60
|
+
instead of the heading at its start (the gyro heading with
|
|
61
|
+
``use_gyro(True)``, else the wheel-based one since the last
|
|
62
|
+
``reset()``). Any value is accepted: the nearest whole-turn
|
|
63
|
+
equivalent of the current heading is targeted, so ``heading=90``
|
|
64
|
+
while the internal heading reads 450 corrects by 0 degrees, never by
|
|
65
|
+
a full turn. ``None`` keeps the Pybricks behavior.
|
|
66
|
+
|
|
67
|
+
``exit_speed``: speed in mm/s the robot has when it reaches the
|
|
68
|
+
target; it keeps driving at that speed until the next command, so the
|
|
69
|
+
next ``straight()`` (or ``drive()``) blends in without a stop. Implies
|
|
70
|
+
``then=Stop.NONE``; any other explicit ``then`` raises ``ValueError``.
|
|
71
|
+
Clamped to the move's drive speed. ``0`` (default) stops or holds as
|
|
72
|
+
``then`` says. ``then=Stop.NONE`` without ``exit_speed`` keeps the
|
|
73
|
+
upstream meaning: continue at the drive speed.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def heading_target(self, angle: Optional[Number] = None) -> Optional[float]:
|
|
77
|
+
"""heading_target(angle) / heading_target() -> float
|
|
78
|
+
|
|
79
|
+
Steers a running :meth:`straight` (PeakHub): replaces the heading
|
|
80
|
+
setpoint without touching the distance trajectory (position, speed,
|
|
81
|
+
exit speed and end condition stay). Absolute heading in degrees,
|
|
82
|
+
float, same frame and nearest-whole-turn rule as ``straight(heading=)``.
|
|
83
|
+
The heading controller branches off its current reference on a new
|
|
84
|
+
trajectory, so small steps (the usual < 1 degree per 10 ms) are smooth
|
|
85
|
+
and large ones are limited by the ``turn_rate`` / ``turn_acceleration``
|
|
86
|
+
settings. Without an argument returns the current heading target.
|
|
87
|
+
|
|
88
|
+
Typical use, a line follower as a setpoint generator::
|
|
89
|
+
|
|
90
|
+
db.straight(dist, speed=v, exit_speed=e, heading=course, wait=False)
|
|
91
|
+
while not db.done():
|
|
92
|
+
data = await floor.all_sensor_data()
|
|
93
|
+
db.heading_target(course + K * data[0])
|
|
94
|
+
await wait(10)
|
|
95
|
+
|
|
96
|
+
Raises ``OSError`` when no ``straight()`` is running (``drive()``,
|
|
97
|
+
``turn()``, ``curve()``, ``arc()`` and an idle drivebase are not
|
|
98
|
+
steerable).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PIDController:
|
|
103
|
+
"""Discrete PID controller: ``output = kp*e + ki*integral(e) + kd*de/dt``.
|
|
104
|
+
|
|
105
|
+
``ki = kd = 0`` gives a plain P or PD controller. ``integral_limit`` clamps
|
|
106
|
+
the I term (anti-windup), ``output_limit`` the total output; ``None`` means
|
|
107
|
+
unlimited. A gap between updates longer than ``reset_after`` milliseconds
|
|
108
|
+
restarts the controller (a new movement); ``None`` never. ``stopwatch``
|
|
109
|
+
is the time source (default: an own ``StopWatch``); ``log`` a
|
|
110
|
+
:class:`RingBuffer` with :attr:`LOG_FIELDS` (see :meth:`make_log`).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
LOG_FIELDS: Tuple[Tuple[str, str], ...]
|
|
114
|
+
"""``t_ms, dt, error, p, i, d, output`` -- the row layout of the log."""
|
|
115
|
+
|
|
116
|
+
kp: float
|
|
117
|
+
ki: float
|
|
118
|
+
kd: float
|
|
119
|
+
integral_limit: Optional[float]
|
|
120
|
+
output_limit: Optional[float]
|
|
121
|
+
reset_after: Optional[int]
|
|
122
|
+
stopwatch: StopWatch
|
|
123
|
+
log: Optional[RingBuffer]
|
|
124
|
+
name: str
|
|
125
|
+
output: float
|
|
126
|
+
"""The last output."""
|
|
127
|
+
integral: float
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
kp: float,
|
|
132
|
+
ki: float = 0.0,
|
|
133
|
+
kd: float = 0.0,
|
|
134
|
+
integral_limit: Optional[float] = None,
|
|
135
|
+
output_limit: Optional[float] = None,
|
|
136
|
+
reset_after: Optional[int] = 100,
|
|
137
|
+
stopwatch: Optional[StopWatch] = None,
|
|
138
|
+
log: Optional[RingBuffer] = None,
|
|
139
|
+
name: str = "PID",
|
|
140
|
+
) -> None: ...
|
|
141
|
+
|
|
142
|
+
def make_log(self, seconds: float, rate: int = 100) -> RingBuffer:
|
|
143
|
+
"""Attach and return a RingBuffer for ``seconds`` of updates at ``rate`` Hz."""
|
|
144
|
+
|
|
145
|
+
def reset(self) -> None:
|
|
146
|
+
"""Forget the integral, the last error and the last update time."""
|
|
147
|
+
|
|
148
|
+
def update(self, error: float, derivative: Optional[float] = None, p_limit: Optional[float] = None) -> float:
|
|
149
|
+
"""One controller step; returns the output.
|
|
150
|
+
|
|
151
|
+
``error`` is setpoint minus measurement. ``derivative`` is d(error)/dt
|
|
152
|
+
when measured (e.g. minus the gyro rate for a heading error); ``None``
|
|
153
|
+
differentiates the error numerically. ``p_limit`` clamps the P term for
|
|
154
|
+
this step only, e.g. a braking profile that leaves the D term free to
|
|
155
|
+
damp.
|
|
156
|
+
"""
|
|
@@ -4,7 +4,56 @@ helpers. EXPERIMENTAL: the API may still change between releases.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
TYPE_CHECKING,
|
|
9
|
+
Any,
|
|
10
|
+
Awaitable,
|
|
11
|
+
Callable,
|
|
12
|
+
Iterable,
|
|
13
|
+
Iterator,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Tuple,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from pybricks._common import MaybeAwaitableTuple
|
|
21
|
+
else:
|
|
22
|
+
# Upstream defines this only for type checkers; runtime importers of the
|
|
23
|
+
# stubs (the docs build) need a value. Annotations are strings here
|
|
24
|
+
# (from __future__ import annotations), so a placeholder suffices.
|
|
25
|
+
MaybeAwaitableTuple = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def multitask(*coroutines: Awaitable[Any], race: bool = False) -> MaybeAwaitableTuple:
|
|
29
|
+
"""multitask(coroutine1, coroutine2, ..., race=False) -> Tuple
|
|
30
|
+
|
|
31
|
+
``pybricks.tools.multitask`` (the same function on the hub), typed to
|
|
32
|
+
accept any awaitable.
|
|
33
|
+
|
|
34
|
+
The upstream stub asks for ``Coroutine``, which every *maybe-awaitable*
|
|
35
|
+
hub method fails to satisfy for a type checker: ``motor.run_angle(...)``,
|
|
36
|
+
``db.straight(...)`` or ``sensor.read()`` are declared as awaitables, not
|
|
37
|
+
as coroutines, because they return a plain value when called outside
|
|
38
|
+
``run_task``. Importing ``multitask`` (and :func:`run_task`) from
|
|
39
|
+
``sciro.tools`` instead of ``pybricks.tools`` removes those warnings::
|
|
40
|
+
|
|
41
|
+
from sciro.tools import multitask, run_task
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
await multitask(db.straight(500), log.record(db.state), race=True)
|
|
45
|
+
|
|
46
|
+
run_task(main())
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_task(coroutine: Optional[Awaitable[Any]] = None) -> Optional[bool]:
|
|
51
|
+
"""run_task(coroutine) -> bool | None
|
|
52
|
+
|
|
53
|
+
``pybricks.tools.run_task`` (the same function on the hub), typed to
|
|
54
|
+
accept any awaitable; see :func:`multitask`. Without an argument it
|
|
55
|
+
returns whether the run loop is active.
|
|
56
|
+
"""
|
|
8
57
|
|
|
9
58
|
|
|
10
59
|
class RingBuffer:
|
|
@@ -65,7 +114,8 @@ class Logger:
|
|
|
65
114
|
fixed buffer (not a ring) allocated by :meth:`start`.
|
|
66
115
|
|
|
67
116
|
Controller columns: ``t_ms, traj_ms, position, speed, status, torque,
|
|
68
|
-
ref_position, ref_speed, est_position, est_speed, p, i, d``
|
|
117
|
+
ref_position, ref_speed, est_position, est_speed, p, i, d, position_c``
|
|
118
|
+
(``position_c`` = position in 0.01 units, PeakHub; position in
|
|
69
119
|
the controller's units: mm or deg; torque in uNm; status bits: actuation
|
|
70
120
|
0..1, stalled 2, on target 3, integration paused 4).
|
|
71
121
|
Servo (Motor) columns: ``t_ms, time_ms, angle, speed, status, voltage_mv,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -16,6 +16,9 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: pybricksdev>=2.3
|
|
18
18
|
Requires-Dist: pybricks>=3.6
|
|
19
|
+
Provides-Extra: docs
|
|
20
|
+
Requires-Dist: sphinx>=8; extra == "docs"
|
|
21
|
+
Requires-Dist: furo; extra == "docs"
|
|
19
22
|
Dynamic: license-file
|
|
20
23
|
|
|
21
24
|
# scirodev
|
|
@@ -89,6 +92,18 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
89
92
|
stub files here mirror the frozen modules; a release of this package matches
|
|
90
93
|
the PeakHub firmware of the same date.
|
|
91
94
|
|
|
95
|
+
## API reference (HTML)
|
|
96
|
+
|
|
97
|
+
The stubs double as the source of the API reference, built with Sphinx the way
|
|
98
|
+
docs.pybricks.com is (cross-links into it for the upstream types):
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
pip install -e ".[docs]"
|
|
102
|
+
scripts/build-docs # -> docs/_build/html/index.html
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The build output is static HTML; a website can copy that folder as is.
|
|
106
|
+
|
|
92
107
|
## Releasing
|
|
93
108
|
|
|
94
109
|
Bump `version` in `pyproject.toml`, commit, tag `vX.Y.Z` and push the tag. The
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""sciro.parameters -- the Pybricks parameter types, with the PeakHub port set.
|
|
2
|
-
|
|
3
|
-
On the hub this module re-exports ``pybricks.parameters`` unchanged (the firmware
|
|
4
|
-
has always had ``Port.G`` and ``Port.H``); this stub exists because the upstream
|
|
5
|
-
stubs declare ``Port.A`` .. ``Port.F`` only.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from pybricks.parameters import ( # noqa: F401 (re-exports)
|
|
9
|
-
Axis as Axis,
|
|
10
|
-
Button as Button,
|
|
11
|
-
Color as Color,
|
|
12
|
-
Direction as Direction,
|
|
13
|
-
Icon as Icon,
|
|
14
|
-
Side as Side,
|
|
15
|
-
Stop as Stop,
|
|
16
|
-
)
|
|
17
|
-
from pybricks.parameters import _PybricksEnum
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Port(_PybricksEnum):
|
|
21
|
-
"""Port on the PeakHub. Eight LPF2/PUP ports, A .. H."""
|
|
22
|
-
|
|
23
|
-
A: Port = ord("A")
|
|
24
|
-
B: Port = ord("B")
|
|
25
|
-
C: Port = ord("C")
|
|
26
|
-
D: Port = ord("D")
|
|
27
|
-
E: Port = ord("E")
|
|
28
|
-
F: Port = ord("F")
|
|
29
|
-
G: Port = ord("G")
|
|
30
|
-
H: Port = ord("H")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class ExtPort:
|
|
34
|
-
"""Extension port of a PUMP device (1-based, as in its stream table)."""
|
|
35
|
-
|
|
36
|
-
EXT1: int = 1
|
|
37
|
-
EXT2: int = 2
|
|
38
|
-
EXT3: int = 3
|
|
39
|
-
EXT4: int = 4
|
|
40
|
-
EXT5: int = 5
|
|
41
|
-
EXT6: int = 6
|
scirodev-0.5.0/sciro/robotics.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""Stubs for ``sciro.robotics`` (PeakHub firmware) -- control helpers.
|
|
2
|
-
EXPERIMENTAL: the API may still change between releases.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from typing import Optional, Tuple
|
|
8
|
-
|
|
9
|
-
from pybricks import _common
|
|
10
|
-
from pybricks.robotics import DriveBase as _DriveBase
|
|
11
|
-
from pybricks.tools import StopWatch
|
|
12
|
-
|
|
13
|
-
from sciro.tools import Logger, RingBuffer
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Control(_common.Control):
|
|
17
|
-
"""A drivebase / motor controller, with its built-in logger exposed."""
|
|
18
|
-
|
|
19
|
-
log: Logger
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class DriveBase(_DriveBase):
|
|
23
|
-
"""``pybricks.robotics.DriveBase`` (the same class on the hub); the stub
|
|
24
|
-
adds the ``log`` on ``heading_control`` / ``distance_control``."""
|
|
25
|
-
|
|
26
|
-
heading_control: Control # type: ignore[assignment]
|
|
27
|
-
distance_control: Control # type: ignore[assignment]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class PIDController:
|
|
31
|
-
"""Discrete PID controller: ``output = kp*e + ki*integral(e) + kd*de/dt``.
|
|
32
|
-
|
|
33
|
-
``ki = kd = 0`` gives a plain P or PD controller. ``integral_limit`` clamps
|
|
34
|
-
the I term (anti-windup), ``output_limit`` the total output; ``None`` means
|
|
35
|
-
unlimited. A gap between updates longer than ``reset_after`` milliseconds
|
|
36
|
-
restarts the controller (a new movement); ``None`` never. ``stopwatch``
|
|
37
|
-
is the time source (default: an own ``StopWatch``); ``log`` a
|
|
38
|
-
:class:`RingBuffer` with :attr:`LOG_FIELDS` (see :meth:`make_log`).
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
LOG_FIELDS: Tuple[Tuple[str, str], ...]
|
|
42
|
-
"""``t_ms, dt, error, p, i, d, output`` -- the row layout of the log."""
|
|
43
|
-
|
|
44
|
-
kp: float
|
|
45
|
-
ki: float
|
|
46
|
-
kd: float
|
|
47
|
-
integral_limit: Optional[float]
|
|
48
|
-
output_limit: Optional[float]
|
|
49
|
-
reset_after: Optional[int]
|
|
50
|
-
stopwatch: StopWatch
|
|
51
|
-
log: Optional[RingBuffer]
|
|
52
|
-
name: str
|
|
53
|
-
output: float
|
|
54
|
-
"""The last output."""
|
|
55
|
-
integral: float
|
|
56
|
-
|
|
57
|
-
def __init__(
|
|
58
|
-
self,
|
|
59
|
-
kp: float,
|
|
60
|
-
ki: float = 0.0,
|
|
61
|
-
kd: float = 0.0,
|
|
62
|
-
integral_limit: Optional[float] = None,
|
|
63
|
-
output_limit: Optional[float] = None,
|
|
64
|
-
reset_after: Optional[int] = 100,
|
|
65
|
-
stopwatch: Optional[StopWatch] = None,
|
|
66
|
-
log: Optional[RingBuffer] = None,
|
|
67
|
-
name: str = "PID",
|
|
68
|
-
) -> None: ...
|
|
69
|
-
|
|
70
|
-
def make_log(self, seconds: float, rate: int = 100) -> RingBuffer:
|
|
71
|
-
"""Attach and return a RingBuffer for ``seconds`` of updates at ``rate`` Hz."""
|
|
72
|
-
|
|
73
|
-
def reset(self) -> None:
|
|
74
|
-
"""Forget the integral, the last error and the last update time."""
|
|
75
|
-
|
|
76
|
-
def update(self, error: float, derivative: Optional[float] = None, p_limit: Optional[float] = None) -> float:
|
|
77
|
-
"""One controller step; returns the output.
|
|
78
|
-
|
|
79
|
-
``error`` is setpoint minus measurement. ``derivative`` is d(error)/dt
|
|
80
|
-
when measured (e.g. minus the gyro rate for a heading error); ``None``
|
|
81
|
-
differentiates the error numerically. ``p_limit`` clamps the P term for
|
|
82
|
-
this step only, e.g. a braking profile that leaves the D term free to
|
|
83
|
-
damp.
|
|
84
|
-
"""
|
|
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
|