scirodev 0.1.0__py3-none-any.whl

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.
sciro/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """sciro -- PeakHub extensions to the Pybricks API.
2
+
3
+ These are typing stubs (like the `pybricks` package): the real modules are frozen
4
+ into the PeakHub firmware. Import from `sciro.parameters`, `sciro.iodevices` and
5
+ `sciro.floorpro`.
6
+ """
sciro/_common.py ADDED
@@ -0,0 +1,31 @@
1
+ """Typing helpers for the sciro stubs, in the style of ``pybricks._common``:
2
+ a value that is returned directly, or awaitable under multitasking. The
3
+ upstream ``MaybeAwaitableTuple[T]`` is a 1-tuple, so fixed-shape results get
4
+ their own classes here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Tuple
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Awaitable
13
+
14
+ _Line = Tuple[float, float, float, float, int, bool]
15
+ _Ints = Tuple[int, ...]
16
+ _IRCalib = Tuple[Tuple[int, ...], Tuple[int, ...], int, int]
17
+ _RGBC = Tuple[int, int, int, int, int]
18
+ _Euler = Tuple[float, float, float, int]
19
+ _StateData = Tuple[bytes, bytes]
20
+
21
+ class MaybeAwaitableLine(_Line, Awaitable[_Line]): ...
22
+
23
+ class MaybeAwaitableInts(_Ints, Awaitable[_Ints]): ...
24
+
25
+ class MaybeAwaitableIRCalib(_IRCalib, Awaitable[_IRCalib]): ...
26
+
27
+ class MaybeAwaitableRGBC(_RGBC, Awaitable[_RGBC]): ...
28
+
29
+ class MaybeAwaitableEuler(_Euler, Awaitable[_Euler]): ...
30
+
31
+ class MaybeAwaitableStateData(_StateData, Awaitable[_StateData]): ...
sciro/floorpro.py ADDED
@@ -0,0 +1,148 @@
1
+ """sciro.floorpro -- the LP-FloorPro line sensor over PUMP (PeakHub).
2
+
3
+ Stub for the frozen module of the same name. Every reading method returns the
4
+ value directly, or an awaitable under multitasking.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Optional, Tuple
10
+
11
+ if TYPE_CHECKING:
12
+ from pybricks._common import MaybeAwaitable, MaybeAwaitableFloat
13
+
14
+ from ._common import (
15
+ MaybeAwaitableEuler,
16
+ MaybeAwaitableInts,
17
+ MaybeAwaitableIRCalib,
18
+ MaybeAwaitableLine,
19
+ MaybeAwaitableRGBC,
20
+ )
21
+
22
+ from .iodevices import PUMPDevice, StreamInfo
23
+ from .parameters import Port as _Port
24
+
25
+
26
+ class _Stream:
27
+ dev: PUMPDevice
28
+ id: int
29
+
30
+ def subscribe(self, mode: int, rate: int = 0) -> MaybeAwaitable:
31
+ """subscribe(mode, rate=0) -- see :meth:`PUMPDevice.subscribe`."""
32
+
33
+ def state(self) -> Optional[bytes]:
34
+ """state() -> bytes | None -- the stream's current state prefix."""
35
+
36
+
37
+ class Line(_Stream):
38
+ """IR array derived data (default: every sample, 400 Hz)."""
39
+
40
+ def read(self) -> MaybeAwaitableLine:
41
+ """read() -> Tuple
42
+
43
+ Returns ``(cog_dark, cog_bright, brightness, darkness, mask, calibrating)``:
44
+ centre of gravity of the dark / bright pixels in sensor pitches from the
45
+ middle sensor (-7 .. +7), overall brightness / darkness (0 .. 1), a
46
+ 15-bit bright-pixel mask (bit i = sensor i), and whether calibration is
47
+ active.
48
+ """
49
+
50
+ def calibrate(self, enable: bool = True) -> MaybeAwaitable:
51
+ """calibrate(enable=True) -- start/stop min-max calibration of the array."""
52
+
53
+
54
+ class IRRaw(_Stream):
55
+ """15 raw 12-bit ADC readings. Off by default: use :meth:`oneshot` or subscribe."""
56
+
57
+ def read(self) -> MaybeAwaitableInts:
58
+ """read() -> Tuple[int, ...] -- 15 values, 0 .. 4095, sensor 0 first."""
59
+
60
+ def oneshot(self) -> MaybeAwaitableInts:
61
+ """oneshot() -> Tuple[int, ...] -- request one frame and return it."""
62
+
63
+
64
+ class IRCalib(_Stream):
65
+ """Calibration table of the IR array. Off by default."""
66
+
67
+ def read(self) -> MaybeAwaitableIRCalib:
68
+ """read() -> Tuple
69
+
70
+ Returns ``(min, max, min_visited_mask, max_visited_mask)`` with ``min`` and
71
+ ``max`` tuples of 15 raw counts.
72
+ """
73
+
74
+ def oneshot(self) -> MaybeAwaitableIRCalib:
75
+ """oneshot() -> Tuple -- request the table once and return it."""
76
+
77
+
78
+ class ColorSensor(_Stream):
79
+ """TCS3400 on an extension port: raw R, G, B, C at device resolution.
80
+
81
+ Every sample is tagged with the settings it was measured under, so a read
82
+ after a settings change waits for the first sample taken with the new
83
+ settings.
84
+ """
85
+
86
+ def read(self) -> MaybeAwaitableRGBC:
87
+ """read() -> Tuple[int, int, int, int, int] -- (red, green, blue, clear, status)."""
88
+
89
+ def settings(self) -> Tuple[int, int, int]:
90
+ """settings() -> Tuple[int, int, int] -- (led_percent, gain_x, atime) in effect."""
91
+
92
+ def set_light(self, percent: int) -> MaybeAwaitable:
93
+ """set_light(percent) -- illumination LED duty, 0 .. 100."""
94
+
95
+ def set_gain(self, gain_x: int) -> MaybeAwaitable:
96
+ """set_gain(gain_x) -- analog gain: 1, 4, 16 or 64."""
97
+
98
+ def set_integration(self, atime: int) -> MaybeAwaitable:
99
+ """set_integration(atime) -- TCS3400 ATIME register 0 .. 255: (256 - atime) * 2.78 ms."""
100
+
101
+
102
+ class IMU(_Stream):
103
+ """BNO086 on an extension port: roll, pitch, yaw in degrees.
104
+
105
+ The heading offset lives on the hub: :meth:`reset_heading` makes the current
106
+ yaw read as the given angle.
107
+ """
108
+
109
+ def read(self) -> MaybeAwaitableEuler:
110
+ """read() -> Tuple -- (roll, pitch, yaw, status), degrees; yaw includes the offset."""
111
+
112
+ def heading(self) -> MaybeAwaitableFloat:
113
+ """heading() -> float -- yaw in degrees (-180 .. 180], with the offset applied."""
114
+
115
+ def reset_heading(self, angle: float = 0) -> MaybeAwaitable:
116
+ """reset_heading(angle=0) -- make the current heading read as ``angle``."""
117
+
118
+
119
+ class FloorPro:
120
+ """LP-FloorPro connected to a PeakHub port, speaking PUMP."""
121
+
122
+ dev: PUMPDevice
123
+ """The underlying generic device."""
124
+ serial: str
125
+ """The device's 8-character short ID."""
126
+ line: Line
127
+ ir_raw: IRRaw
128
+ ir_calib: IRCalib
129
+
130
+ def __init__(self, port: _Port):
131
+ """FloorPro(port)
132
+
133
+ Arguments:
134
+ port (Port): Port the sensor is connected to. Raises ``OSError`` if
135
+ no device is there or it is not a FloorPro.
136
+ """
137
+
138
+ def color_sensor(self, ext_port: int = 1) -> ColorSensor:
139
+ """color_sensor(ext_port=1) -> ColorSensor
140
+
141
+ The TCS3400 on extension port 1 or 2; raises ``OSError`` if none.
142
+ """
143
+
144
+ def imu(self, ext_port: int = 2) -> IMU:
145
+ """imu(ext_port=2) -> IMU -- the BNO086 on extension port 1 or 2; raises ``OSError`` if none."""
146
+
147
+ def streams(self) -> Tuple[StreamInfo, ...]:
148
+ """streams() -> Tuple -- the enumerated streams ``(id, url, ext_port, state_len)``."""
sciro/iodevices.py ADDED
@@ -0,0 +1,79 @@
1
+ """sciro.iodevices -- PeakHub-only device classes (re-exported from
2
+ ``pybricks.iodevices`` on the hub; declared here for the IDE).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
8
+
9
+ if TYPE_CHECKING:
10
+ from pybricks._common import MaybeAwaitable
11
+
12
+ from ._common import MaybeAwaitableStateData
13
+
14
+ from .parameters import Port as _Port
15
+
16
+ # (stream id, url, ext_port, state length)
17
+ StreamInfo = Tuple[int, str, int, int]
18
+
19
+
20
+ class PUMPDevice:
21
+ """Generic access to a PUMP device (Power UART Multiplex Protocol) on a
22
+ PeakHub port. Streams are addressed by id (1..N); state and data are raw
23
+ ``bytes`` decoded by the user or a convenience class such as
24
+ :class:`sciro.floorpro.FloorPro`. Spec: ``PUMP-Protocol.md``.
25
+ """
26
+
27
+ OFF: int = 0
28
+ """Stream-control mode: stream is silent."""
29
+ ONESHOT: int = 1
30
+ """Stream-control mode: one frame, then the mode self-clears to OFF."""
31
+ ON_CHANGE: int = 2
32
+ """Stream-control mode: a frame whenever a new sample differs."""
33
+ PERIODIC: int = 3
34
+ """Stream-control mode: every sample (rate 0) or at the given rate."""
35
+
36
+ def __init__(self, port: _Port):
37
+ """PUMPDevice(port)
38
+
39
+ Arguments:
40
+ port (Port): Port the device is connected to. Raises ``OSError``
41
+ (``ENODEV``) if no PUMP device has completed its handshake there
42
+ within a few seconds.
43
+ """
44
+
45
+ def info(self) -> Dict[str, Union[str, Tuple[StreamInfo, ...]]]:
46
+ """info() -> Dict
47
+
48
+ Returns:
49
+ ``{"url": str, "serial": str, "streams": ((id, url, ext_port, state_len), ...)}``
50
+ """
51
+
52
+ def state(self, stream: int) -> Optional[bytes]:
53
+ """state(stream) -> bytes | None
54
+
55
+ The last state prefix echoed by the stream (first two bytes are the
56
+ stream-control word: mode, rate), or ``None`` if none was received yet.
57
+ """
58
+
59
+ def read(self, stream: int) -> MaybeAwaitableStateData:
60
+ """read(stream) -> Tuple[bytes, bytes]
61
+
62
+ Waits for a frame measured under the currently requested settings and
63
+ returns ``(state, data)``. Awaitable under multitasking.
64
+ """
65
+
66
+ def change_state(self, stream: int, state: bytes) -> MaybeAwaitable:
67
+ """change_state(stream, state)
68
+
69
+ Sets the stream's whole state prefix (must be exactly its length) and
70
+ waits until the device echoes it. Raises ``OSError`` (``ETIMEDOUT``) if
71
+ the device never acknowledges.
72
+ """
73
+
74
+ def subscribe(self, stream: int, mode: int, rate: int = 0) -> MaybeAwaitable:
75
+ """subscribe(stream, mode, rate=0)
76
+
77
+ Changes only the stream-control word (keeps the device-specific state
78
+ bytes). ``rate`` is in units of 10 Hz, 0 = every sample.
79
+ """
sciro/parameters.py ADDED
@@ -0,0 +1,30 @@
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")
sciro/py.typed ADDED
File without changes
scirodev/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """scirodev -- PeakHub tooling on top of pybricksdev, and the `sciro` API stubs."""
2
+
3
+ __version__ = "0.1.0"
scirodev/cli.py ADDED
@@ -0,0 +1,17 @@
1
+ """`scirodev` command line: pybricksdev's CLI (run, download, flash, ...) under
2
+ our name, so one tool covers PeakHub as well. PeakHub-specific commands can be
3
+ added here later; everything else is forwarded unchanged.
4
+ """
5
+
6
+ import sys
7
+
8
+
9
+ def main() -> None:
10
+ from pybricksdev.cli import main as pybricksdev_main
11
+
12
+ sys.argv[0] = "scirodev"
13
+ pybricksdev_main()
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: scirodev
3
+ Version: 0.1.0
4
+ Summary: PeakHub / sciro extensions to Pybricks: typed API stubs (sciro.*) and hub tooling on top of pybricksdev
5
+ Author: Thomas Schank
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sciurus-robotics/scirodev
8
+ Project-URL: Repository, https://github.com/sciurus-robotics/scirodev
9
+ Keywords: pybricks,lego,peakhub,micropython,stubs
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Embedded Systems
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pybricksdev>=2.3
18
+ Requires-Dist: pybricks>=3.6
19
+ Dynamic: license-file
20
+
21
+ # scirodev
22
+
23
+ Host-side companion to the PeakHub firmware: **typed API stubs** for the `sciro`
24
+ namespace that PeakHub programs import, plus the hub tooling (it depends on
25
+ `pybricksdev`, so `scirodev run ble prog.py` works exactly like `pybricksdev`).
26
+
27
+ ```
28
+ pip install scirodev # from PyPI
29
+ pip install "scirodev @ git+https://github.com/sciurus-robotics/scirodev.git@v0.1.0"
30
+ pipx install scirodev # just the CLI, in its own venv
31
+ ```
32
+
33
+ Hacking on it:
34
+
35
+ ```
36
+ pip install -e .
37
+ ```
38
+
39
+ Then your IDE/type checker knows these, which the upstream `pybricks` stubs don't:
40
+
41
+ ```python
42
+ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 ports)
43
+ from sciro.iodevices import PUMPDevice # generic PUMP device access
44
+ from sciro.floorpro import FloorPro # LP-FloorPro convenience class
45
+
46
+ fp = FloorPro(Port.G)
47
+ cog_dark, cog_bright, brightness, darkness, mask, calibrating = fp.line.read()
48
+ ```
49
+
50
+ On the hub these modules are frozen into the PeakHub firmware:
51
+ `sciro.parameters` and `sciro.iodevices` re-export the same runtime objects as
52
+ their `pybricks.*` counterparts, so `sciro.parameters.Port is
53
+ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
54
+ stub files here mirror the frozen modules; a release of this package matches
55
+ the PeakHub firmware of the same date.
56
+
57
+ ## Releasing
58
+
59
+ Bump `version` in `pyproject.toml`, commit, tag `vX.Y.Z` and push the tag. The
60
+ GitHub workflow builds, type-checks and publishes to PyPI via Trusted Publishing
61
+ (configured on pypi.org: owner `sciurus-robotics`, repo `scirodev`, workflow
62
+ `publish.yml`, environment `pypi`).
@@ -0,0 +1,14 @@
1
+ sciro/__init__.py,sha256=auvDdSvjOd3FXLu8XV0mOGyahF06zkmfkGEj7US783o,238
2
+ sciro/_common.py,sha256=g2e5p1jEZ4Z1s2EtyL2sOY6cau1r4xbtZI1RKXQDTZk,1046
3
+ sciro/floorpro.py,sha256=fRj6IycPNEzYCp2BlYJ2rdZipKUGjO8n-9wdEXUOlo8,5005
4
+ sciro/iodevices.py,sha256=H_iE44JkJwzYR41BVylG8wJ8mR4a72pcQKBdbAbJp7g,2787
5
+ sciro/parameters.py,sha256=POB0G5i5bsmXDLoKkjZ_M14xLT_3GyaidpzRQrhHLqk,821
6
+ sciro/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ scirodev/__init__.py,sha256=PigYDhaDY9FpENpagsmRhOKNt7suuiLz__kHrf-WHJw,107
8
+ scirodev/cli.py,sha256=EedyRghlMPpNQ4qA0E9pIDK_tNBdfwf7sRBIiaNuO1s,405
9
+ scirodev-0.1.0.dist-info/licenses/LICENSE,sha256=5eMIVXEq0VbDVA3Y9p3IXJemflDENP4sQA2eSx_PNgI,1088
10
+ scirodev-0.1.0.dist-info/METADATA,sha256=q1Ox66tdgnC05FpLNsqclwWRclZpKtLWojNqHhRM_4A,2407
11
+ scirodev-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ scirodev-0.1.0.dist-info/entry_points.txt,sha256=G0501dWnjtPC7Bjp0jzKPlNXKS63Q1rD8acTcrw-AGI,47
13
+ scirodev-0.1.0.dist-info/top_level.txt,sha256=jwUBs_F6VA_ZrXFXxuZGG4rKLSaGoTUjsHZejBvrY8U,15
14
+ scirodev-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scirodev = scirodev.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sciurus Robotics, Thomas Schank
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ sciro
2
+ scirodev