scirodev 0.5.6__tar.gz → 0.7.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 (27) hide show
  1. {scirodev-0.5.6/scirodev.egg-info → scirodev-0.7.3}/PKG-INFO +34 -4
  2. {scirodev-0.5.6 → scirodev-0.7.3}/README.md +33 -3
  3. {scirodev-0.5.6 → scirodev-0.7.3}/pyproject.toml +1 -1
  4. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/_common.py +3 -0
  5. scirodev-0.7.3/sciro/canbus.py +86 -0
  6. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/hubs.py +64 -1
  7. scirodev-0.7.3/scirodev/__init__.py +8 -0
  8. scirodev-0.7.3/scirodev/cli.py +146 -0
  9. scirodev-0.7.3/scirodev/rename.py +112 -0
  10. {scirodev-0.5.6 → scirodev-0.7.3/scirodev.egg-info}/PKG-INFO +34 -4
  11. {scirodev-0.5.6 → scirodev-0.7.3}/scirodev.egg-info/SOURCES.txt +2 -0
  12. scirodev-0.5.6/scirodev/__init__.py +0 -3
  13. scirodev-0.5.6/scirodev/cli.py +0 -17
  14. {scirodev-0.5.6 → scirodev-0.7.3}/LICENSE +0 -0
  15. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/__init__.py +0 -0
  16. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/floorpro.py +0 -0
  17. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/iodevices.py +0 -0
  18. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/parameters.py +0 -0
  19. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/pump.py +0 -0
  20. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/py.typed +0 -0
  21. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/robotics.py +0 -0
  22. {scirodev-0.5.6 → scirodev-0.7.3}/sciro/tools.py +0 -0
  23. {scirodev-0.5.6 → scirodev-0.7.3}/scirodev.egg-info/dependency_links.txt +0 -0
  24. {scirodev-0.5.6 → scirodev-0.7.3}/scirodev.egg-info/entry_points.txt +0 -0
  25. {scirodev-0.5.6 → scirodev-0.7.3}/scirodev.egg-info/requires.txt +0 -0
  26. {scirodev-0.5.6 → scirodev-0.7.3}/scirodev.egg-info/top_level.txt +0 -0
  27. {scirodev-0.5.6 → scirodev-0.7.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scirodev
3
- Version: 0.5.6
3
+ Version: 0.7.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
@@ -23,9 +23,14 @@ Dynamic: license-file
23
23
 
24
24
  # scirodev
25
25
 
26
- Host-side companion to the PeakHub firmware: **typed API stubs** for the `sciro`
27
- namespace that PeakHub programs import, plus the hub tooling (it depends on
28
- `pybricksdev`, so `scirodev run ble prog.py` works exactly like `pybricksdev`).
26
+ Host-side companion to Sciurus Robotics hubs (PeakHub so far): **typed API
27
+ stubs** for the `sciro` namespace that PeakHub programs import, plus the
28
+ `scirodev` command line tool.
29
+
30
+ The tool is built on `pybricksdev`. It offers pybricksdev's own tools
31
+ unchanged (`scirodev run ble prog.py` works exactly like `pybricksdev run`),
32
+ and adds tools that only exist for sciro hubs, such as `scirodev rename`.
33
+ `scirodev -h` lists both and marks which is which.
29
34
 
30
35
  ```
31
36
  pip install scirodev # from PyPI
@@ -46,6 +51,7 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
46
51
  from sciro.iodevices import PUMPDevice # generic PUMP device access
47
52
  from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
48
53
  from sciro.hubs import PeakHub # hub class incl. display.device()
54
+ from sciro.canbus import CAN # beta: raw classic-CAN frames on connector 1/2
49
55
  from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
50
56
  from sciro.robotics import PIDController # experimental: PID with optional logging
51
57
 
@@ -92,6 +98,30 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
92
98
  stub files here mirror the frozen modules; a release of this package matches
93
99
  the PeakHub firmware of the same date.
94
100
 
101
+ ## Hub names
102
+
103
+ Every PeakHub advertises under its own name, `Peak-XXXX` out of the box (the
104
+ first four characters of its board ID), so several hubs on one table can be
105
+ told apart:
106
+
107
+ ```
108
+ scirodev run ble --name Peak-BYN9 prog.py
109
+ ```
110
+
111
+ Give a hub a name of your choice (1 to 16 printable ASCII characters); it is
112
+ stored on the hub and survives power cycles and firmware updates:
113
+
114
+ ```
115
+ scirodev rename usb MyRobot # over USB: applied at once
116
+ scirodev rename ble MyRobot -n Peak-BYN9 # over Bluetooth: confirm on the hub
117
+ scirodev rename usb --default # back to Peak-XXXX
118
+ ```
119
+
120
+ Over Bluetooth the hub spells the new name on its display, then shows `?`.
121
+ Press the centre button to accept; any other button, or 10 seconds without
122
+ one, rejects. This keeps somebody else in radio range from renaming your hub.
123
+ `hub.system.name()` returns the name.
124
+
95
125
  ## API reference (HTML)
96
126
 
97
127
  The stubs double as the source of the API reference, built with Sphinx the way
@@ -1,8 +1,13 @@
1
1
  # scirodev
2
2
 
3
- Host-side companion to the PeakHub firmware: **typed API stubs** for the `sciro`
4
- namespace that PeakHub programs import, plus the hub tooling (it depends on
5
- `pybricksdev`, so `scirodev run ble prog.py` works exactly like `pybricksdev`).
3
+ Host-side companion to Sciurus Robotics hubs (PeakHub so far): **typed API
4
+ stubs** for the `sciro` namespace that PeakHub programs import, plus the
5
+ `scirodev` command line tool.
6
+
7
+ The tool is built on `pybricksdev`. It offers pybricksdev's own tools
8
+ unchanged (`scirodev run ble prog.py` works exactly like `pybricksdev run`),
9
+ and adds tools that only exist for sciro hubs, such as `scirodev rename`.
10
+ `scirodev -h` lists both and marks which is which.
6
11
 
7
12
  ```
8
13
  pip install scirodev # from PyPI
@@ -23,6 +28,7 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
23
28
  from sciro.iodevices import PUMPDevice # generic PUMP device access
24
29
  from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
25
30
  from sciro.hubs import PeakHub # hub class incl. display.device()
31
+ from sciro.canbus import CAN # beta: raw classic-CAN frames on connector 1/2
26
32
  from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
27
33
  from sciro.robotics import PIDController # experimental: PID with optional logging
28
34
 
@@ -69,6 +75,30 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
69
75
  stub files here mirror the frozen modules; a release of this package matches
70
76
  the PeakHub firmware of the same date.
71
77
 
78
+ ## Hub names
79
+
80
+ Every PeakHub advertises under its own name, `Peak-XXXX` out of the box (the
81
+ first four characters of its board ID), so several hubs on one table can be
82
+ told apart:
83
+
84
+ ```
85
+ scirodev run ble --name Peak-BYN9 prog.py
86
+ ```
87
+
88
+ Give a hub a name of your choice (1 to 16 printable ASCII characters); it is
89
+ stored on the hub and survives power cycles and firmware updates:
90
+
91
+ ```
92
+ scirodev rename usb MyRobot # over USB: applied at once
93
+ scirodev rename ble MyRobot -n Peak-BYN9 # over Bluetooth: confirm on the hub
94
+ scirodev rename usb --default # back to Peak-XXXX
95
+ ```
96
+
97
+ Over Bluetooth the hub spells the new name on its display, then shows `?`.
98
+ Press the centre button to accept; any other button, or 10 seconds without
99
+ one, rejects. This keeps somebody else in radio range from renaming your hub.
100
+ `hub.system.name()` returns the name.
101
+
72
102
  ## API reference (HTML)
73
103
 
74
104
  The stubs double as the source of the API reference, built with Sphinx the way
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scirodev"
7
- version = "0.5.6"
7
+ version = "0.7.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"
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
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
23
  _Bools = Tuple[bool, ...]
24
+ _Frame = Tuple[int, bytes, bool]
24
25
 
25
26
  class MaybeAwaitableLine(_Line, Awaitable[_Line]): ...
26
27
 
@@ -43,3 +44,5 @@ if TYPE_CHECKING:
43
44
  class MaybeAwaitableStr(str, Awaitable[str]): ...
44
45
 
45
46
  class MaybeAwaitableBools(_Bools, Awaitable[_Bools]): ...
47
+
48
+ class MaybeAwaitableFrame(_Frame, Awaitable[_Frame]): ...
@@ -0,0 +1,86 @@
1
+ """sciro.canbus -- the PeakHub CAN connectors (beta).
2
+
3
+ Classic CAN (11/29-bit identifiers, up to 8 data bytes) on CAN connector 1 or 2.
4
+ Both transceivers are powered while a program holds a ``CAN`` object and are
5
+ switched off when the program ends.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
11
+
12
+ if TYPE_CHECKING:
13
+ from pybricks._common import MaybeAwaitable
14
+
15
+ from ._common import MaybeAwaitableFrame
16
+
17
+ Frame = Tuple[int, bytes, bool]
18
+ """A received frame: ``(id, data, extended)``."""
19
+
20
+
21
+ class CAN:
22
+ """One of the hub's CAN connectors."""
23
+
24
+ def __init__(self, bus: int, bitrate: int = 500000):
25
+ """CAN(bus, bitrate=500000)
26
+
27
+ Arguments:
28
+ bus (int): Connector number, 1 or 2.
29
+ bitrate (int): Bits per second: 125000, 250000, 500000 or 1000000.
30
+ Raises ``OSError`` (``ENOTSUP``) on a board whose CAN
31
+ transceivers must stay off.
32
+ """
33
+
34
+ def bitrate(self, bitrate: Optional[int] = None) -> Optional[int]:
35
+ """bitrate() -> int
36
+ bitrate(bitrate)
37
+
38
+ Gets or sets the bit rate. Setting it restarts the controller: frames
39
+ waiting to be sent are dropped, received frames are kept.
40
+ """
41
+
42
+ def send(self, id: int, data: bytes = b"", extended: bool = False, timeout: Optional[int] = 100) -> MaybeAwaitable:
43
+ """send(id, data=b"", extended=False, timeout=100)
44
+
45
+ Queues one frame. Waits up to ``timeout`` ms for room in the transmit
46
+ queue (it only fills when nobody on the bus acknowledges) and raises
47
+ ``OSError`` (``ETIMEDOUT``) after that.
48
+
49
+ Arguments:
50
+ id (int): Identifier, 11 bits or 29 bits with ``extended=True``.
51
+ data (bytes): 0 to 8 bytes.
52
+ extended (bool): Use a 29-bit identifier.
53
+ timeout (int): Milliseconds, ``None`` to wait forever.
54
+ """
55
+
56
+ def recv(self, timeout: Optional[int] = 1000) -> Optional[MaybeAwaitableFrame]:
57
+ """recv(timeout=1000) -> Tuple[int, bytes, bool] | None
58
+
59
+ Takes the oldest received frame as ``(id, data, extended)``, or ``None``
60
+ when nothing arrived within ``timeout`` ms (``None`` = wait forever,
61
+ ``0`` = just poll). Under multitasking ``await`` the result; it is then
62
+ never ``None`` at the call site, so narrow the type (e.g. ``assert``).
63
+ """
64
+
65
+ def pending(self) -> int:
66
+ """pending() -> int
67
+
68
+ Number of received frames waiting (up to 32 are queued; beyond that,
69
+ frames are counted as lost).
70
+ """
71
+
72
+ def clear(self) -> None:
73
+ """clear()
74
+
75
+ Drops the queued received frames and zeroes the counters.
76
+ """
77
+
78
+ def stats(self) -> Dict[str, Union[int, bool]]:
79
+ """stats() -> Dict
80
+
81
+ Counters and controller state: ``tx_queued``, ``tx_done`` (acknowledged
82
+ on the bus), ``rx``, ``rx_lost``, ``bus_off_count``, ``tec``, ``rec``
83
+ (CAN error counters), ``lec`` (last error code, 0 none, 1 stuff, 2 form,
84
+ 3 ack, 4 bit1, 5 bit0, 6 crc, 7 unchanged), ``warning``, ``passive``,
85
+ ``bus_off``.
86
+ """
@@ -7,7 +7,7 @@ the upstream stubs lack: the ``PeakHub`` class, its ``display.device()``, and th
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any, Optional, Protocol, Tuple
10
+ from typing import Any, Dict, Optional, Protocol, Tuple, Union
11
11
 
12
12
  from pybricks import _common
13
13
  from pybricks.hubs import PrimeHub as PrimeHub # noqa: F401 (re-export)
@@ -122,6 +122,68 @@ class Battery(_common.Battery):
122
122
  """
123
123
 
124
124
 
125
+ class UsbPd:
126
+ """**Experimental.** The hub's USB-PD input.
127
+
128
+ Reached as :attr:`PeakHub.usb_pd`. Lets a program read the measured input
129
+ voltage and ask the USB-PD sink to renegotiate to a different one.
130
+ """
131
+
132
+ def voltage(self, volts: Optional[int] = None) -> Optional[int]:
133
+ """voltage(volts=None) -> int
134
+
135
+ Without an argument, returns the **measured** USB-PD input voltage in
136
+ millivolts. The sense sits before the ideal diode, so it reads the
137
+ supply itself and is not masked by a connected battery. Returns 0 if
138
+ the ADC has not sampled yet (only in the first few ms after boot).
139
+
140
+ With an argument, asks the sink to renegotiate to that voltage.
141
+
142
+ Arguments:
143
+ volts (int): 5, 9, 12 or 15. Any other value raises ``ValueError``.
144
+
145
+ Returns:
146
+ The measured voltage in mV when called with no argument,
147
+ otherwise ``None``.
148
+
149
+ .. warning::
150
+
151
+ **Experimental, and it is a request rather than a command.**
152
+
153
+ A supply that does not offer the requested voltage simply refuses.
154
+ Nothing is raised in that case and the hub stays where it was, so
155
+ **read the voltage back** to see what actually happened rather than
156
+ assuming the call took effect.
157
+
158
+ Higher PD gears (20 V, 28 V) are deliberately unreachable: the
159
+ hub's voltage sense saturates at 18.81 V, so the firmware could not
160
+ even measure what it had asked for.
161
+
162
+ Renegotiating itself is well behaved in practice: measured working even
163
+ while driving motors on a small (2x18650-class) power bank, and on a
164
+ bench supply switching 9 V to 12 V and back with the hub up throughout.
165
+
166
+ Some power banks that were already switched on when the cable went
167
+ in feed plain 5 V first and then cycle VBUS by themselves a few seconds
168
+ later to start PD; a hub running from USB alone restarts at 9 V then.
169
+ That is the bank's doing, not the request's. Switching such a bank off
170
+ and on before pressing PWR avoids it.
171
+ """
172
+
173
+ def status(self) -> Dict[str, Union[bool, int]]:
174
+ """status() -> Dict
175
+
176
+ What the USB-PD sink negotiated with the supply:
177
+ ``{"pd": bool, "bc": bool, "qc2": bool, "qc3": bool, "epr": bool,
178
+ "max_ma": int, "raw": int, "mv": int}``. ``pd`` is ``True`` under a PD
179
+ contract (the normal 9 V case). ``max_ma`` is the contract's current
180
+ limit (0 without PD), ``raw`` the sink's status register, ``mv`` the
181
+ measured input voltage.
182
+
183
+ Diagnostic: lets a program see whether a contract exists at all.
184
+ """
185
+
186
+
125
187
  class PeakHub:
126
188
  """LEGO-compatible hub by Sciurus Robotics: 8 ports, 5x5 RGB matrix, IMU."""
127
189
 
@@ -135,6 +197,7 @@ class PeakHub:
135
197
  imu = _common.IMU()
136
198
  speaker = _common.Speaker()
137
199
  system = System()
200
+ usb_pd = UsbPd()
138
201
 
139
202
  def __init__(
140
203
  self,
@@ -0,0 +1,8 @@
1
+ """scirodev -- tooling for Sciurus Robotics hubs on top of pybricksdev, and the `sciro` API stubs."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("scirodev")
7
+ except PackageNotFoundError: # running from a source tree that is not installed
8
+ __version__ = "0+unknown"
@@ -0,0 +1,146 @@
1
+ """`scirodev` command line.
2
+
3
+ One tool for Sciurus Robotics hubs (PeakHub so far). It is built on
4
+ pybricksdev: pybricksdev's own tools (run, compile, flash, ...) are offered
5
+ unchanged, next to the tools that only exist for sciro hubs (rename).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import asyncio
12
+ import logging
13
+ import sys
14
+
15
+ from . import __version__
16
+
17
+ DESCRIPTION = """\
18
+ Command line tool for Sciurus Robotics hubs (PeakHub).
19
+
20
+ Built on pybricksdev. Tools marked [pybricksdev] are pybricksdev's own,
21
+ unchanged, and work with any hub running Pybricks firmware. The other tools
22
+ are extensions for sciro hubs.
23
+ """
24
+
25
+ EPILOG = """\
26
+ Run `%(prog)s <tool> --help` for tool-specific arguments.
27
+
28
+ Examples:
29
+ %(prog)s run ble --name Peak-BYN9 program.py
30
+ %(prog)s rename ble MyRobot --name Peak-BYN9
31
+ """
32
+
33
+ # pybricksdev tools to offer, by class name. Looked up by name so a pybricksdev
34
+ # release that adds or drops one does not break us.
35
+ PYBRICKSDEV_TOOLS = ("Compile", "Run", "Flash", "DFU", "OAD", "LWP3", "Udev")
36
+
37
+
38
+ def _pybricksdev_version() -> str:
39
+ from importlib.metadata import PackageNotFoundError, version
40
+
41
+ try:
42
+ return version("pybricksdev")
43
+ except PackageNotFoundError:
44
+ return "?"
45
+
46
+
47
+ def build_parser() -> tuple[argparse.ArgumentParser, argparse._SubParsersAction]:
48
+ import pybricksdev.cli as pbcli
49
+
50
+ from .rename import Rename
51
+
52
+ parser = argparse.ArgumentParser(
53
+ prog="scirodev",
54
+ description=DESCRIPTION,
55
+ epilog=EPILOG,
56
+ formatter_class=argparse.RawDescriptionHelpFormatter,
57
+ )
58
+ parser.add_argument(
59
+ "-v",
60
+ "--version",
61
+ action="version",
62
+ version=f"scirodev v{__version__} (pybricksdev v{_pybricksdev_version()})",
63
+ )
64
+ parser.add_argument("-d", "--debug", action="store_true", help="enable debug logging")
65
+
66
+ subparsers = parser.add_subparsers(metavar="<tool>", dest="tool", help="the tool to use")
67
+
68
+ # Our own tools first.
69
+ for tool in (Rename(),):
70
+ tool.add_parser(subparsers)
71
+ own = len(subparsers._choices_actions)
72
+
73
+ for name in PYBRICKSDEV_TOOLS:
74
+ cls = getattr(pbcli, name, None)
75
+ if cls is not None:
76
+ cls().add_parser(subparsers)
77
+
78
+ # Mark what comes from pybricksdev in the tool list.
79
+ for action in subparsers._choices_actions[own:]:
80
+ action.help = f"{action.help or ''} [pybricksdev]".strip()
81
+
82
+ return parser, subparsers
83
+
84
+
85
+ def pairing_hint(error: str) -> str | None:
86
+ """Plain words for the Bluetooth errors a PIN-protected hub can cause."""
87
+ e = error.lower()
88
+ if "peer removed pairing information" in e:
89
+ return (
90
+ "This computer was paired with the hub, but the hub no longer knows it\n"
91
+ "(its PIN was set or changed). Make the computer forget the hub in its\n"
92
+ "Bluetooth settings, then run the command again and enter the hub's PIN."
93
+ )
94
+ if "authentication" in e or "encryption is insufficient" in e or "pairing" in e:
95
+ return (
96
+ "The hub is protected with a PIN. Enter it in the dialog of your operating\n"
97
+ "system when it appears; if the command gave up meanwhile, run it again.\n"
98
+ "After 3 wrong PINs the hub refuses new computers until its Bluetooth\n"
99
+ "button is switched off and on."
100
+ )
101
+ return None
102
+
103
+
104
+ def main() -> None:
105
+ if sys.platform == "win32":
106
+ # Same workaround as pybricksdev: bad side effects of pythoncom.
107
+ try:
108
+ from bleak_winrt._winrt import MTA, init_apartment
109
+ except ImportError:
110
+ from winrt._winrt import MTA, init_apartment
111
+
112
+ init_apartment(MTA)
113
+
114
+ parser, subparsers = build_parser()
115
+
116
+ try:
117
+ import argcomplete
118
+
119
+ argcomplete.autocomplete(parser)
120
+ except ImportError:
121
+ pass
122
+
123
+ args = parser.parse_args()
124
+
125
+ logging.basicConfig(
126
+ format="%(asctime)s: %(levelname)s: %(name)s: %(message)s",
127
+ level=logging.DEBUG if args.debug else logging.WARNING,
128
+ )
129
+
130
+ if not args.tool:
131
+ parser.error(f'Missing name of tool: {"|".join(subparsers.choices.keys())}')
132
+
133
+ try:
134
+ result = asyncio.run(subparsers.choices[args.tool].tool.run(args))
135
+ except Exception as e:
136
+ hint = pairing_hint(str(e))
137
+ if hint is None or args.debug:
138
+ raise
139
+ print(f"error: {e}\n\n{hint}", file=sys.stderr)
140
+ sys.exit(1)
141
+ if isinstance(result, int) and result:
142
+ sys.exit(result)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -0,0 +1,112 @@
1
+ """`scirodev rename`: give a PeakHub a name of its own (see DESCRIPTION)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import sys
8
+
9
+ # PeakHub extension of the Pybricks command set (PBIO_PYBRICKS_COMMAND_SET_HUB_NAME).
10
+ SET_HUB_NAME = 0xA0
11
+ NAME_MAX = 16
12
+
13
+
14
+ def check_name(name: str) -> str:
15
+ if name == "":
16
+ return name
17
+ if len(name) > NAME_MAX:
18
+ raise argparse.ArgumentTypeError(f"at most {NAME_MAX} characters")
19
+ if any(not (0x20 <= ord(c) <= 0x7E) for c in name):
20
+ raise argparse.ArgumentTypeError("printable ASCII characters only")
21
+ if name != name.strip():
22
+ raise argparse.ArgumentTypeError("no leading or trailing spaces")
23
+ return name
24
+
25
+
26
+ DESCRIPTION = """\
27
+ Give a PeakHub a name of its own.
28
+
29
+ The name is what the hub advertises over Bluetooth (so `scirodev run ble
30
+ --name <name>` finds exactly this hub) and what hub.system.name() returns. It
31
+ is stored on the hub across power cycles and firmware updates.
32
+
33
+ Over USB the hub takes the name at once. Over Bluetooth, where anybody in
34
+ range could send the command, the hub spells the new name on its display,
35
+ shows `?` and applies it only when the centre button is pressed (any other
36
+ button, or 10 s without one, rejects it). The hub must be idle: no program
37
+ running, not in power-save.
38
+ """
39
+
40
+
41
+ class Rename:
42
+ """The `rename` tool (same shape as pybricksdev's Tool classes)."""
43
+
44
+ def add_parser(self, subparsers: argparse._SubParsersAction) -> None:
45
+ p = subparsers.add_parser(
46
+ "rename",
47
+ help="set the name a PeakHub advertises under",
48
+ description=DESCRIPTION,
49
+ formatter_class=argparse.RawDescriptionHelpFormatter,
50
+ )
51
+ p.tool = self
52
+ self.parser = p
53
+ p.add_argument("conntype", choices=["ble", "usb"], help="how to reach the hub")
54
+ p.add_argument(
55
+ "new_name",
56
+ type=check_name,
57
+ nargs="?",
58
+ help=f"the new name: 1 to {NAME_MAX} printable ASCII characters",
59
+ )
60
+ p.add_argument("--default", action="store_true", help="go back to the default name (Peak-XXXX)")
61
+ p.add_argument("-n", "--name", help="current name of the hub to rename (ble; default: first hub found)")
62
+
63
+ async def run(self, args: argparse.Namespace) -> int:
64
+ if args.default == (args.new_name is not None):
65
+ self.parser.error("give either a new name or --default")
66
+ if args.default:
67
+ args.new_name = ""
68
+ elif args.new_name == "":
69
+ self.parser.error("the name must not be empty (use --default for the default name)")
70
+ return await rename(args)
71
+
72
+
73
+ async def rename(args: argparse.Namespace) -> int:
74
+ from pybricksdev.ble.pybricks import PYBRICKS_COMMAND_EVENT_UUID
75
+
76
+ if args.conntype == "ble":
77
+ from pybricksdev.ble import find_device
78
+ from pybricksdev.connections.pybricks import PybricksHubBLE
79
+
80
+ print(f"Searching for {args.name or 'any hub with Pybricks service'}...")
81
+ hub = PybricksHubBLE(await find_device(args.name))
82
+ else:
83
+ from usb.core import find as find_usb
84
+
85
+ from pybricksdev.connections.pybricks import PybricksHubUSB
86
+ from pybricksdev.usb import LEGO_USB_VID
87
+
88
+ device = find_usb(custom_match=lambda d: d.idVendor == LEGO_USB_VID and d.product.endswith("Pybricks"))
89
+ if device is None:
90
+ print("Pybricks Hub not found.", file=sys.stderr)
91
+ return 1
92
+ hub = PybricksHubUSB(device)
93
+
94
+ shown = args.new_name or "the default name"
95
+ await hub.connect()
96
+ try:
97
+ await hub.write_gatt_char(
98
+ PYBRICKS_COMMAND_EVENT_UUID,
99
+ bytes([SET_HUB_NAME]) + args.new_name.encode("ascii"),
100
+ True,
101
+ )
102
+ if args.conntype == "ble":
103
+ print(f"The hub now shows the new name ({shown}) and then '?'.")
104
+ print("Press the centre button on the hub to accept. Any other button, or 10 s, rejects.")
105
+ # Stay connected while the user decides, so nobody else can slip in.
106
+ await asyncio.sleep(len(args.new_name) * 0.6 + 10.5)
107
+ print("Done. If accepted, the hub advertises under the new name from the next connection on.")
108
+ else:
109
+ print(f"Renamed to {shown}.")
110
+ finally:
111
+ await hub.disconnect()
112
+ return 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scirodev
3
- Version: 0.5.6
3
+ Version: 0.7.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
@@ -23,9 +23,14 @@ Dynamic: license-file
23
23
 
24
24
  # scirodev
25
25
 
26
- Host-side companion to the PeakHub firmware: **typed API stubs** for the `sciro`
27
- namespace that PeakHub programs import, plus the hub tooling (it depends on
28
- `pybricksdev`, so `scirodev run ble prog.py` works exactly like `pybricksdev`).
26
+ Host-side companion to Sciurus Robotics hubs (PeakHub so far): **typed API
27
+ stubs** for the `sciro` namespace that PeakHub programs import, plus the
28
+ `scirodev` command line tool.
29
+
30
+ The tool is built on `pybricksdev`. It offers pybricksdev's own tools
31
+ unchanged (`scirodev run ble prog.py` works exactly like `pybricksdev run`),
32
+ and adds tools that only exist for sciro hubs, such as `scirodev rename`.
33
+ `scirodev -h` lists both and marks which is which.
29
34
 
30
35
  ```
31
36
  pip install scirodev # from PyPI
@@ -46,6 +51,7 @@ from sciro.parameters import Port # Port.A .. Port.H (PeakHub has 8 por
46
51
  from sciro.iodevices import PUMPDevice # generic PUMP device access
47
52
  from sciro.pump import FloorPro # PUMP devices (LP-FloorPro, ...)
48
53
  from sciro.hubs import PeakHub # hub class incl. display.device()
54
+ from sciro.canbus import CAN # beta: raw classic-CAN frames on connector 1/2
49
55
  from sciro.tools import RingBuffer # experimental: RAM recorder -> CSV file via the console
50
56
  from sciro.robotics import PIDController # experimental: PID with optional logging
51
57
 
@@ -92,6 +98,30 @@ pybricks.parameters.Port` — the stubs only add what the IDE is missing. The
92
98
  stub files here mirror the frozen modules; a release of this package matches
93
99
  the PeakHub firmware of the same date.
94
100
 
101
+ ## Hub names
102
+
103
+ Every PeakHub advertises under its own name, `Peak-XXXX` out of the box (the
104
+ first four characters of its board ID), so several hubs on one table can be
105
+ told apart:
106
+
107
+ ```
108
+ scirodev run ble --name Peak-BYN9 prog.py
109
+ ```
110
+
111
+ Give a hub a name of your choice (1 to 16 printable ASCII characters); it is
112
+ stored on the hub and survives power cycles and firmware updates:
113
+
114
+ ```
115
+ scirodev rename usb MyRobot # over USB: applied at once
116
+ scirodev rename ble MyRobot -n Peak-BYN9 # over Bluetooth: confirm on the hub
117
+ scirodev rename usb --default # back to Peak-XXXX
118
+ ```
119
+
120
+ Over Bluetooth the hub spells the new name on its display, then shows `?`.
121
+ Press the centre button to accept; any other button, or 10 seconds without
122
+ one, rejects. This keeps somebody else in radio range from renaming your hub.
123
+ `hub.system.name()` returns the name.
124
+
95
125
  ## API reference (HTML)
96
126
 
97
127
  The stubs double as the source of the API reference, built with Sphinx the way
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  sciro/__init__.py
5
5
  sciro/_common.py
6
+ sciro/canbus.py
6
7
  sciro/floorpro.py
7
8
  sciro/hubs.py
8
9
  sciro/iodevices.py
@@ -13,6 +14,7 @@ sciro/robotics.py
13
14
  sciro/tools.py
14
15
  scirodev/__init__.py
15
16
  scirodev/cli.py
17
+ scirodev/rename.py
16
18
  scirodev.egg-info/PKG-INFO
17
19
  scirodev.egg-info/SOURCES.txt
18
20
  scirodev.egg-info/dependency_links.txt
@@ -1,3 +0,0 @@
1
- """scirodev -- PeakHub tooling on top of pybricksdev, and the `sciro` API stubs."""
2
-
3
- __version__ = "0.1.0"
@@ -1,17 +0,0 @@
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()
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