scirodev 0.4.0__tar.gz → 0.5.0__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.4.0 → scirodev-0.5.0}/PKG-INFO +29 -1
- {scirodev-0.4.0 → scirodev-0.5.0}/README.md +28 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/pyproject.toml +1 -1
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/_common.py +7 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/hubs.py +29 -1
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/pump.py +41 -2
- scirodev-0.5.0/sciro/robotics.py +84 -0
- scirodev-0.5.0/sciro/tools.py +91 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev.egg-info/PKG-INFO +29 -1
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev.egg-info/SOURCES.txt +2 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/LICENSE +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/__init__.py +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/floorpro.py +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/iodevices.py +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/parameters.py +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/sciro/py.typed +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev/__init__.py +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev/cli.py +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev.egg-info/dependency_links.txt +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev.egg-info/entry_points.txt +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev.egg-info/requires.txt +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/scirodev.egg-info/top_level.txt +0 -0
- {scirodev-0.4.0 → scirodev-0.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -43,6 +43,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
43
43
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
44
44
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
45
45
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
46
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
47
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
46
48
|
|
|
47
49
|
hub = PeakHub()
|
|
48
50
|
fp = FloorPro(Port.G)
|
|
@@ -50,6 +52,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
50
52
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
56
|
+
console, and a PID controller that can log into it:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sciro.robotics import PIDController
|
|
60
|
+
from sciro.tools import RingBuffer
|
|
61
|
+
|
|
62
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
63
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
64
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
65
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
69
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
70
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
71
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
72
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
73
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
74
|
+
anything else.
|
|
75
|
+
|
|
76
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
77
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
78
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
79
|
+
path the CSV goes to the terminal.
|
|
80
|
+
|
|
53
81
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
54
82
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
55
83
|
`examples/` here stays minimal.
|
|
@@ -23,6 +23,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
23
23
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
24
24
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
25
25
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
26
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
27
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
26
28
|
|
|
27
29
|
hub = PeakHub()
|
|
28
30
|
fp = FloorPro(Port.G)
|
|
@@ -30,6 +32,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
30
32
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
31
33
|
```
|
|
32
34
|
|
|
35
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
36
|
+
console, and a PID controller that can log into it:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from sciro.robotics import PIDController
|
|
40
|
+
from sciro.tools import RingBuffer
|
|
41
|
+
|
|
42
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
43
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
44
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
45
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
49
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
50
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
51
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
52
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
53
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
54
|
+
anything else.
|
|
55
|
+
|
|
56
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
57
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
58
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
59
|
+
path the CSV goes to the terminal.
|
|
60
|
+
|
|
33
61
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
34
62
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
35
63
|
`examples/` here stays minimal.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scirodev"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
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"
|
|
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
|
|
18
18
|
_Euler = Tuple[float, float, float, int]
|
|
19
19
|
_StateData = Tuple[bytes, bytes]
|
|
20
20
|
_RGB = Tuple[int, int, int]
|
|
21
|
+
_Calibration = Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int], Tuple[int, int, int], bool, bool, int]
|
|
21
22
|
_Pixels = Tuple[_RGB, ...]
|
|
22
23
|
|
|
23
24
|
class MaybeAwaitableLine(_Line, Awaitable[_Line]): ...
|
|
@@ -33,3 +34,9 @@ if TYPE_CHECKING:
|
|
|
33
34
|
class MaybeAwaitableStateData(_StateData, Awaitable[_StateData]): ...
|
|
34
35
|
|
|
35
36
|
class MaybeAwaitablePixels(_Pixels, Awaitable[_Pixels]): ...
|
|
37
|
+
|
|
38
|
+
class MaybeAwaitableRGB8(_RGB, Awaitable[_RGB]): ...
|
|
39
|
+
|
|
40
|
+
class MaybeAwaitableCalibration(_Calibration, Awaitable[_Calibration]): ...
|
|
41
|
+
|
|
42
|
+
class MaybeAwaitableStr(str, Awaitable[str]): ...
|
|
@@ -45,6 +45,34 @@ class LightMatrix(_common.LightMatrix):
|
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
class System(_common.System):
|
|
49
|
+
"""The PeakHub's system object: everything ``pybricks`` offers, plus the
|
|
50
|
+
board identity.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def device_id(self) -> str:
|
|
54
|
+
"""device_id() -> str
|
|
55
|
+
|
|
56
|
+
The full factory unique ID of the hub's MCU as a 24-character uppercase
|
|
57
|
+
hex string. Stable per physical hub.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def short_id(self) -> str:
|
|
61
|
+
"""short_id() -> str
|
|
62
|
+
|
|
63
|
+
The hub's 8-character short ID (Crockford base32, derived from the
|
|
64
|
+
device ID): the same value the boot console prints and the device
|
|
65
|
+
inventory uses.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def info(self) -> dict:
|
|
69
|
+
"""info() -> dict
|
|
70
|
+
|
|
71
|
+
``{"name", "device_id", "short_id", "reset_reason", "program_id",
|
|
72
|
+
"program_start_type"}`` in one call.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
48
76
|
class PeakHub:
|
|
49
77
|
"""LEGO-compatible hub by Sciurus Robotics: 8 ports, 5x5 RGB matrix, IMU."""
|
|
50
78
|
|
|
@@ -56,7 +84,7 @@ class PeakHub:
|
|
|
56
84
|
display = LightMatrix(5, 5)
|
|
57
85
|
imu = _common.IMU()
|
|
58
86
|
speaker = _common.Speaker()
|
|
59
|
-
system =
|
|
87
|
+
system = System()
|
|
60
88
|
ble = _common.BLE()
|
|
61
89
|
|
|
62
90
|
def __init__(
|
|
@@ -19,6 +19,9 @@ if TYPE_CHECKING:
|
|
|
19
19
|
MaybeAwaitableIRCalib,
|
|
20
20
|
MaybeAwaitableLine,
|
|
21
21
|
MaybeAwaitablePixels,
|
|
22
|
+
MaybeAwaitableRGB8,
|
|
23
|
+
MaybeAwaitableCalibration,
|
|
24
|
+
MaybeAwaitableStr,
|
|
22
25
|
MaybeAwaitableRGBC,
|
|
23
26
|
)
|
|
24
27
|
|
|
@@ -122,12 +125,48 @@ class ColorSensor(_Stream):
|
|
|
122
125
|
def read(self) -> MaybeAwaitableRGBC:
|
|
123
126
|
"""read() -> Tuple[int, int, int, int, int] -- raw (red, green, blue, clear, status) at device resolution."""
|
|
124
127
|
|
|
128
|
+
def calibrated(self) -> MaybeAwaitableRGB8:
|
|
129
|
+
"""calibrated() -> Tuple[int, int, int]
|
|
130
|
+
|
|
131
|
+
Red, green, blue 0 .. 255 as the device itself shows them: stretched
|
|
132
|
+
over the calibrated range when a valid calibration applies, scaled to
|
|
133
|
+
full scale otherwise.
|
|
134
|
+
"""
|
|
135
|
+
|
|
125
136
|
def hsv(self) -> MaybeAwaitableColor:
|
|
126
137
|
"""hsv() -> Color
|
|
127
138
|
|
|
128
139
|
Hue (0 .. 359), saturation (0 .. 100) and value (0 .. 100) of the
|
|
129
|
-
surface, as a ``Color
|
|
130
|
-
|
|
140
|
+
surface, as a ``Color``: standard HSV of the calibrated colour when a
|
|
141
|
+
valid calibration applies, of the raw reading otherwise.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def calibration_status(self) -> MaybeAwaitableStr:
|
|
145
|
+
"""calibration_status() -> str
|
|
146
|
+
|
|
147
|
+
``"none"`` (nothing stored), ``"weak"`` (stored but not applicable:
|
|
148
|
+
incomplete, or LED / gain / integration differ from the calibration
|
|
149
|
+
profile), ``"ok"`` (applied), or ``"calibrating"``.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def calibrate(self, enable: bool = True) -> MaybeAwaitable:
|
|
153
|
+
"""calibrate(enable=True)
|
|
154
|
+
|
|
155
|
+
Start or stop the range calibration on the device. While it runs, move
|
|
156
|
+
the sensor over the darkest and brightest surfaces (or elements) it
|
|
157
|
+
will see; stopping stores the table on the sensor, bound to the LED,
|
|
158
|
+
gain and integration time in force. Changing those meanwhile aborts it.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def clear_calibration(self) -> MaybeAwaitable:
|
|
162
|
+
"""clear_calibration() -- drop the stored calibration (back to full-scale colour)."""
|
|
163
|
+
|
|
164
|
+
def calibration(self) -> MaybeAwaitableCalibration:
|
|
165
|
+
"""calibration() -> Tuple
|
|
166
|
+
|
|
167
|
+
The stored table: ``(min, max, (led, gain, atime), valid, applicable,
|
|
168
|
+
visited_mask)`` with ``min``/``max`` tuples of four counts (red, green,
|
|
169
|
+
blue, clear). Fetched from the device on demand.
|
|
131
170
|
"""
|
|
132
171
|
|
|
133
172
|
def color(self) -> MaybeAwaitableColor:
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
"""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Stubs for ``sciro.tools`` (PeakHub firmware) -- recording and publishing
|
|
2
|
+
helpers. EXPERIMENTAL: the API may still change between releases.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Awaitable, Callable, Iterable, Iterator, Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RingBuffer:
|
|
11
|
+
"""Fixed-size recorder of rows with a fixed binary layout, kept in RAM.
|
|
12
|
+
|
|
13
|
+
``fields`` are ``(name, format)`` pairs with ``struct`` formats (``"I"``
|
|
14
|
+
unsigned 32 bit, ``"i"`` signed, ``"f"`` float, ``"h"`` 16 bit, ``"B"``
|
|
15
|
+
byte, ...); ``capacity`` rows are kept, older rows are overwritten.
|
|
16
|
+
RAM used = capacity * row size (100 Hz * 30 s * 7 floats = 84 KB).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
fields: Tuple[Tuple[str, str], ...]
|
|
20
|
+
names: Tuple[str, ...]
|
|
21
|
+
format: str
|
|
22
|
+
row_size: int
|
|
23
|
+
capacity: int
|
|
24
|
+
name: Optional[str]
|
|
25
|
+
count: int
|
|
26
|
+
"""Rows appended since the last clear (not capped at capacity)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, fields: Iterable[Sequence[str]], capacity: int, name: Optional[str] = None) -> None: ...
|
|
29
|
+
|
|
30
|
+
def __len__(self) -> int:
|
|
31
|
+
"""Rows currently kept (at most ``capacity``)."""
|
|
32
|
+
|
|
33
|
+
def __getitem__(self, idx: int) -> Tuple[float | int, ...]:
|
|
34
|
+
"""Row ``idx`` as a tuple; 0 is the oldest kept row, -1 the newest."""
|
|
35
|
+
|
|
36
|
+
def __iter__(self) -> Iterator[Tuple[float | int, ...]]:
|
|
37
|
+
"""Rows, oldest first."""
|
|
38
|
+
|
|
39
|
+
def clear(self) -> None: ...
|
|
40
|
+
|
|
41
|
+
def append(self, *values: float | int) -> None:
|
|
42
|
+
"""Append one row; values in field order."""
|
|
43
|
+
|
|
44
|
+
def record(self, source: Callable[[], Sequence[float | int]], period: int = 10) -> Awaitable[None]:
|
|
45
|
+
"""Coroutine: append ``source()`` every ``period`` ms until cancelled.
|
|
46
|
+
|
|
47
|
+
Run it next to the movement, e.g.
|
|
48
|
+
``await multitask(drive(), log.record(db.state), race=True)``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def publish(self, path: Optional[str] = None, decimals: int = 3) -> None:
|
|
52
|
+
"""Print the rows as CSV (``#`` header comments, then the names).
|
|
53
|
+
|
|
54
|
+
With ``path`` the block is wrapped in the pybricksdev file markers
|
|
55
|
+
(``_file_begin_ <path>`` ... ``_file_end_``): ``scirodev run`` writes it
|
|
56
|
+
to that file, relative to the folder of the running script, instead of
|
|
57
|
+
the terminal. Expect on the order of 1000 rows per few seconds over BLE.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Logger:
|
|
62
|
+
"""The built-in pbio logger of a controller (``DriveBase.heading_control.log``,
|
|
63
|
+
``DriveBase.distance_control.log``, ``Motor.log``): rows are written by
|
|
64
|
+
the firmware's 5 ms control loop while the controller is active, into a
|
|
65
|
+
fixed buffer (not a ring) allocated by :meth:`start`.
|
|
66
|
+
|
|
67
|
+
Controller columns: ``t_ms, traj_ms, position, speed, status, torque,
|
|
68
|
+
ref_position, ref_speed, est_position, est_speed, p, i, d`` (position in
|
|
69
|
+
the controller's units: mm or deg; torque in uNm; status bits: actuation
|
|
70
|
+
0..1, stalled 2, on target 3, integration paused 4).
|
|
71
|
+
Servo (Motor) columns: ``t_ms, time_ms, angle, speed, status, voltage_mv,
|
|
72
|
+
est_angle, est_speed, torque_fb, torque_ff, observer_mv``.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def start(self, duration: int, down_sample: int = 1) -> None:
|
|
76
|
+
"""Allocate and start: ``duration`` ms, one row every
|
|
77
|
+
``5 * down_sample`` ms. RAM = rows * 52 bytes (controller), so 30 s at
|
|
78
|
+
``down_sample=2`` (100 Hz) is 156 KB; prefer 10 (20 Hz) for long runs.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def stop(self) -> None: ...
|
|
82
|
+
|
|
83
|
+
def save(self, path: Optional[str] = None) -> None:
|
|
84
|
+
"""Upstream: print the rows framed with ``PB_OF:<path>`` / ``PB_EOF``,
|
|
85
|
+
no header. Kept unchanged."""
|
|
86
|
+
|
|
87
|
+
def publish(self, path: Optional[str] = None, names: Optional[str] = None) -> None:
|
|
88
|
+
"""PeakHub: print the rows as CSV with a header line, framed with the
|
|
89
|
+
``_file_begin_ <path>`` / ``_file_end_`` markers like
|
|
90
|
+
:meth:`RingBuffer.publish`. ``names`` overrides the header
|
|
91
|
+
(comma separated). Stops logging first."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scirodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -43,6 +43,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
43
43
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
44
44
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
45
45
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
46
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
47
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
46
48
|
|
|
47
49
|
hub = PeakHub()
|
|
48
50
|
fp = FloorPro(Port.G)
|
|
@@ -50,6 +52,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
50
52
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
51
53
|
```
|
|
52
54
|
|
|
55
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
56
|
+
console, and a PID controller that can log into it:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sciro.robotics import PIDController
|
|
60
|
+
from sciro.tools import RingBuffer
|
|
61
|
+
|
|
62
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
63
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
64
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
65
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
69
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
70
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
71
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
72
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
73
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
74
|
+
anything else.
|
|
75
|
+
|
|
76
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
77
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
78
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
79
|
+
path the CSV goes to the terminal.
|
|
80
|
+
|
|
53
81
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
54
82
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
55
83
|
`examples/` here stays minimal.
|
|
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
|