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.
Files changed (25) hide show
  1. {scirodev-0.5.0/scirodev.egg-info → scirodev-0.5.3}/PKG-INFO +16 -1
  2. {scirodev-0.5.0 → scirodev-0.5.3}/README.md +12 -0
  3. {scirodev-0.5.0 → scirodev-0.5.3}/pyproject.toml +4 -1
  4. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/_common.py +3 -0
  5. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/iodevices.py +1 -1
  6. scirodev-0.5.3/sciro/parameters.py +62 -0
  7. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/pump.py +54 -9
  8. scirodev-0.5.3/sciro/robotics.py +156 -0
  9. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/tools.py +52 -2
  10. {scirodev-0.5.0 → scirodev-0.5.3/scirodev.egg-info}/PKG-INFO +16 -1
  11. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/requires.txt +4 -0
  12. scirodev-0.5.0/sciro/parameters.py +0 -41
  13. scirodev-0.5.0/sciro/robotics.py +0 -84
  14. {scirodev-0.5.0 → scirodev-0.5.3}/LICENSE +0 -0
  15. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/__init__.py +0 -0
  16. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/floorpro.py +0 -0
  17. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/hubs.py +0 -0
  18. {scirodev-0.5.0 → scirodev-0.5.3}/sciro/py.typed +0 -0
  19. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev/__init__.py +0 -0
  20. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev/cli.py +0 -0
  21. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/SOURCES.txt +0 -0
  22. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/dependency_links.txt +0 -0
  23. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/entry_points.txt +0 -0
  24. {scirodev-0.5.0 → scirodev-0.5.3}/scirodev.egg-info/top_level.txt +0 -0
  25. {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.0
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.0"
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
- centre of gravity of the dark / bright pixels in sensor pitches from the
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 colour sensor on an extension port of a PUMP device.
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 colour sensor there.
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 colour when a
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 colour)."""
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 colours (default: red, yellow, green,
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 colours color() chooses from; with no argument, get them."""
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 Awaitable, Callable, Iterable, Iterator, Optional, Sequence, Tuple
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`` (position in
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.0
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,2 +1,6 @@
1
1
  pybricksdev>=2.3
2
2
  pybricks>=3.6
3
+
4
+ [docs]
5
+ sphinx>=8
6
+ furo
@@ -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
@@ -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