wedopy 0.1.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.
- wedopy-0.1.0/CHANGELOG.md +21 -0
- wedopy-0.1.0/LICENSE +21 -0
- wedopy-0.1.0/MANIFEST.in +12 -0
- wedopy-0.1.0/PKG-INFO +112 -0
- wedopy-0.1.0/README.md +85 -0
- wedopy-0.1.0/examples/blink_and_move.py +22 -0
- wedopy-0.1.0/examples/distance_sensor.py +18 -0
- wedopy-0.1.0/examples/led.py +11 -0
- wedopy-0.1.0/examples/motor.py +10 -0
- wedopy-0.1.0/examples/tilt_diag.py +22 -0
- wedopy-0.1.0/examples/tilt_sensor.py +9 -0
- wedopy-0.1.0/examples/two_motors.py +10 -0
- wedopy-0.1.0/pyproject.toml +50 -0
- wedopy-0.1.0/setup.cfg +4 -0
- wedopy-0.1.0/setup.py +15 -0
- wedopy-0.1.0/src/wedopy/__init__.py +38 -0
- wedopy-0.1.0/src/wedopy/button.py +14 -0
- wedopy-0.1.0/src/wedopy/exceptions.py +15 -0
- wedopy-0.1.0/src/wedopy/hub.py +146 -0
- wedopy-0.1.0/src/wedopy/led.py +30 -0
- wedopy-0.1.0/src/wedopy/motor.py +55 -0
- wedopy-0.1.0/src/wedopy/sensors.py +96 -0
- wedopy-0.1.0/src/wedopy/session.py +88 -0
- wedopy-0.1.0/src/wedopy/task.py +59 -0
- wedopy-0.1.0/src/wedopy/tasks.py +216 -0
- wedopy-0.1.0/src/wedopy/time.py +9 -0
- wedopy-0.1.0/tests/conftest.py +44 -0
- wedopy-0.1.0/tests/helpers.py +48 -0
- wedopy-0.1.0/tests/test_button.py +19 -0
- wedopy-0.1.0/tests/test_distance.py +83 -0
- wedopy-0.1.0/tests/test_hub.py +170 -0
- wedopy-0.1.0/tests/test_led.py +59 -0
- wedopy-0.1.0/tests/test_motor.py +71 -0
- wedopy-0.1.0/tests/test_protocol.py +73 -0
- wedopy-0.1.0/tests/test_safety.py +194 -0
- wedopy-0.1.0/tests/test_tasks.py +368 -0
- wedopy-0.1.0/tests/test_tilt.py +97 -0
- wedopy-0.1.0/wedopy.egg-info/PKG-INFO +112 -0
- wedopy-0.1.0/wedopy.egg-info/SOURCES.txt +55 -0
- wedopy-0.1.0/wedopy.egg-info/dependency_links.txt +1 -0
- wedopy-0.1.0/wedopy.egg-info/requires.txt +5 -0
- wedopy-0.1.0/wedopy.egg-info/top_level.txt +2 -0
- wedopy-0.1.0/wedopy_core/__init__.py +1 -0
- wedopy-0.1.0/wedopy_core/_wedopy.c +45797 -0
- wedopy-0.1.0/wedopy_core/_wedopy.pxd +3 -0
- wedopy-0.1.0/wedopy_core/_wedopy.pyx +759 -0
- wedopy-0.1.0/wedopy_core/exceptions.py +46 -0
- wedopy-0.1.0/wedopy_core/protocol/__init__.py +1 -0
- wedopy-0.1.0/wedopy_core/protocol/constants.py +79 -0
- wedopy-0.1.0/wedopy_core/protocol/uuid.py +36 -0
- wedopy-0.1.0/wedopy_core/runtime/__init__.py +1 -0
- wedopy-0.1.0/wedopy_core/runtime/supervisor.py +34 -0
- wedopy-0.1.0/wedopy_core/runtime/timing.py +17 -0
- wedopy-0.1.0/wedopy_core/transport/__init__.py +1 -0
- wedopy-0.1.0/wedopy_core/transport/ble.py +163 -0
- wedopy-0.1.0/wedopy_core/transport/interfaces.py +45 -0
- wedopy-0.1.0/wedopy_core/transport/simulated.py +223 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0.dev0
|
|
4
|
+
|
|
5
|
+
First implementation of design spec 1.2.0 (`WeDo_2_0_con_Cython.md`).
|
|
6
|
+
|
|
7
|
+
- Cython core `wedopy_core._wedopy`: protocol codecs, state machine, single-writer coordinator
|
|
8
|
+
(normal/emergency queues), heartbeat and watchdog.
|
|
9
|
+
- Public API: `Hub`, `Motor`, `MotorGroup`, `DistanceSensor`, `TiltSensor`, `LED`, `Button`,
|
|
10
|
+
cooperative tasks (`hub.tasks`, `task.sleep`, `task.yield_now`), `running_time`.
|
|
11
|
+
- BLE backend `BleakTransport` (Linux/BlueZ first) and `SimulatedHub` transport for tests.
|
|
12
|
+
- Validated on real WeDo 2.0 hardware (Nov 2026): compiled build, `BleakTransport`
|
|
13
|
+
(Linux/BlueZ), port discovery, tilt sensor (direction and angle), distance, LED,
|
|
14
|
+
motor, battery, button.
|
|
15
|
+
- Tilt sensor: angle mode uses `UNIT_RAW` with `expected_len=2` (`int8` pairs).
|
|
16
|
+
`UNIT_SI` makes the hub silently ignore the configuration and never notify.
|
|
17
|
+
- Tilt bump mode (2) is not implemented by the hub firmware; documented as a spec
|
|
18
|
+
artifact, not a wedopy bug.
|
|
19
|
+
- Not yet validated on hardware: watchdog timings under a real BLE stall,
|
|
20
|
+
`refresh_ports()` replay, behaviour when the process is killed with the link
|
|
21
|
+
open, and the `strong` stop profile (not implemented).
|
wedopy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wedopy authors
|
|
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.
|
wedopy-0.1.0/MANIFEST.in
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
include LICENSE
|
|
2
|
+
include README.md
|
|
3
|
+
include CHANGELOG.md
|
|
4
|
+
include pyproject.toml
|
|
5
|
+
include setup.py
|
|
6
|
+
include wedopy_core/_wedopy.pyx
|
|
7
|
+
include wedopy_core/_wedopy.pxd
|
|
8
|
+
include wedopy_core/_wedopy.c
|
|
9
|
+
recursive-include wedopy_core *.py
|
|
10
|
+
recursive-include src *.py
|
|
11
|
+
recursive-include tests *.py
|
|
12
|
+
recursive-include examples *.py
|
wedopy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wedopy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library for the LEGO WeDo 2.0 Smart Hub over BLE (API in Python, core in Cython)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: lego,wedo,wedo2,ble,bluetooth,robotics,iot,cython
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
11
|
+
Classifier: Topic :: System :: Hardware
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: bleak>=0.22
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: Cython>=3.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# wedopy
|
|
29
|
+
|
|
30
|
+
Python library for the **LEGO WeDo 2.0 Smart Hub** over Bluetooth Low Energy.
|
|
31
|
+
Public API in Python, execution core in Cython. Implements design spec 1.2.0.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from wedopy import Hub, task
|
|
35
|
+
|
|
36
|
+
with Hub() as hub:
|
|
37
|
+
motor = hub.motor("A")
|
|
38
|
+
motor.run(100)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install (development)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m venv .venv && source .venv/bin/activate
|
|
45
|
+
python -m pip install -e ".[dev]" # builds the Cython extension, installs bleak
|
|
46
|
+
python -m pytest
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or with uv:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync
|
|
53
|
+
uv run pytest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Linux/BlueZ is the first supported platform (BLE through `bleak`).
|
|
57
|
+
|
|
58
|
+
## Layout
|
|
59
|
+
|
|
60
|
+
| Path | Contents |
|
|
61
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `src/wedopy/` | Public API: `Hub`, `Motor`, `MotorGroup`, sensors, `LED`, `Button`, `task`, `tasks`, exceptions |
|
|
63
|
+
| `wedopy_core/_wedopy.pyx` | Cython core: protocol codecs, state machine, single-writer coordinator, heartbeat, watchdog |
|
|
64
|
+
| `wedopy_core/transport/` | `Transport` interface, `BleakTransport` (BLE), `SimulatedHub` (tests) |
|
|
65
|
+
| `wedopy_core/protocol/` | UUIDs and constants |
|
|
66
|
+
| `wedopy_core/runtime/` | `Timing` parameters, process `supervisor` |
|
|
67
|
+
| `tests/`, `examples/` | pytest suite (simulated hub), runnable examples |
|
|
68
|
+
|
|
69
|
+
Deviations from the spec's tree: `src/wedo/` is `src/wedopy/` (the import name is `wedopy`); the
|
|
70
|
+
compiled module is `wedopy_core._wedopy`; protocol codecs live in the Cython core instead of
|
|
71
|
+
separate `.py` files; exceptions are defined in `wedopy_core/exceptions.py` and re-exported by
|
|
72
|
+
`wedopy.exceptions`, so the core never imports the public package.
|
|
73
|
+
|
|
74
|
+
## Behaviour worth knowing
|
|
75
|
+
|
|
76
|
+
- Every BLE operation goes through one coordinator thread (single writer). Writes and the
|
|
77
|
+
heartbeat are always _with response_; there is no silent fallback to write-without-response.
|
|
78
|
+
- While any motor has a non-zero speed, a heartbeat (Battery Level read) runs every 0.5 s. With
|
|
79
|
+
no valid ACK for 2 s the watchdog stops the motors with emergency priority (2 attempts), logs
|
|
80
|
+
the result on `wedopy.watchdog`, and moves the hub to `error`. The watchdog and heartbeat run on
|
|
81
|
+
their own thread, so a blocked task scheduler cannot disable them.
|
|
82
|
+
- Normal operations time out after 1 s, which bounds how long an emergency request can wait
|
|
83
|
+
behind an in-flight operation (an in-flight BLE call cannot be aborted).
|
|
84
|
+
- Stop profile is `nobarrier`. `strong` is not offered.
|
|
85
|
+
- `hub.tasks.cancel()` never stops motors; protect motors with `try/finally`, or call
|
|
86
|
+
`hub.stop_all()`. Leaving `with Hub()` cancels tasks first, then stops motors.
|
|
87
|
+
- Logging goes through the standard `logging` module (`wedopy`, `wedopy.watchdog`, `wedopy.tasks`).
|
|
88
|
+
- Sensors only notify on change. `read_angle()`, `read_direction()` and the distance sensor
|
|
89
|
+
return the _last_ value; if the physical value has not changed since the last notification,
|
|
90
|
+
the call blocks until the sensor sends a new sample or the timeout fires. Move/incline the
|
|
91
|
+
sensor (or expose it to a new distance) after configuring it.
|
|
92
|
+
- The tilt sensor is a 4-position switch, not an accelerometer. Its direction mode (1) uses
|
|
93
|
+
`UNIT_SI` (4-byte `float32`); its angle mode (0) requires `UNIT_RAW` (2-byte `int8` pair)
|
|
94
|
+
— with `UNIT_SI` the hub silently ignores the configuration. Bump mode (2) is accepted by
|
|
95
|
+
the hub but never emits; the firmware does not implement it.
|
|
96
|
+
|
|
97
|
+
## Status
|
|
98
|
+
|
|
99
|
+
Verified with the simulated hub and on real WeDo 2.0 hardware (Nov 2026):
|
|
100
|
+
|
|
101
|
+
- Compiled Cython build (`wedopy_core._wedopy`).
|
|
102
|
+
- `BleakTransport` on Linux/BlueZ: connect, port discovery, notifications.
|
|
103
|
+
- Tilt sensor: direction mode (1, `UNIT_SI`) and angle mode (0, `UNIT_RAW`).
|
|
104
|
+
- Battery, button, LED, motor, distance sensor.
|
|
105
|
+
|
|
106
|
+
**Not yet verified on hardware:** watchdog timings under a real BLE stall,
|
|
107
|
+
`refresh_ports()` replay, behaviour when the process is killed with the link
|
|
108
|
+
open, and the `strong` stop profile (not implemented).
|
|
109
|
+
|
|
110
|
+
`wedopy_core/_wedopy.pyx` is written in the subset of Cython that is also valid Python, so
|
|
111
|
+
`tests/conftest.py` can run it uncompiled when the extension is not built (it warns). Typed
|
|
112
|
+
`cdef` optimisations can be added later once profiling shows they matter.
|
wedopy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# wedopy
|
|
2
|
+
|
|
3
|
+
Python library for the **LEGO WeDo 2.0 Smart Hub** over Bluetooth Low Energy.
|
|
4
|
+
Public API in Python, execution core in Cython. Implements design spec 1.2.0.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from wedopy import Hub, task
|
|
8
|
+
|
|
9
|
+
with Hub() as hub:
|
|
10
|
+
motor = hub.motor("A")
|
|
11
|
+
motor.run(100)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install (development)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m venv .venv && source .venv/bin/activate
|
|
18
|
+
python -m pip install -e ".[dev]" # builds the Cython extension, installs bleak
|
|
19
|
+
python -m pytest
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with uv:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv sync
|
|
26
|
+
uv run pytest
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Linux/BlueZ is the first supported platform (BLE through `bleak`).
|
|
30
|
+
|
|
31
|
+
## Layout
|
|
32
|
+
|
|
33
|
+
| Path | Contents |
|
|
34
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `src/wedopy/` | Public API: `Hub`, `Motor`, `MotorGroup`, sensors, `LED`, `Button`, `task`, `tasks`, exceptions |
|
|
36
|
+
| `wedopy_core/_wedopy.pyx` | Cython core: protocol codecs, state machine, single-writer coordinator, heartbeat, watchdog |
|
|
37
|
+
| `wedopy_core/transport/` | `Transport` interface, `BleakTransport` (BLE), `SimulatedHub` (tests) |
|
|
38
|
+
| `wedopy_core/protocol/` | UUIDs and constants |
|
|
39
|
+
| `wedopy_core/runtime/` | `Timing` parameters, process `supervisor` |
|
|
40
|
+
| `tests/`, `examples/` | pytest suite (simulated hub), runnable examples |
|
|
41
|
+
|
|
42
|
+
Deviations from the spec's tree: `src/wedo/` is `src/wedopy/` (the import name is `wedopy`); the
|
|
43
|
+
compiled module is `wedopy_core._wedopy`; protocol codecs live in the Cython core instead of
|
|
44
|
+
separate `.py` files; exceptions are defined in `wedopy_core/exceptions.py` and re-exported by
|
|
45
|
+
`wedopy.exceptions`, so the core never imports the public package.
|
|
46
|
+
|
|
47
|
+
## Behaviour worth knowing
|
|
48
|
+
|
|
49
|
+
- Every BLE operation goes through one coordinator thread (single writer). Writes and the
|
|
50
|
+
heartbeat are always _with response_; there is no silent fallback to write-without-response.
|
|
51
|
+
- While any motor has a non-zero speed, a heartbeat (Battery Level read) runs every 0.5 s. With
|
|
52
|
+
no valid ACK for 2 s the watchdog stops the motors with emergency priority (2 attempts), logs
|
|
53
|
+
the result on `wedopy.watchdog`, and moves the hub to `error`. The watchdog and heartbeat run on
|
|
54
|
+
their own thread, so a blocked task scheduler cannot disable them.
|
|
55
|
+
- Normal operations time out after 1 s, which bounds how long an emergency request can wait
|
|
56
|
+
behind an in-flight operation (an in-flight BLE call cannot be aborted).
|
|
57
|
+
- Stop profile is `nobarrier`. `strong` is not offered.
|
|
58
|
+
- `hub.tasks.cancel()` never stops motors; protect motors with `try/finally`, or call
|
|
59
|
+
`hub.stop_all()`. Leaving `with Hub()` cancels tasks first, then stops motors.
|
|
60
|
+
- Logging goes through the standard `logging` module (`wedopy`, `wedopy.watchdog`, `wedopy.tasks`).
|
|
61
|
+
- Sensors only notify on change. `read_angle()`, `read_direction()` and the distance sensor
|
|
62
|
+
return the _last_ value; if the physical value has not changed since the last notification,
|
|
63
|
+
the call blocks until the sensor sends a new sample or the timeout fires. Move/incline the
|
|
64
|
+
sensor (or expose it to a new distance) after configuring it.
|
|
65
|
+
- The tilt sensor is a 4-position switch, not an accelerometer. Its direction mode (1) uses
|
|
66
|
+
`UNIT_SI` (4-byte `float32`); its angle mode (0) requires `UNIT_RAW` (2-byte `int8` pair)
|
|
67
|
+
— with `UNIT_SI` the hub silently ignores the configuration. Bump mode (2) is accepted by
|
|
68
|
+
the hub but never emits; the firmware does not implement it.
|
|
69
|
+
|
|
70
|
+
## Status
|
|
71
|
+
|
|
72
|
+
Verified with the simulated hub and on real WeDo 2.0 hardware (Nov 2026):
|
|
73
|
+
|
|
74
|
+
- Compiled Cython build (`wedopy_core._wedopy`).
|
|
75
|
+
- `BleakTransport` on Linux/BlueZ: connect, port discovery, notifications.
|
|
76
|
+
- Tilt sensor: direction mode (1, `UNIT_SI`) and angle mode (0, `UNIT_RAW`).
|
|
77
|
+
- Battery, button, LED, motor, distance sensor.
|
|
78
|
+
|
|
79
|
+
**Not yet verified on hardware:** watchdog timings under a real BLE stall,
|
|
80
|
+
`refresh_ports()` replay, behaviour when the process is killed with the link
|
|
81
|
+
open, and the `strong` stop profile (not implemented).
|
|
82
|
+
|
|
83
|
+
`wedopy_core/_wedopy.pyx` is written in the subset of Cython that is also valid Python, so
|
|
84
|
+
`tests/conftest.py` can run it uncompiled when the extension is not built (it warns). Typed
|
|
85
|
+
`cdef` optimisations can be added later once profiling shows they matter.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Blink the LED while a motor runs, using cooperative tasks (no threads, no asyncio)."""
|
|
2
|
+
from wedopy import Hub, task
|
|
3
|
+
|
|
4
|
+
with Hub() as hub:
|
|
5
|
+
def blink():
|
|
6
|
+
while True:
|
|
7
|
+
hub.led.index(6)
|
|
8
|
+
yield task.sleep(0.3)
|
|
9
|
+
hub.led.index(9)
|
|
10
|
+
yield task.sleep(0.3)
|
|
11
|
+
|
|
12
|
+
def move():
|
|
13
|
+
motor = hub.motor("A")
|
|
14
|
+
try: # cancelling a task never stops the motor by itself
|
|
15
|
+
motor.run(75)
|
|
16
|
+
yield task.sleep(3)
|
|
17
|
+
finally:
|
|
18
|
+
motor.stop()
|
|
19
|
+
|
|
20
|
+
hub.tasks.add(blink, background=True) # does not keep wait() alive
|
|
21
|
+
hub.tasks.add(move)
|
|
22
|
+
hub.tasks.wait() # returns when move() ends; blink is cancelled
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Drive until the distance sensor reports something nearer than 3 (scale 0..10)."""
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from wedopy import Hub, OperationTimeout
|
|
5
|
+
|
|
6
|
+
with Hub() as hub:
|
|
7
|
+
sensor = hub.distance_sensor("B")
|
|
8
|
+
motor = hub.motor("A")
|
|
9
|
+
motor.run(70)
|
|
10
|
+
while True:
|
|
11
|
+
try:
|
|
12
|
+
if sensor.is_near(3):
|
|
13
|
+
break
|
|
14
|
+
except OperationTimeout:
|
|
15
|
+
print("no reading from the sensor; stopping")
|
|
16
|
+
break
|
|
17
|
+
time.sleep(0.05)
|
|
18
|
+
motor.stop()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from wedopy import Hub
|
|
3
|
+
from wedopy_core.protocol import constants as K
|
|
4
|
+
from wedopy_core import _wedopy as core
|
|
5
|
+
|
|
6
|
+
_orig = core.Coordinator._on_notify
|
|
7
|
+
def _patched(self, uuid, data):
|
|
8
|
+
if uuid == K.CHAR_SENSOR:
|
|
9
|
+
print(f"NOTIFY conn={data[1]:02x} value={data[2:].hex()} len={len(data)-2}")
|
|
10
|
+
return _orig(self, uuid, data)
|
|
11
|
+
core.Coordinator._on_notify = _patched
|
|
12
|
+
|
|
13
|
+
with Hub() as hub:
|
|
14
|
+
t = hub.tilt_sensor("A")
|
|
15
|
+
print(">>> mueve el sensor en X e Y por separado <<<")
|
|
16
|
+
for i in range(15):
|
|
17
|
+
raw = hub._session.core.read_sensor(
|
|
18
|
+
t._conn, K.TYPE_TILT, K.TILT_ANGLE, K.UNIT_RAW,
|
|
19
|
+
expected_len=None, timeout=3.0,
|
|
20
|
+
)
|
|
21
|
+
print(f"[{i}] raw={raw.hex()} len={len(raw)}")
|
|
22
|
+
time.sleep(1)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel", "Cython>=3.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wedopy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python library for the LEGO WeDo 2.0 Smart Hub over BLE (API in Python, core in Cython)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = ["bleak>=0.22"]
|
|
14
|
+
keywords = ["lego", "wedo", "wedo2", "ble", "bluetooth", "robotics", "iot", "cython"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
20
|
+
"Topic :: System :: Hardware",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Operating System :: POSIX :: Linux",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=7", "Cython>=3.0"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = { "wedopy" = "src/wedopy", "wedopy_core" = "wedopy_core" }
|
|
36
|
+
packages = [
|
|
37
|
+
"wedopy",
|
|
38
|
+
"wedopy_core",
|
|
39
|
+
"wedopy_core.protocol",
|
|
40
|
+
"wedopy_core.transport",
|
|
41
|
+
"wedopy_core.runtime",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=8.4.2",
|
|
50
|
+
]
|
wedopy-0.1.0/setup.cfg
ADDED
wedopy-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Build script: compiles the private Cython core ``wedopy_core._wedopy``."""
|
|
2
|
+
from setuptools import Extension, setup
|
|
3
|
+
from Cython.Build import cythonize
|
|
4
|
+
|
|
5
|
+
extensions = [
|
|
6
|
+
Extension("wedopy_core._wedopy", ["wedopy_core/_wedopy.pyx"]),
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
setup(
|
|
10
|
+
ext_modules=cythonize(
|
|
11
|
+
extensions,
|
|
12
|
+
language_level=3,
|
|
13
|
+
compiler_directives={"binding": True},
|
|
14
|
+
),
|
|
15
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""wedopy: control the LEGO WeDo 2.0 Smart Hub from Python.
|
|
2
|
+
|
|
3
|
+
from wedopy import Hub
|
|
4
|
+
|
|
5
|
+
with Hub() as hub:
|
|
6
|
+
motor = hub.motor("A")
|
|
7
|
+
motor.run(100)
|
|
8
|
+
"""
|
|
9
|
+
from . import task
|
|
10
|
+
from .button import Button
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
AmbiguousDeviceError,
|
|
13
|
+
BLEError,
|
|
14
|
+
DeviceNotFoundError,
|
|
15
|
+
DeviceTypeError,
|
|
16
|
+
HubBusyError,
|
|
17
|
+
HubConnectionError,
|
|
18
|
+
HubDisconnectedError,
|
|
19
|
+
InvalidStateError,
|
|
20
|
+
OperationTimeout,
|
|
21
|
+
UnsupportedFeatureError,
|
|
22
|
+
WedoError,
|
|
23
|
+
)
|
|
24
|
+
from .hub import Hub
|
|
25
|
+
from .led import LED
|
|
26
|
+
from .motor import Motor, MotorGroup
|
|
27
|
+
from .sensors import DistanceSensor, TiltSensor
|
|
28
|
+
from .time import running_time
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0.dev0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Hub", "Motor", "MotorGroup", "DistanceSensor", "TiltSensor", "LED", "Button",
|
|
34
|
+
"task", "running_time",
|
|
35
|
+
"WedoError", "BLEError", "HubConnectionError", "HubDisconnectedError", "OperationTimeout",
|
|
36
|
+
"HubBusyError", "DeviceNotFoundError", "DeviceTypeError", "AmbiguousDeviceError",
|
|
37
|
+
"InvalidStateError", "UnsupportedFeatureError",
|
|
38
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Built-in button (spec 5.6)."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Button:
|
|
5
|
+
def __init__(self, session):
|
|
6
|
+
self._session = session
|
|
7
|
+
|
|
8
|
+
@property
|
|
9
|
+
def is_pressed(self):
|
|
10
|
+
return self._session.core.button_pressed()
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def is_released(self):
|
|
14
|
+
return not self.is_pressed
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Public exceptions. The classes are defined in ``wedopy_core.exceptions`` (so the core does
|
|
2
|
+
not depend on this package) and re-exported here."""
|
|
3
|
+
from wedopy_core import exceptions as _core
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"WedoError", "BLEError", "HubConnectionError", "HubDisconnectedError", "OperationTimeout",
|
|
7
|
+
"HubBusyError", "DeviceNotFoundError", "DeviceTypeError", "AmbiguousDeviceError",
|
|
8
|
+
"InvalidStateError", "UnsupportedFeatureError",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
for _name in __all__:
|
|
12
|
+
_cls = getattr(_core, _name)
|
|
13
|
+
_cls.__module__ = __name__
|
|
14
|
+
globals()[_name] = _cls
|
|
15
|
+
del _name, _cls
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""``Hub``: the physical WeDo 2.0 Smart Hub (spec 5.1)."""
|
|
2
|
+
from wedopy_core.exceptions import (
|
|
3
|
+
AmbiguousDeviceError,
|
|
4
|
+
DeviceNotFoundError,
|
|
5
|
+
DeviceTypeError,
|
|
6
|
+
)
|
|
7
|
+
from wedopy_core.protocol import constants as K
|
|
8
|
+
|
|
9
|
+
from .button import Button
|
|
10
|
+
from .led import LED
|
|
11
|
+
from .motor import Motor, MotorGroup
|
|
12
|
+
from .sensors import DistanceSensor, TiltSensor
|
|
13
|
+
from .session import Session
|
|
14
|
+
|
|
15
|
+
_PORTS = {"A": K.CONN_PORT_A, "B": K.CONN_PORT_B}
|
|
16
|
+
_CONN_PORT = {v: k for k, v in _PORTS.items()}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _port_conn(port):
|
|
20
|
+
if not isinstance(port, str):
|
|
21
|
+
raise TypeError("port must be 'A' or 'B', not %s" % type(port).__name__)
|
|
22
|
+
conn = _PORTS.get(port.upper())
|
|
23
|
+
if conn is None:
|
|
24
|
+
raise ValueError("port must be 'A' or 'B', got %r" % port)
|
|
25
|
+
return conn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Hub:
|
|
29
|
+
"""Create it, then ``connect()`` or use it as a context manager (recommended)::
|
|
30
|
+
|
|
31
|
+
with Hub() as hub:
|
|
32
|
+
hub.motor("A").run(100)
|
|
33
|
+
|
|
34
|
+
``transport`` and ``timing`` are for tests and tuning; leave them out in normal use.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, *, transport=None, timing=None):
|
|
38
|
+
self._session = Session(transport, timing)
|
|
39
|
+
self._led = LED(self._session)
|
|
40
|
+
self._button = Button(self._session)
|
|
41
|
+
|
|
42
|
+
# -- lifecycle ----------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def connect(self):
|
|
45
|
+
"""Connect to the first WeDo 2.0 hub found. Returns ``self``."""
|
|
46
|
+
self._session.connect()
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_connected(self):
|
|
51
|
+
return self._session.is_connected
|
|
52
|
+
|
|
53
|
+
def disconnect(self):
|
|
54
|
+
"""Cancel tasks, try to stop the motors, close BLE. Safe to call twice."""
|
|
55
|
+
self._session.disconnect()
|
|
56
|
+
|
|
57
|
+
def shutdown(self):
|
|
58
|
+
"""Ask the hub to power off. It must be switched on by hand afterwards."""
|
|
59
|
+
self._session.shutdown()
|
|
60
|
+
|
|
61
|
+
def stop_all(self):
|
|
62
|
+
"""Ask every motor to stop (``nobarrier`` profile: no ordering guarantee against other orders)."""
|
|
63
|
+
self._session.stop_all()
|
|
64
|
+
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
return self.connect()
|
|
67
|
+
|
|
68
|
+
def __exit__(self, exc_type, exc, tb):
|
|
69
|
+
try:
|
|
70
|
+
self.disconnect()
|
|
71
|
+
except Exception:
|
|
72
|
+
if exc_type is None:
|
|
73
|
+
raise
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
# -- ports and devices ----------------------------------------------------
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def ports(self):
|
|
80
|
+
"""Snapshot of the known external devices, e.g. ``{"A": "motor", "B": "distance"}``."""
|
|
81
|
+
raw = self._session.ports_raw()
|
|
82
|
+
return {name: K.TYPE_NAMES.get(raw[conn], "unknown(0x%02x)" % raw[conn])
|
|
83
|
+
for name, conn in _PORTS.items() if conn in raw}
|
|
84
|
+
|
|
85
|
+
def refresh_ports(self):
|
|
86
|
+
"""Ask the hub to report its devices again and return the new snapshot."""
|
|
87
|
+
self._session.refresh_ports()
|
|
88
|
+
return self.ports
|
|
89
|
+
|
|
90
|
+
def _select(self, type_code, port):
|
|
91
|
+
self._session.core.check_usable()
|
|
92
|
+
raw = self._session.ports_raw()
|
|
93
|
+
wanted = K.TYPE_NAMES[type_code]
|
|
94
|
+
if port is not None:
|
|
95
|
+
conn = _port_conn(port)
|
|
96
|
+
found = raw.get(conn)
|
|
97
|
+
label = port.upper()
|
|
98
|
+
if found is None:
|
|
99
|
+
raise DeviceNotFoundError("nothing is connected to port %s" % label)
|
|
100
|
+
if found != type_code:
|
|
101
|
+
raise DeviceTypeError("port %s holds a %s, not a %s" % (
|
|
102
|
+
label, K.TYPE_NAMES.get(found, "unknown device"), wanted))
|
|
103
|
+
return conn
|
|
104
|
+
candidates = [c for c in (K.CONN_PORT_A, K.CONN_PORT_B) if raw.get(c) == type_code]
|
|
105
|
+
if not candidates:
|
|
106
|
+
raise DeviceNotFoundError("no %s is connected" % wanted)
|
|
107
|
+
if len(candidates) > 1:
|
|
108
|
+
raise AmbiguousDeviceError("more than one %s is connected; pass the port ('A' or 'B')" % wanted)
|
|
109
|
+
return candidates[0]
|
|
110
|
+
|
|
111
|
+
def motor(self, port=None):
|
|
112
|
+
conn = self._select(K.TYPE_MOTOR, port)
|
|
113
|
+
return Motor(self._session, conn, _CONN_PORT[conn])
|
|
114
|
+
|
|
115
|
+
def motors(self, ports):
|
|
116
|
+
"""A group of motors, e.g. ``hub.motors(("A", "B"))``; driven A first, then B."""
|
|
117
|
+
if isinstance(ports, str):
|
|
118
|
+
raise TypeError("ports must be a sequence such as ('A', 'B')")
|
|
119
|
+
ports = list(ports)
|
|
120
|
+
if not ports:
|
|
121
|
+
raise ValueError("ports must not be empty")
|
|
122
|
+
conns = [_port_conn(p) for p in ports]
|
|
123
|
+
if len(set(conns)) != len(conns):
|
|
124
|
+
raise ValueError("duplicate port in %r" % (ports,))
|
|
125
|
+
return MotorGroup([self.motor(_CONN_PORT[c]) for c in conns])
|
|
126
|
+
|
|
127
|
+
def distance_sensor(self, port=None):
|
|
128
|
+
conn = self._select(K.TYPE_MOTION, port)
|
|
129
|
+
return DistanceSensor(self._session, conn, _CONN_PORT[conn])
|
|
130
|
+
|
|
131
|
+
def tilt_sensor(self, port=None):
|
|
132
|
+
conn = self._select(K.TYPE_TILT, port)
|
|
133
|
+
return TiltSensor(self._session, conn, _CONN_PORT[conn])
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def led(self):
|
|
137
|
+
return self._led
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def button(self):
|
|
141
|
+
return self._button
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def tasks(self):
|
|
145
|
+
"""Cooperative task manager of this hub (``add`` / ``wait`` / ``cancel``)."""
|
|
146
|
+
return self._session.tasks
|