scirodev 0.4.1__tar.gz → 0.5.2__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.1 → scirodev-0.5.2}/PKG-INFO +44 -1
- {scirodev-0.4.1 → scirodev-0.5.2}/README.md +40 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/pyproject.toml +4 -1
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/_common.py +3 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/iodevices.py +1 -1
- scirodev-0.5.2/sciro/parameters.py +62 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/pump.py +54 -9
- scirodev-0.5.2/sciro/robotics.py +156 -0
- scirodev-0.5.2/sciro/tools.py +92 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev.egg-info/PKG-INFO +44 -1
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev.egg-info/SOURCES.txt +2 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev.egg-info/requires.txt +4 -0
- scirodev-0.4.1/sciro/parameters.py +0 -41
- {scirodev-0.4.1 → scirodev-0.5.2}/LICENSE +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/__init__.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/floorpro.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/hubs.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/sciro/py.typed +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev/__init__.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev/cli.py +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev.egg-info/dependency_links.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev.egg-info/entry_points.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/scirodev.egg-info/top_level.txt +0 -0
- {scirodev-0.4.1 → scirodev-0.5.2}/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.2
|
|
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
|
|
@@ -43,6 +46,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
43
46
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
44
47
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
45
48
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
49
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
50
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
46
51
|
|
|
47
52
|
hub = PeakHub()
|
|
48
53
|
fp = FloorPro(Port.G)
|
|
@@ -50,6 +55,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
50
55
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
51
56
|
```
|
|
52
57
|
|
|
58
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
59
|
+
console, and a PID controller that can log into it:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from sciro.robotics import PIDController
|
|
63
|
+
from sciro.tools import RingBuffer
|
|
64
|
+
|
|
65
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
66
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
67
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
68
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
72
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
73
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
74
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
75
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
76
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
77
|
+
anything else.
|
|
78
|
+
|
|
79
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
80
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
81
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
82
|
+
path the CSV goes to the terminal.
|
|
83
|
+
|
|
53
84
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
54
85
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
55
86
|
`examples/` here stays minimal.
|
|
@@ -61,6 +92,18 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
61
92
|
stub files here mirror the frozen modules; a release of this package matches
|
|
62
93
|
the PeakHub firmware of the same date.
|
|
63
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
|
+
|
|
64
107
|
## Releasing
|
|
65
108
|
|
|
66
109
|
Bump `version` in `pyproject.toml`, commit, tag `vX.Y.Z` and push the tag. The
|
|
@@ -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.
|
|
@@ -41,6 +69,18 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
41
69
|
stub files here mirror the frozen modules; a release of this package matches
|
|
42
70
|
the PeakHub firmware of the same date.
|
|
43
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
|
+
|
|
44
84
|
## Releasing
|
|
45
85
|
|
|
46
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.
|
|
7
|
+
version = "0.5.2"
|
|
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
|
+
"""
|
|
@@ -0,0 +1,92 @@
|
|
|
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_c``
|
|
69
|
+
(``position_c`` = position in 0.01 units, PeakHub; position in
|
|
70
|
+
the controller's units: mm or deg; torque in uNm; status bits: actuation
|
|
71
|
+
0..1, stalled 2, on target 3, integration paused 4).
|
|
72
|
+
Servo (Motor) columns: ``t_ms, time_ms, angle, speed, status, voltage_mv,
|
|
73
|
+
est_angle, est_speed, torque_fb, torque_ff, observer_mv``.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def start(self, duration: int, down_sample: int = 1) -> None:
|
|
77
|
+
"""Allocate and start: ``duration`` ms, one row every
|
|
78
|
+
``5 * down_sample`` ms. RAM = rows * 52 bytes (controller), so 30 s at
|
|
79
|
+
``down_sample=2`` (100 Hz) is 156 KB; prefer 10 (20 Hz) for long runs.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def stop(self) -> None: ...
|
|
83
|
+
|
|
84
|
+
def save(self, path: Optional[str] = None) -> None:
|
|
85
|
+
"""Upstream: print the rows framed with ``PB_OF:<path>`` / ``PB_EOF``,
|
|
86
|
+
no header. Kept unchanged."""
|
|
87
|
+
|
|
88
|
+
def publish(self, path: Optional[str] = None, names: Optional[str] = None) -> None:
|
|
89
|
+
"""PeakHub: print the rows as CSV with a header line, framed with the
|
|
90
|
+
``_file_begin_ <path>`` / ``_file_end_`` markers like
|
|
91
|
+
:meth:`RingBuffer.publish`. ``names`` overrides the header
|
|
92
|
+
(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.2
|
|
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
|
|
@@ -43,6 +46,8 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
|
|
|
43
46
|
from sciro.iodevices import PUMPDevice # generic PUMP device access
|
|
44
47
|
from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
|
|
45
48
|
from sciro.hubs import PeakHub # hub class incl. display.device()
|
|
49
|
+
from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
|
|
50
|
+
from sciro.robotics import PIDController # experimental: PID with optional logging
|
|
46
51
|
|
|
47
52
|
hub = PeakHub()
|
|
48
53
|
fp = FloorPro(Port.G)
|
|
@@ -50,6 +55,32 @@ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
|
|
|
50
55
|
hub.display.device(fp, brightness=50) # mirror the sensor's LED strip on the 5x5
|
|
51
56
|
```
|
|
52
57
|
|
|
58
|
+
Experimental (API may change): a RAM recorder that publishes CSV through the
|
|
59
|
+
console, and a PID controller that can log into it:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from sciro.robotics import PIDController
|
|
63
|
+
from sciro.tools import RingBuffer
|
|
64
|
+
|
|
65
|
+
pid = PIDController(kp=15, kd=0.2, output_limit=300)
|
|
66
|
+
log = pid.make_log(seconds=10, rate=100) # RingBuffer of pid.LOG_FIELDS
|
|
67
|
+
turn_rate = pid.update(error, derivative=-hub.imu.angular_velocity(Axis.Z))
|
|
68
|
+
log.publish("doc/pid_run.csv") # scirodev run writes the file next to the script
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The drivebase's own controllers log too (built into Pybricks, undocumented
|
|
72
|
+
upstream): `db.heading_control.log.start(5000, down_sample=2)` records the
|
|
73
|
+
reference trajectory, the estimated state and the P/I/D terms at 100 Hz from the
|
|
74
|
+
firmware's control loop; `db.heading_control.log.publish("heading.csv")` writes
|
|
75
|
+
it with a header line. `sciro.robotics.DriveBase` is the Pybricks class with
|
|
76
|
+
these attributes typed. `log.record(db.state)` is the coroutine form for
|
|
77
|
+
anything else.
|
|
78
|
+
|
|
79
|
+
`publish(path)` wraps the CSV in pybricksdev's `_file_begin_ <path>` /
|
|
80
|
+
`_file_end_` lines; `scirodev run` (and `pybricksdev run`) then write the block
|
|
81
|
+
to that file, relative to the script's folder, instead of echoing it. Without a
|
|
82
|
+
path the CSV goes to the terminal.
|
|
83
|
+
|
|
53
84
|
Full demo programs for the LP FloorPro (both for LEGO hubs and the PeakHub) live in
|
|
54
85
|
[sciurus-robotics/FloorPro-CodeDemos](https://github.com/sciurus-robotics/FloorPro-CodeDemos);
|
|
55
86
|
`examples/` here stays minimal.
|
|
@@ -61,6 +92,18 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
|
|
|
61
92
|
stub files here mirror the frozen modules; a release of this package matches
|
|
62
93
|
the PeakHub firmware of the same date.
|
|
63
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
|
+
|
|
64
107
|
## Releasing
|
|
65
108
|
|
|
66
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
|
|
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
|