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.
Files changed (57) hide show
  1. wedopy-0.1.0/CHANGELOG.md +21 -0
  2. wedopy-0.1.0/LICENSE +21 -0
  3. wedopy-0.1.0/MANIFEST.in +12 -0
  4. wedopy-0.1.0/PKG-INFO +112 -0
  5. wedopy-0.1.0/README.md +85 -0
  6. wedopy-0.1.0/examples/blink_and_move.py +22 -0
  7. wedopy-0.1.0/examples/distance_sensor.py +18 -0
  8. wedopy-0.1.0/examples/led.py +11 -0
  9. wedopy-0.1.0/examples/motor.py +10 -0
  10. wedopy-0.1.0/examples/tilt_diag.py +22 -0
  11. wedopy-0.1.0/examples/tilt_sensor.py +9 -0
  12. wedopy-0.1.0/examples/two_motors.py +10 -0
  13. wedopy-0.1.0/pyproject.toml +50 -0
  14. wedopy-0.1.0/setup.cfg +4 -0
  15. wedopy-0.1.0/setup.py +15 -0
  16. wedopy-0.1.0/src/wedopy/__init__.py +38 -0
  17. wedopy-0.1.0/src/wedopy/button.py +14 -0
  18. wedopy-0.1.0/src/wedopy/exceptions.py +15 -0
  19. wedopy-0.1.0/src/wedopy/hub.py +146 -0
  20. wedopy-0.1.0/src/wedopy/led.py +30 -0
  21. wedopy-0.1.0/src/wedopy/motor.py +55 -0
  22. wedopy-0.1.0/src/wedopy/sensors.py +96 -0
  23. wedopy-0.1.0/src/wedopy/session.py +88 -0
  24. wedopy-0.1.0/src/wedopy/task.py +59 -0
  25. wedopy-0.1.0/src/wedopy/tasks.py +216 -0
  26. wedopy-0.1.0/src/wedopy/time.py +9 -0
  27. wedopy-0.1.0/tests/conftest.py +44 -0
  28. wedopy-0.1.0/tests/helpers.py +48 -0
  29. wedopy-0.1.0/tests/test_button.py +19 -0
  30. wedopy-0.1.0/tests/test_distance.py +83 -0
  31. wedopy-0.1.0/tests/test_hub.py +170 -0
  32. wedopy-0.1.0/tests/test_led.py +59 -0
  33. wedopy-0.1.0/tests/test_motor.py +71 -0
  34. wedopy-0.1.0/tests/test_protocol.py +73 -0
  35. wedopy-0.1.0/tests/test_safety.py +194 -0
  36. wedopy-0.1.0/tests/test_tasks.py +368 -0
  37. wedopy-0.1.0/tests/test_tilt.py +97 -0
  38. wedopy-0.1.0/wedopy.egg-info/PKG-INFO +112 -0
  39. wedopy-0.1.0/wedopy.egg-info/SOURCES.txt +55 -0
  40. wedopy-0.1.0/wedopy.egg-info/dependency_links.txt +1 -0
  41. wedopy-0.1.0/wedopy.egg-info/requires.txt +5 -0
  42. wedopy-0.1.0/wedopy.egg-info/top_level.txt +2 -0
  43. wedopy-0.1.0/wedopy_core/__init__.py +1 -0
  44. wedopy-0.1.0/wedopy_core/_wedopy.c +45797 -0
  45. wedopy-0.1.0/wedopy_core/_wedopy.pxd +3 -0
  46. wedopy-0.1.0/wedopy_core/_wedopy.pyx +759 -0
  47. wedopy-0.1.0/wedopy_core/exceptions.py +46 -0
  48. wedopy-0.1.0/wedopy_core/protocol/__init__.py +1 -0
  49. wedopy-0.1.0/wedopy_core/protocol/constants.py +79 -0
  50. wedopy-0.1.0/wedopy_core/protocol/uuid.py +36 -0
  51. wedopy-0.1.0/wedopy_core/runtime/__init__.py +1 -0
  52. wedopy-0.1.0/wedopy_core/runtime/supervisor.py +34 -0
  53. wedopy-0.1.0/wedopy_core/runtime/timing.py +17 -0
  54. wedopy-0.1.0/wedopy_core/transport/__init__.py +1 -0
  55. wedopy-0.1.0/wedopy_core/transport/ble.py +163 -0
  56. wedopy-0.1.0/wedopy_core/transport/interfaces.py +45 -0
  57. 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.
@@ -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,11 @@
1
+ """Cycle through the indexed LED colours, then show an RGB colour."""
2
+ import time
3
+
4
+ from wedopy import Hub
5
+
6
+ with Hub() as hub:
7
+ for i in range(1, 11):
8
+ hub.led.index(i)
9
+ time.sleep(0.3)
10
+ hub.led.rgb(255, 80, 0)
11
+ time.sleep(1)
@@ -0,0 +1,10 @@
1
+ """Run motor A for two seconds."""
2
+ import time
3
+
4
+ from wedopy import Hub
5
+
6
+ with Hub() as hub:
7
+ motor = hub.motor("A")
8
+ motor.run(100)
9
+ time.sleep(2)
10
+ 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,9 @@
1
+ """Print the tilt angle and direction."""
2
+ import time
3
+ from wedopy import Hub
4
+
5
+ with Hub() as hub:
6
+ tilt = hub.tilt_sensor("A")
7
+ for _ in range(20):
8
+ print("angle", tilt.read_angle(), "| direction", tilt.read_direction())
9
+ time.sleep(0.25)
@@ -0,0 +1,10 @@
1
+ """Drive two motors together (A first, then B; not atomic), then stop."""
2
+ import time
3
+
4
+ from wedopy import Hub
5
+
6
+ with Hub() as hub:
7
+ motors = hub.motors(("A", "B"))
8
+ motors.run(75)
9
+ time.sleep(2)
10
+ motors.stop()
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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