btbricks 0.2.1__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.
- btbricks/__init__.py +52 -0
- btbricks/bt.py +1197 -0
- btbricks/bthub.py +218 -0
- btbricks/ctrl_plus.py +8 -0
- btbricks-0.2.1.dist-info/METADATA +232 -0
- btbricks-0.2.1.dist-info/RECORD +13 -0
- btbricks-0.2.1.dist-info/WHEEL +5 -0
- btbricks-0.2.1.dist-info/licenses/LICENSE +21 -0
- btbricks-0.2.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_bthub.py +102 -0
- tests/test_constants.py +77 -0
- tests/test_imports.py +94 -0
btbricks/bthub.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
try:
|
|
3
|
+
from micropython import const
|
|
4
|
+
from time import sleep_ms
|
|
5
|
+
except:
|
|
6
|
+
def const(x):
|
|
7
|
+
return x
|
|
8
|
+
def sleep_ms(x):
|
|
9
|
+
pass
|
|
10
|
+
try:
|
|
11
|
+
from .bt import BLEHandler
|
|
12
|
+
except:
|
|
13
|
+
from bt import BLEHandler
|
|
14
|
+
|
|
15
|
+
__HUB_NOTIFY_DESC = const(0x0F)
|
|
16
|
+
__REMOTE_NOTIFY_DESC = const(0x0C)
|
|
17
|
+
__MARIO_NOTIFY_DESC = const(14)
|
|
18
|
+
__HUB_PORT_ACC = const(0x61)
|
|
19
|
+
__HUB_PORT_GYRO = const(0x62)
|
|
20
|
+
__HUB_PORT_TILT = const(0x63)
|
|
21
|
+
|
|
22
|
+
_MODE = const(0)
|
|
23
|
+
_MODE_BYTE = const(1)
|
|
24
|
+
_MODE_DATA_SETS = const(2)
|
|
25
|
+
_MODE_DATA_SET_TYPE = const(3)
|
|
26
|
+
|
|
27
|
+
OFF = const(0)
|
|
28
|
+
PINK = const(1)
|
|
29
|
+
PURPLE = const(2)
|
|
30
|
+
DARK_BLUE = const(3)
|
|
31
|
+
BLUE = const(4)
|
|
32
|
+
TEAL = const(5)
|
|
33
|
+
GREEN = const(6)
|
|
34
|
+
YELLOW = const(7)
|
|
35
|
+
ORANGE = const(8)
|
|
36
|
+
RED = const(9)
|
|
37
|
+
WHITE = const(10)
|
|
38
|
+
|
|
39
|
+
def clamp_int(n, floor=-100, ceiling=100):
|
|
40
|
+
return max(min(round(n), ceiling), floor)
|
|
41
|
+
|
|
42
|
+
class BtHub():
|
|
43
|
+
"""
|
|
44
|
+
BtHub
|
|
45
|
+
|
|
46
|
+
A class for connecting to and controlling LEGO Hub devices via Bluetooth Low Energy (BLE).
|
|
47
|
+
|
|
48
|
+
This class provides an interface to communicate with LEGO Smart Hubs running standard LEGO firmware.
|
|
49
|
+
It handles BLE connection management, motor control, sensor data subscription, and LED control.
|
|
50
|
+
|
|
51
|
+
:param ble_handler: Optional BLEHandler instance for managing BLE connections. If None, a new BLEHandler will be created.
|
|
52
|
+
:type ble_handler: BLEHandler, optional
|
|
53
|
+
|
|
54
|
+
:raises ConnectionError: If the BLE connection to the hub fails.
|
|
55
|
+
|
|
56
|
+
Example::
|
|
57
|
+
|
|
58
|
+
hub = BtHub()
|
|
59
|
+
hub.connect()
|
|
60
|
+
hub.dc(1, 50) # Run motor on port 1 at 50%
|
|
61
|
+
acceleration = hub.acc() # Get accelerometer data
|
|
62
|
+
hub.disconnect()
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
ble_handler (BLEHandler): Handler for BLE communication operations
|
|
66
|
+
_conn_handle: Internal connection handle for the active BLE connection
|
|
67
|
+
acc_sub (bool): Flag indicating if accelerometer subscription is active
|
|
68
|
+
gyro_sub (bool): Flag indicating if gyroscope subscription is active
|
|
69
|
+
tilt_sub (bool): Flag indicating if tilt subscription is active
|
|
70
|
+
hub_data (dict): Dictionary storing sensor data by port
|
|
71
|
+
mode_info (dict): Dictionary storing mode information by port
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
__PORTS = {
|
|
75
|
+
1:0, 2:1, 3:2, 4:3,
|
|
76
|
+
"A":0, "B":1, "C":2, "D":3}
|
|
77
|
+
|
|
78
|
+
def __init__(self, ble_handler:BLEHandler=None):
|
|
79
|
+
if ble_handler is None:
|
|
80
|
+
ble_handler = BLEHandler()
|
|
81
|
+
self.ble_handler = ble_handler
|
|
82
|
+
self._conn_handle = None
|
|
83
|
+
self.acc_sub = False
|
|
84
|
+
self.gyro_sub = False
|
|
85
|
+
self.tilt_sub = False
|
|
86
|
+
self.hub_data = {}
|
|
87
|
+
self.mode_info = {}
|
|
88
|
+
|
|
89
|
+
def is_connected(self):
|
|
90
|
+
return self._conn_handle is not None
|
|
91
|
+
|
|
92
|
+
def connect(self):
|
|
93
|
+
self._conn_handle = self.ble_handler.connect_lego()
|
|
94
|
+
if self._conn_handle is not None:
|
|
95
|
+
sleep_ms(500)
|
|
96
|
+
# Subscribe to motion data of SMART Hubs
|
|
97
|
+
self.write(0x0A, 0x00, 0x41, __HUB_PORT_ACC, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01)
|
|
98
|
+
sleep_ms(200)
|
|
99
|
+
self.write(0x0A, 0x00, 0x41, __HUB_PORT_GYRO, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01)
|
|
100
|
+
sleep_ms(200)
|
|
101
|
+
self.write(0x0A, 0x00, 0x41, __HUB_PORT_TILT, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01)
|
|
102
|
+
sleep_ms(200)
|
|
103
|
+
|
|
104
|
+
# Initialize all ports with mode 0
|
|
105
|
+
mode = 0
|
|
106
|
+
for i in range(4):
|
|
107
|
+
# SUBSCRIBE_MODE
|
|
108
|
+
self.write(0x0A,0x00,0x41, i, mode, 0x01, 0x00, 0x00, 0x00, 0x01)
|
|
109
|
+
sleep_ms(100)
|
|
110
|
+
# GET_MODE_INFO
|
|
111
|
+
self.write(0x06, 0x00, 0x22, i, mode, 0x80)
|
|
112
|
+
sleep_ms(100)
|
|
113
|
+
|
|
114
|
+
# Enable notify on smart hubs
|
|
115
|
+
self.ble_handler.enable_notify(self._conn_handle, __HUB_NOTIFY_DESC, self.__on_notify)
|
|
116
|
+
sleep_ms(200)
|
|
117
|
+
self.set_led_color(GREEN)
|
|
118
|
+
else:
|
|
119
|
+
print("Connection failed")
|
|
120
|
+
|
|
121
|
+
def disconnect(self):
|
|
122
|
+
if self._conn_handle is not None:
|
|
123
|
+
self.ble_handler.disconnect(self._conn_handle)
|
|
124
|
+
self._conn_handle = None
|
|
125
|
+
|
|
126
|
+
def write(self, *data):
|
|
127
|
+
self.ble_handler.lego_write(
|
|
128
|
+
struct.pack("%sB" % len(data), *data),
|
|
129
|
+
self._conn_handle
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def set_led_color(self, idx):
|
|
133
|
+
self.write(0x08, 0x00, 0x81, 0x32, 0x11, 0x51, 0x00, idx)
|
|
134
|
+
|
|
135
|
+
def set_remote_led_color(self, idx):
|
|
136
|
+
self.write(0x08, 0x00, 0x81, 0x34, 0x11, 0x51, 0x00, idx)
|
|
137
|
+
|
|
138
|
+
def __on_notify(self, data):
|
|
139
|
+
# hub = data[1]
|
|
140
|
+
message_type = data[2]
|
|
141
|
+
port = data[3]
|
|
142
|
+
payload = data[4:]
|
|
143
|
+
if message_type == 0x45:
|
|
144
|
+
self.hub_data[port] = payload
|
|
145
|
+
elif message_type == 0x44:
|
|
146
|
+
self.mode_info[port] = {
|
|
147
|
+
_MODE : payload[0],
|
|
148
|
+
_MODE_BYTE : payload[1],
|
|
149
|
+
_MODE_DATA_SETS : payload[2],
|
|
150
|
+
_MODE_DATA_SET_TYPE : payload[3],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def unpack_data(self, port, fmt="3h"):
|
|
154
|
+
if port in self.hub_data.keys():
|
|
155
|
+
return struct.unpack(fmt, self.hub_data[port])
|
|
156
|
+
|
|
157
|
+
def acc(self):
|
|
158
|
+
return self.unpack_data(__HUB_PORT_ACC)
|
|
159
|
+
|
|
160
|
+
def gyro(self):
|
|
161
|
+
return self.unpack_data(__HUB_PORT_GYRO)
|
|
162
|
+
|
|
163
|
+
def tilt(self):
|
|
164
|
+
return self.unpack_data(__HUB_PORT_TILT)
|
|
165
|
+
|
|
166
|
+
def dc(self, port, pct):
|
|
167
|
+
self.write(0x06, 0x00, 0x81, self.__PORTS[port], 0x11, 0x51, 0x00, clamp_int(pct))
|
|
168
|
+
|
|
169
|
+
def run_target(self, port, degrees, speed=50, max_power=100, acceleration=100, deceleration=100, stop_action=0):
|
|
170
|
+
degree_bits = struct.unpack("<BBBB", struct.pack("<i", degrees))
|
|
171
|
+
self.write(0x0D, 0x00, 0x81, self.__PORTS[port], 0x11, 0x0D, degree_bits[0], degree_bits[1], degree_bits[2], degree_bits[3], speed, max_power, 0x7E)
|
|
172
|
+
|
|
173
|
+
def mode(self, port, mode, *data):
|
|
174
|
+
# set_mode
|
|
175
|
+
self.write(0x0A,0x00,0x41, self.__PORTS[port], mode, 0x01, 0x00, 0x00, 0x00, 0x01)
|
|
176
|
+
sleep_ms(100)
|
|
177
|
+
if data:
|
|
178
|
+
self.write(7+len(data), 0x00, 0x81, self.__PORTS[port], 0x00, 0x51, mode, *data)
|
|
179
|
+
sleep_ms(100)
|
|
180
|
+
# request_mode_info
|
|
181
|
+
self.write(0x06, 0x00, 0x22, self.__PORTS[port], mode, 0x80)
|
|
182
|
+
sleep_ms(100)
|
|
183
|
+
|
|
184
|
+
def run(self, port, speed, max_power=100, acceleration=100, deceleration=100):
|
|
185
|
+
# Start motor at given speed
|
|
186
|
+
self.write(0x09, 0x00, 0x81, self.__PORTS[port], 0x11, 0x07, clamp_int(speed), max_power, 0x00)
|
|
187
|
+
|
|
188
|
+
def run_time(self, port, time, speed=50, max_power=100, acceleration=100, deceleration=100, stop_action=0):
|
|
189
|
+
# Rotate motor for a given time
|
|
190
|
+
time_bits = struct.unpack("<BB", struct.pack("<H", time))
|
|
191
|
+
self.write(0x0B, 0x00, 0x81, self.__PORTS[port], 0x11, 0x09, time_bits[0], time_bits[1], speed, max_power, 0x00)
|
|
192
|
+
|
|
193
|
+
def run_angle(self, port, degrees, speed=50, max_power=100, acceleration=100, deceleration=100, stop_action=0):
|
|
194
|
+
# Rotate motor for a given number of degrees relative to current position
|
|
195
|
+
degree_bits = struct.unpack("<BBBB", struct.pack("<i", degrees))
|
|
196
|
+
self.write(0x0D, 0x00, 0x81, self.__PORTS[port], 0x11, 0x0B, degree_bits[0], degree_bits[1], degree_bits[2], degree_bits[3], speed, max_power, 0x7E)
|
|
197
|
+
|
|
198
|
+
def get(self, port):
|
|
199
|
+
port = self.__PORTS[port]
|
|
200
|
+
if port in self.hub_data:
|
|
201
|
+
value = None
|
|
202
|
+
payload = self.hub_data[port]
|
|
203
|
+
no_data_sets = None
|
|
204
|
+
data_set_type = 0
|
|
205
|
+
if port in self.mode_info:
|
|
206
|
+
data_set_type = self.mode_info[port][_MODE_DATA_SET_TYPE]
|
|
207
|
+
no_data_sets = self.mode_info[port][_MODE_DATA_SETS]
|
|
208
|
+
|
|
209
|
+
if data_set_type == 0x00:
|
|
210
|
+
message = struct.unpack("%sb" % len(payload), payload)
|
|
211
|
+
value = message[:no_data_sets]
|
|
212
|
+
elif data_set_type == 0x01:
|
|
213
|
+
message = struct.unpack("%sh" % (len(payload)//2), payload)
|
|
214
|
+
value = message[:no_data_sets]
|
|
215
|
+
elif data_set_type == 0x02:
|
|
216
|
+
message = struct.unpack("%si" % (len(payload)//4), payload)
|
|
217
|
+
value = message[:no_data_sets]
|
|
218
|
+
return value
|
btbricks/ctrl_plus.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: btbricks
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: A MicroPython Bluetooth library for remote controlling LEGO hubs via BLE
|
|
5
|
+
Home-page: https://github.com/antonvh/btbricks
|
|
6
|
+
Author: Anton Vanhoucke
|
|
7
|
+
Author-email: Anton Vanhoucke <anton@antonsmindstorms.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/antonvh/btbricks
|
|
10
|
+
Project-URL: Documentation, https://docs.antonsmindstorms.com/en/latest/Software/btbricks/docs/index.html
|
|
11
|
+
Project-URL: Repository, https://github.com/antonvh/btbricks.git
|
|
12
|
+
Project-URL: Issues, https://github.com/antonvh/btbricks/issues
|
|
13
|
+
Keywords: micropython,bluetooth,ble,lego,hub,control
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Education
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Hardware
|
|
22
|
+
Requires-Python: >=3.7
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
29
|
+
Requires-Dist: flake8>=5.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: sphinx>=4.0; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.12; extra == "docs"
|
|
35
|
+
Dynamic: author
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: requires-python
|
|
39
|
+
|
|
40
|
+
# btbricks
|
|
41
|
+
|
|
42
|
+
<img alt="btbricks logo" src="https://raw.githubusercontent.com/antonvh/btbricks/master/img/btbricks.png" width="200">
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/btbricks/)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
[](https://micropython.org/)
|
|
47
|
+
|
|
48
|
+
A MicroPython Bluetooth library. It implements BLE (Bluetooth 5, Bluetooth Low Energy). Of the know BLE services, this library implements Nordic Uart Service (NUS), LEGO Service and MIDI service. The library contains both the BLE Central (client) and BLE Peripheral (server) classes.
|
|
49
|
+
|
|
50
|
+
These BLE services allow for controlling LEGO hubs, running official firmware. The services also allow creating custom Bluetooth peripherals: RC controllers, MIDI devices, etc. To control the LEGO hubs, you can best use a [hub expansion board, like the LMS-ESP32](https://www.antonsmindstorms.com/product/wifi-python-esp32-board-for-mindstorms/).
|
|
51
|
+
|
|
52
|
+
## Table of Contents
|
|
53
|
+
|
|
54
|
+
- [Features](#features)
|
|
55
|
+
- [Installation](#installation)
|
|
56
|
+
- [Quick Start](#quick-start)
|
|
57
|
+
- [Documentation](#documentation-and-api-reference)
|
|
58
|
+
- [Supported Platforms](#supported-platforms)
|
|
59
|
+
- [Firmware Notes](#firmware-notes)
|
|
60
|
+
- [License](#license)
|
|
61
|
+
- [Author](#author)
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- 🔌 **BLE Communication**: Comprehensive Bluetooth Low Energy support via MicroPython's `ubluetooth`
|
|
66
|
+
- 🎮 **Hub Control**: Control LEGO MINDSTORMS hubs, SPIKE sets, and smart hubs over Bluetooth
|
|
67
|
+
- 📱 **Custom Peripherals**: Create RC controllers, MIDI controllers, and other BLE peripherals compatible with LEGO hubs
|
|
68
|
+
- 🚀 **MicroPython Ready**: Optimized for MicroPython on ESP32, LEGO SPIKE, and other platforms
|
|
69
|
+
- 📡 **LEGO Protocol**: Full support for LEGO Bluetooth protocols (LPF2, LPUP, CTRL+)
|
|
70
|
+
- 🎛️ **Multiple Interfaces**: Nordic UART, MIDI, RC control, and native LEGO hub communication
|
|
71
|
+
- ⚙️ **Advanced BLE**: Automatic MTU negotiation, descriptor handling, and efficient payload management
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
### On LMS-ESP32
|
|
76
|
+
|
|
77
|
+
The module should be included in the latest Micropython firmware from <https://wwww.antonsmindstorms.com>. If not, use ViperIDE or Thonny and create a new file called rcservo.py.
|
|
78
|
+
Copy the contents from the same file in this repository inside.
|
|
79
|
+
|
|
80
|
+
### On MicroPython device using `micropip` from PyPI
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import micropip
|
|
84
|
+
await micropip.install("btbricks")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Note: `micropip` must be available on the target board and may require an internet connection from the device.
|
|
88
|
+
|
|
89
|
+
### On SPIKE Legacy or MINDSTORMS Robot Inventor
|
|
90
|
+
|
|
91
|
+
Use the installer script in mpy-robot-tools: <https://github.com/antonvh/mpy-robot-tools/blob/master/Installer/install_mpy_robot_tools.py>
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
### Connect to a LEGO Hub
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from btbricks import BtHub
|
|
99
|
+
|
|
100
|
+
# Create hub instance
|
|
101
|
+
hub = BtHub()
|
|
102
|
+
|
|
103
|
+
# Connect to a nearby hub
|
|
104
|
+
hub.connect()
|
|
105
|
+
|
|
106
|
+
if hub.is_connected():
|
|
107
|
+
# Set hub LED to green
|
|
108
|
+
hub.set_led_color(6) # GREEN constant
|
|
109
|
+
|
|
110
|
+
# Read accelerometer data
|
|
111
|
+
acc = hub.acc()
|
|
112
|
+
if acc:
|
|
113
|
+
print(f"Accelerometer: {acc}")
|
|
114
|
+
|
|
115
|
+
# Control motor on port A with 50% power
|
|
116
|
+
hub.dc("A", 50)
|
|
117
|
+
|
|
118
|
+
hub.disconnect()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Create an RC Receiver (Hub-side)
|
|
122
|
+
Use the examples in the `examples/` folder for full, runnable code. Minimal receiver/transmitter snippets:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from btbricks import RCReceiver, R_STICK_HOR, R_STICK_VER
|
|
126
|
+
from time import sleep_ms
|
|
127
|
+
|
|
128
|
+
# Create RC receiver (advertises as "robot" by default)
|
|
129
|
+
rcv = RCReceiver(name="robot")
|
|
130
|
+
|
|
131
|
+
print("Waiting for RC transmitter to connect...")
|
|
132
|
+
try:
|
|
133
|
+
while True:
|
|
134
|
+
if rcv.is_connected():
|
|
135
|
+
steering = rcv.controller_state(R_STICK_HOR)
|
|
136
|
+
throttle = rcv.controller_state(R_STICK_VER)
|
|
137
|
+
print(f"Steering: {steering}, Throttle: {throttle}")
|
|
138
|
+
sleep_ms(100)
|
|
139
|
+
else:
|
|
140
|
+
sleep_ms(500)
|
|
141
|
+
except KeyboardInterrupt:
|
|
142
|
+
rcv.disconnect()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from btbricks import RCTransmitter, L_STICK_HOR, R_STICK_VER
|
|
147
|
+
from time import sleep
|
|
148
|
+
|
|
149
|
+
# Create RC transmitter (central)
|
|
150
|
+
tx = RCTransmitter()
|
|
151
|
+
|
|
152
|
+
if tx.connect(name="robot"):
|
|
153
|
+
try:
|
|
154
|
+
while tx.is_connected():
|
|
155
|
+
# Set stick values in range [-100, 100]
|
|
156
|
+
tx.set_stick(L_STICK_HOR, 0)
|
|
157
|
+
tx.set_stick(R_STICK_VER, 50)
|
|
158
|
+
tx.transmit()
|
|
159
|
+
sleep(0.1)
|
|
160
|
+
except KeyboardInterrupt:
|
|
161
|
+
tx.disconnect()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Create a MIDI Controller
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from btbricks import MidiController
|
|
168
|
+
from time import sleep
|
|
169
|
+
|
|
170
|
+
# Create MIDI controller (advertises as "amh-midi" by default)
|
|
171
|
+
midi = MidiController(name="amh-midi")
|
|
172
|
+
|
|
173
|
+
print("MIDI controller started, connect from your DAW...")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
while True:
|
|
177
|
+
# Send MIDI note on: middle C (note 60), velocity 100
|
|
178
|
+
midi.note_on(60, 100)
|
|
179
|
+
sleep(0.5)
|
|
180
|
+
|
|
181
|
+
# Send MIDI note off
|
|
182
|
+
midi.note_off(60)
|
|
183
|
+
sleep(0.5)
|
|
184
|
+
|
|
185
|
+
# Or send a chord
|
|
186
|
+
midi.chord_on("C4", velocity=100, style="M") # C major chord
|
|
187
|
+
sleep(1)
|
|
188
|
+
midi.note_off(60) # Stop the chord
|
|
189
|
+
|
|
190
|
+
except KeyboardInterrupt:
|
|
191
|
+
print("MIDI controller stopped")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Documentation and API reference
|
|
195
|
+
|
|
196
|
+
See the full documentation and API reference at:
|
|
197
|
+
|
|
198
|
+
https://docs.antonsmindstorms.com/en/latest/Software/btbricks/docs/index.html
|
|
199
|
+
|
|
200
|
+
### Core Classes
|
|
201
|
+
|
|
202
|
+
- `BLEHandler`: Low-level Bluetooth communication
|
|
203
|
+
- `UARTCentral`: Nordic UART client mode
|
|
204
|
+
- `UARTPeripheral`: Nordic UART server mode
|
|
205
|
+
- `RCReceiver`: Receive RC control signals
|
|
206
|
+
- `RCTransmitter`: Send RC control signals
|
|
207
|
+
- `MidiController`: Send MIDI commands over BLE
|
|
208
|
+
- `BtHub`: High-level hub communication interface
|
|
209
|
+
|
|
210
|
+
### Control Constants
|
|
211
|
+
|
|
212
|
+
- Sticks: `L_STICK_HOR`, `L_STICK_VER`, `R_STICK_HOR`, `R_STICK_VER`
|
|
213
|
+
- Triggers: `L_TRIGGER`, `R_TRIGGER`
|
|
214
|
+
- Buttons: `BUTTONS`
|
|
215
|
+
- Settings: `SETTING1`, `SETTING2`
|
|
216
|
+
|
|
217
|
+
## Supported Platforms
|
|
218
|
+
|
|
219
|
+
- **LEGO MINDSTORMS EV3** (with MicroPython firmware)
|
|
220
|
+
- **LEGO SPIKE Prime/Prime Essential** (with MINDSTORMS firmware)
|
|
221
|
+
- **LEGO SPIKE Robot Inventor**
|
|
222
|
+
- **ESP32** with MicroPython
|
|
223
|
+
- Other MicroPython boards with `ubluetooth` support
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License
|
|
229
|
+
|
|
230
|
+
## Author
|
|
231
|
+
|
|
232
|
+
Anton Vanhoucke
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
btbricks/__init__.py,sha256=U1Yxw-A51X4eGmuACmqdfRpb9v_CNs6sgTy4RBKMQZU,1051
|
|
2
|
+
btbricks/bt.py,sha256=JLtXm3RsfpLTbWzGYhrEC1_AMlTpyXHaEoBR1Q5lC7A,44575
|
|
3
|
+
btbricks/bthub.py,sha256=TVvpVo5E9nIzwmASMTAjjLs0lbY4Su4CYFlBU4KZ3dY,7967
|
|
4
|
+
btbricks/ctrl_plus.py,sha256=CAnkG_SZ9Zysv4ZUDJ7UE0HTLMGeZYD0JxPEIIf-o6Y,177
|
|
5
|
+
btbricks-0.2.1.dist-info/licenses/LICENSE,sha256=yVFkYtNY8Mlp5U_xedI4-O8Hxjg_vu-taxJlv8y-xVk,1078
|
|
6
|
+
tests/__init__.py,sha256=CtJ2NCOCvkNNpVgAw3XHsKd_S-C36d7Yuq4QfTPmwgg,34
|
|
7
|
+
tests/test_bthub.py,sha256=9yyBhsydjwN7_yXKzsKTEAxcjW_PqOzwZFcQ2dDv_f0,3298
|
|
8
|
+
tests/test_constants.py,sha256=L0rIn8VsPIPc80qITblsPu9mQltL85pAFLQl2KQUpGY,2023
|
|
9
|
+
tests/test_imports.py,sha256=kHrRnueFPnQxU807Bo9Ddr3x4hsryMroUoWC816N-Nc,2147
|
|
10
|
+
btbricks-0.2.1.dist-info/METADATA,sha256=mdeew3CTC3HfEYMKbHW8AcN2jhrzep9Cf9rUMSE_WkY,7770
|
|
11
|
+
btbricks-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
btbricks-0.2.1.dist-info/top_level.txt,sha256=nznVmPKoDx79OB6rEM180swD9X5G22V35afi5zon1d8,15
|
|
13
|
+
btbricks-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 btbricks contributors
|
|
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.
|
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for btbricks package."""
|
tests/test_bthub.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for BtHub class."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import Mock, MagicMock, patch
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestBtHubInitialization:
|
|
8
|
+
"""Test BtHub initialization."""
|
|
9
|
+
|
|
10
|
+
def test_bthub_init_default(self):
|
|
11
|
+
"""Test BtHub initialization with default BLEHandler."""
|
|
12
|
+
from btbricks import BtHub
|
|
13
|
+
|
|
14
|
+
with patch("btbricks.bthub.BLEHandler"):
|
|
15
|
+
hub = BtHub()
|
|
16
|
+
assert hub._conn_handle is None
|
|
17
|
+
assert hub.acc_sub is False
|
|
18
|
+
assert hub.gyro_sub is False
|
|
19
|
+
assert hub.tilt_sub is False
|
|
20
|
+
assert isinstance(hub.hub_data, dict)
|
|
21
|
+
assert isinstance(hub.mode_info, dict)
|
|
22
|
+
|
|
23
|
+
def test_bthub_init_with_ble_handler(self):
|
|
24
|
+
"""Test BtHub initialization with custom BLEHandler."""
|
|
25
|
+
from btbricks import BtHub
|
|
26
|
+
|
|
27
|
+
mock_ble = Mock()
|
|
28
|
+
hub = BtHub(ble_handler=mock_ble)
|
|
29
|
+
assert hub.ble_handler is mock_ble
|
|
30
|
+
assert hub._conn_handle is None
|
|
31
|
+
|
|
32
|
+
def test_bthub_is_connected_false(self):
|
|
33
|
+
"""Test is_connected returns False when not connected."""
|
|
34
|
+
from btbricks import BtHub
|
|
35
|
+
|
|
36
|
+
with patch("btbricks.bthub.BLEHandler"):
|
|
37
|
+
hub = BtHub()
|
|
38
|
+
assert hub.is_connected() is False
|
|
39
|
+
|
|
40
|
+
def test_bthub_is_connected_true(self):
|
|
41
|
+
"""Test is_connected returns True when connected."""
|
|
42
|
+
from btbricks import BtHub
|
|
43
|
+
|
|
44
|
+
with patch("btbricks.bthub.BLEHandler"):
|
|
45
|
+
hub = BtHub()
|
|
46
|
+
hub._conn_handle = 1
|
|
47
|
+
assert hub.is_connected() is True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestBtHubColorConstants:
|
|
51
|
+
"""Test BtHub LED color constants."""
|
|
52
|
+
|
|
53
|
+
def test_color_constants_exist(self):
|
|
54
|
+
"""Test that color constants are defined."""
|
|
55
|
+
from btbricks.bthub import OFF, PINK, PURPLE, DARK_BLUE, BLUE, TEAL
|
|
56
|
+
from btbricks.bthub import GREEN, YELLOW, ORANGE, RED, WHITE
|
|
57
|
+
|
|
58
|
+
colors = [OFF, PINK, PURPLE, DARK_BLUE, BLUE, TEAL, GREEN, YELLOW, ORANGE, RED, WHITE]
|
|
59
|
+
|
|
60
|
+
# All should be integers
|
|
61
|
+
assert all(isinstance(c, int) for c in colors)
|
|
62
|
+
|
|
63
|
+
# All should be non-negative
|
|
64
|
+
assert all(c >= 0 for c in colors)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestBtHubClampInt:
|
|
68
|
+
"""Test clamp_int helper function."""
|
|
69
|
+
|
|
70
|
+
def test_clamp_int_within_range(self):
|
|
71
|
+
"""Test clamp_int with value within range."""
|
|
72
|
+
from btbricks.bthub import clamp_int
|
|
73
|
+
|
|
74
|
+
assert clamp_int(50) == 50
|
|
75
|
+
assert clamp_int(0) == 0
|
|
76
|
+
|
|
77
|
+
def test_clamp_int_below_min(self):
|
|
78
|
+
"""Test clamp_int with value below minimum."""
|
|
79
|
+
from btbricks.bthub import clamp_int
|
|
80
|
+
|
|
81
|
+
assert clamp_int(-150) == -100
|
|
82
|
+
|
|
83
|
+
def test_clamp_int_above_max(self):
|
|
84
|
+
"""Test clamp_int with value above maximum."""
|
|
85
|
+
from btbricks.bthub import clamp_int
|
|
86
|
+
|
|
87
|
+
assert clamp_int(150) == 100
|
|
88
|
+
|
|
89
|
+
def test_clamp_int_custom_range(self):
|
|
90
|
+
"""Test clamp_int with custom floor and ceiling."""
|
|
91
|
+
from btbricks.bthub import clamp_int
|
|
92
|
+
|
|
93
|
+
assert clamp_int(5, floor=0, ceiling=10) == 5
|
|
94
|
+
assert clamp_int(-5, floor=0, ceiling=10) == 0
|
|
95
|
+
assert clamp_int(15, floor=0, ceiling=10) == 10
|
|
96
|
+
|
|
97
|
+
def test_clamp_int_rounds(self):
|
|
98
|
+
"""Test clamp_int rounds values."""
|
|
99
|
+
from btbricks.bthub import clamp_int
|
|
100
|
+
|
|
101
|
+
assert clamp_int(50.4) == 50
|
|
102
|
+
assert clamp_int(50.5) == 50 or clamp_int(50.5) == 51 # Banker's rounding
|
tests/test_constants.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tests for btbricks constants."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestRCConstants:
|
|
7
|
+
"""Test RC control constants."""
|
|
8
|
+
|
|
9
|
+
def test_stick_constants(self):
|
|
10
|
+
"""Test that stick constants are defined correctly."""
|
|
11
|
+
from btbricks import (
|
|
12
|
+
L_STICK_HOR,
|
|
13
|
+
L_STICK_VER,
|
|
14
|
+
R_STICK_HOR,
|
|
15
|
+
R_STICK_VER,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assert L_STICK_HOR == 0
|
|
19
|
+
assert L_STICK_VER == 1
|
|
20
|
+
assert R_STICK_HOR == 2
|
|
21
|
+
assert R_STICK_VER == 3
|
|
22
|
+
|
|
23
|
+
def test_trigger_constants(self):
|
|
24
|
+
"""Test that trigger constants are defined correctly."""
|
|
25
|
+
from btbricks import L_TRIGGER, R_TRIGGER
|
|
26
|
+
|
|
27
|
+
assert L_TRIGGER == 4
|
|
28
|
+
assert R_TRIGGER == 5
|
|
29
|
+
|
|
30
|
+
def test_button_and_setting_constants(self):
|
|
31
|
+
"""Test button and setting constants."""
|
|
32
|
+
from btbricks import BUTTONS, SETTING1, SETTING2
|
|
33
|
+
|
|
34
|
+
assert BUTTONS == 8
|
|
35
|
+
assert SETTING1 == 6
|
|
36
|
+
assert SETTING2 == 7
|
|
37
|
+
|
|
38
|
+
def test_constants_are_unique(self):
|
|
39
|
+
"""Test that all RC constants are unique."""
|
|
40
|
+
from btbricks import (
|
|
41
|
+
L_STICK_HOR,
|
|
42
|
+
L_STICK_VER,
|
|
43
|
+
R_STICK_HOR,
|
|
44
|
+
R_STICK_VER,
|
|
45
|
+
L_TRIGGER,
|
|
46
|
+
R_TRIGGER,
|
|
47
|
+
SETTING1,
|
|
48
|
+
SETTING2,
|
|
49
|
+
BUTTONS,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
constants = [
|
|
53
|
+
L_STICK_HOR,
|
|
54
|
+
L_STICK_VER,
|
|
55
|
+
R_STICK_HOR,
|
|
56
|
+
R_STICK_VER,
|
|
57
|
+
L_TRIGGER,
|
|
58
|
+
R_TRIGGER,
|
|
59
|
+
SETTING1,
|
|
60
|
+
SETTING2,
|
|
61
|
+
BUTTONS,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
assert len(constants) == len(set(constants))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestPortConstants:
|
|
68
|
+
"""Test port mapping constants in BtHub."""
|
|
69
|
+
|
|
70
|
+
def test_port_mapping(self):
|
|
71
|
+
"""Test that BtHub has correct port mapping."""
|
|
72
|
+
from btbricks import BtHub
|
|
73
|
+
|
|
74
|
+
with __import__("unittest.mock").mock.patch("btbricks.bthub.BLEHandler"):
|
|
75
|
+
hub = BtHub()
|
|
76
|
+
# Check that _BtHub__PORTS exists (name-mangled private attribute)
|
|
77
|
+
assert hasattr(hub, "_BtHub__PORTS")
|