btbricks 0.2.1__py3-none-any.whl → 0.2.3__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 CHANGED
@@ -7,7 +7,7 @@ use it to create custom Bluetooth peripherals like RC controllers or MIDI
7
7
  devices compatible with LEGO hubs.
8
8
  """
9
9
 
10
- __version__ = "0.1.0"
10
+ __version__ = "0.2.3"
11
11
  __author__ = "Anton Vanhoucke"
12
12
  __license__ = "MIT"
13
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: btbricks
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: A MicroPython Bluetooth library for remote controlling LEGO hubs via BLE
5
5
  Home-page: https://github.com/antonvh/btbricks
6
6
  Author: Anton Vanhoucke
@@ -28,6 +28,9 @@ Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
28
28
  Requires-Dist: black>=23.0; extra == "dev"
29
29
  Requires-Dist: flake8>=5.0; extra == "dev"
30
30
  Requires-Dist: mypy>=1.0; extra == "dev"
31
+ Requires-Dist: mpy-cross>=1.20; extra == "dev"
32
+ Requires-Dist: requests>=2.28.0; extra == "dev"
33
+ Requires-Dist: markdown-link-validator>=0.1.0; extra == "dev"
31
34
  Provides-Extra: docs
32
35
  Requires-Dist: sphinx>=4.0; extra == "docs"
33
36
  Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
@@ -37,17 +40,24 @@ Dynamic: home-page
37
40
  Dynamic: license-file
38
41
  Dynamic: requires-python
39
42
 
40
- # btbricks
41
-
43
+ <div align="center">
42
44
  <img alt="btbricks logo" src="https://raw.githubusercontent.com/antonvh/btbricks/master/img/btbricks.png" width="200">
43
45
 
46
+ # btbricks
47
+
44
48
  [![PyPI Version](https://img.shields.io/pypi/v/btbricks.svg)](https://pypi.org/project/btbricks/)
45
49
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
46
50
  [![MicroPython](https://img.shields.io/badge/MicroPython-compatible-orange.svg)](https://micropython.org/)
47
51
 
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.
52
+ btbricks is 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.
53
+
54
+ </div>
49
55
 
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/).
56
+ The BLE services allow controlling LEGO hubs running official firmware. You can
57
+ also create custom Bluetooth peripherals: RC controllers, MIDI devices, and more.
58
+ For LEGO hub control, a
59
+ [hub expansion board like the LMS-ESP32](https://www.antonsmindstorms.com/product/wifi-python-esp32-board-for-mindstorms/)
60
+ is recommended.
51
61
 
52
62
  ## Table of Contents
53
63
 
@@ -56,28 +66,45 @@ These BLE services allow for controlling LEGO hubs, running official firmware. T
56
66
  - [Quick Start](#quick-start)
57
67
  - [Documentation](#documentation-and-api-reference)
58
68
  - [Supported Platforms](#supported-platforms)
59
- - [Firmware Notes](#firmware-notes)
60
69
  - [License](#license)
61
70
  - [Author](#author)
62
71
 
63
72
  ## Features
64
73
 
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
74
+ - 🔌 **BLE Communication**: Comprehensive Bluetooth Low Energy support via
75
+ MicroPython's `ubluetooth`
76
+ - 🎮 **Hub Control**: Control LEGO MINDSTORMS hubs, SPIKE sets, and smart hubs
77
+ over Bluetooth
78
+ - 📱 **Custom Peripherals**: Create RC controllers, MIDI controllers, and other
79
+ BLE peripherals compatible with LEGO hubs
80
+ - 🚀 **MicroPython Ready**: Optimized for MicroPython on ESP32, LEGO SPIKE, and
81
+ other platforms
82
+ - 📡 **LEGO Protocol**: Full support for LEGO Bluetooth protocols (LPF2, LPUP,
83
+ CTRL+)
84
+ - 🎛️ **Multiple Interfaces**: Nordic UART, MIDI, RC control, and native LEGO hub
85
+ communication
86
+ - ⚙️ **Advanced BLE**: Automatic MTU negotiation, descriptor handling, and
87
+ efficient payload management
72
88
 
73
89
  ## Installation
74
90
 
91
+ ### Using ViperIDE Package Manager (Recommended)
92
+
93
+ 1. Open **ViperIDE** on your device
94
+ 2. Go to **Tools** → **Package Manager**
95
+ 3. Select **Install Package via Link**
96
+ 4. Enter the package link: `https://github.com/antonvh/btbricks.git`
97
+ 5. Follow the on-screen prompts to complete installation
98
+
75
99
  ### On LMS-ESP32
76
100
 
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.
101
+ The module should be included in the latest Micropython firmware from <https:/firmware.antonsmindstorms.com>. If not, use ViperIDE as described above.
79
102
 
80
- ### On MicroPython device using `micropip` from PyPI
103
+ ### On SPIKE Legacy or MINDSTORMS Robot Inventor
104
+
105
+ Use the installer script in mpy-robot-tools: <https://github.com/antonvh/mpy-robot-tools/blob/master/Installer/install_mpy_robot_tools.py>
106
+
107
+ ### Deprecated: using `micropip` from PyPI
81
108
 
82
109
  ```python
83
110
  import micropip
@@ -86,10 +113,6 @@ await micropip.install("btbricks")
86
113
 
87
114
  Note: `micropip` must be available on the target board and may require an internet connection from the device.
88
115
 
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
116
  ## Quick Start
94
117
 
95
118
  ### Connect to a LEGO Hub
@@ -119,7 +142,9 @@ if hub.is_connected():
119
142
  ```
120
143
 
121
144
  ### Create an RC Receiver (Hub-side)
122
- Use the examples in the `examples/` folder for full, runnable code. Minimal receiver/transmitter snippets:
145
+
146
+ Use the examples in the `examples/` folder for full, runnable code. Minimal
147
+ receiver/transmitter snippets:
123
148
 
124
149
  ```python
125
150
  from btbricks import RCReceiver, R_STICK_HOR, R_STICK_VER
@@ -195,7 +220,7 @@ except KeyboardInterrupt:
195
220
 
196
221
  See the full documentation and API reference at:
197
222
 
198
- https://docs.antonsmindstorms.com/en/latest/Software/btbricks/docs/index.html
223
+ [docs.antonsmindstorms.com](https://docs.antonsmindstorms.com/en/latest/Software/btbricks/docs/index.html)
199
224
 
200
225
  ### Core Classes
201
226
 
@@ -222,7 +247,6 @@ https://docs.antonsmindstorms.com/en/latest/Software/btbricks/docs/index.html
222
247
  - **ESP32** with MicroPython
223
248
  - Other MicroPython boards with `ubluetooth` support
224
249
 
225
-
226
250
  ## License
227
251
 
228
252
  MIT License
@@ -1,13 +1,14 @@
1
- btbricks/__init__.py,sha256=U1Yxw-A51X4eGmuACmqdfRpb9v_CNs6sgTy4RBKMQZU,1051
1
+ btbricks/__init__.py,sha256=eUuqwGNWUQIA61IJp_p-b79RyEl2bvR_0mQmeQ_beeI,1051
2
2
  btbricks/bt.py,sha256=JLtXm3RsfpLTbWzGYhrEC1_AMlTpyXHaEoBR1Q5lC7A,44575
3
3
  btbricks/bthub.py,sha256=TVvpVo5E9nIzwmASMTAjjLs0lbY4Su4CYFlBU4KZ3dY,7967
4
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
5
+ btbricks-0.2.3.dist-info/licenses/LICENSE,sha256=yVFkYtNY8Mlp5U_xedI4-O8Hxjg_vu-taxJlv8y-xVk,1078
6
6
  tests/__init__.py,sha256=CtJ2NCOCvkNNpVgAw3XHsKd_S-C36d7Yuq4QfTPmwgg,34
7
7
  tests/test_bthub.py,sha256=9yyBhsydjwN7_yXKzsKTEAxcjW_PqOzwZFcQ2dDv_f0,3298
8
8
  tests/test_constants.py,sha256=L0rIn8VsPIPc80qITblsPu9mQltL85pAFLQl2KQUpGY,2023
9
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,,
10
+ tests/test_precommit_checks.py,sha256=mXDtYdLFDgBdOrMBGZyaGz5Z4QtzSfwFpP7axU_H-WE,7075
11
+ btbricks-0.2.3.dist-info/METADATA,sha256=oZGjCxcMEkjr96h-xxk956TLDmdlhEjX4DwwXiRz48o,8160
12
+ btbricks-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ btbricks-0.2.3.dist-info/top_level.txt,sha256=nznVmPKoDx79OB6rEM180swD9X5G22V35afi5zon1d8,15
14
+ btbricks-0.2.3.dist-info/RECORD,,
@@ -0,0 +1,221 @@
1
+ """Pre-commit checks for dead links and mpy-cross compilation."""
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import List, Set
9
+
10
+ import pytest
11
+ import requests
12
+
13
+
14
+ def find_markdown_files() -> List[Path]:
15
+ """Find all markdown files in the project."""
16
+ root = Path(__file__).parent.parent
17
+ md_files = []
18
+ for ext in ["*.md", "*.MD"]:
19
+ md_files.extend(root.glob(ext))
20
+ md_files.extend(root.glob(f"**/{ext}"))
21
+
22
+ # Exclude hidden directories and common ignore patterns
23
+ excluded = {".git", "node_modules", ".venv", "venv", "__pycache__"}
24
+ return [
25
+ f for f in md_files if not any(part.startswith(".") or part in excluded for part in f.parts)
26
+ ]
27
+
28
+
29
+ def extract_urls_from_markdown(file_path: Path) -> Set[str]:
30
+ """Extract all HTTP(S) URLs from a markdown file."""
31
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
32
+
33
+ # Match markdown links [text](url) and plain URLs
34
+ url_pattern = r"https?://[^\s\)<>\"\']+"
35
+ urls = set(re.findall(url_pattern, content))
36
+
37
+ # Clean up any trailing punctuation that might have been captured
38
+ cleaned_urls = set()
39
+ for url in urls:
40
+ # Remove trailing punctuation and backticks
41
+ url = re.sub(r'[.,;:!?\)`]+$', "", url)
42
+ cleaned_urls.add(url)
43
+
44
+ return cleaned_urls
45
+
46
+
47
+ def is_url_valid(url: str, timeout: int = 10) -> tuple[bool, int]:
48
+ """Check if a URL is accessible.
49
+
50
+ Returns:
51
+ tuple of (is_valid, http_code)
52
+ """
53
+ # Skip localhost and example URLs
54
+ if "localhost" in url or "example.com" in url or "127.0.0.1" in url:
55
+ return True, 200
56
+
57
+ try:
58
+ # Use HEAD request first (faster)
59
+ response = requests.head(
60
+ url,
61
+ timeout=timeout,
62
+ allow_redirects=True,
63
+ headers={"User-Agent": "Mozilla/5.0 (compatible; LinkChecker/1.0)"},
64
+ )
65
+
66
+ # Some servers don't support HEAD, try GET if HEAD fails
67
+ if response.status_code >= 400:
68
+ response = requests.get(
69
+ url,
70
+ timeout=timeout,
71
+ allow_redirects=True,
72
+ headers={"User-Agent": "Mozilla/5.0 (compatible; LinkChecker/1.0)"},
73
+ )
74
+
75
+ # Consider 200-399 as valid
76
+ return response.status_code < 400, response.status_code
77
+
78
+ except requests.exceptions.Timeout:
79
+ return False, 408 # Request Timeout
80
+ except requests.exceptions.ConnectionError:
81
+ return False, 0 # Connection failed
82
+ except requests.exceptions.RequestException:
83
+ return False, 0 # Other request errors
84
+
85
+
86
+ def find_python_files() -> List[Path]:
87
+ """Find all Python files in the btbricks package."""
88
+ root = Path(__file__).parent.parent
89
+ btbricks_dir = root / "btbricks"
90
+
91
+ if not btbricks_dir.exists():
92
+ return []
93
+
94
+ py_files = []
95
+ for py_file in btbricks_dir.rglob("*.py"):
96
+ # Skip __pycache__ and other temp directories
97
+ if "__pycache__" not in py_file.parts:
98
+ py_files.append(py_file)
99
+
100
+ return py_files
101
+
102
+
103
+ def check_mpy_cross_available() -> bool:
104
+ """Check if mpy-cross is available."""
105
+ # Try to find mpy-cross in venv first
106
+ venv_mpy_cross = Path(__file__).parent.parent / ".venv" / "bin" / "mpy-cross"
107
+ if venv_mpy_cross.exists():
108
+ return True
109
+
110
+ # Fall back to checking PATH
111
+ try:
112
+ result = subprocess.run(
113
+ ["mpy-cross", "--version"], capture_output=True, text=True, timeout=5
114
+ )
115
+ return result.returncode == 0
116
+ except (FileNotFoundError, subprocess.TimeoutExpired):
117
+ return False
118
+
119
+
120
+ def compile_with_mpy_cross(py_file: Path) -> tuple[bool, str]:
121
+ """Try to compile a Python file with mpy-cross.
122
+
123
+ Returns:
124
+ tuple of (success, error_message)
125
+ """
126
+ # Find mpy-cross command
127
+ venv_mpy_cross = Path(__file__).parent.parent / ".venv" / "bin" / "mpy-cross"
128
+ mpy_cross_cmd = str(venv_mpy_cross) if venv_mpy_cross.exists() else "mpy-cross"
129
+
130
+ with tempfile.TemporaryDirectory() as tmpdir:
131
+ output_file = Path(tmpdir) / "temp.mpy"
132
+
133
+ try:
134
+ result = subprocess.run(
135
+ [mpy_cross_cmd, str(py_file), "-o", str(output_file)],
136
+ capture_output=True,
137
+ text=True,
138
+ timeout=30,
139
+ )
140
+
141
+ if result.returncode == 0:
142
+ return True, ""
143
+ else:
144
+ return False, result.stderr or result.stdout
145
+
146
+ except subprocess.TimeoutExpired:
147
+ return False, "Compilation timeout"
148
+ except Exception as e:
149
+ return False, str(e)
150
+
151
+
152
+ class TestDeadLinks:
153
+ """Test for dead links in markdown files."""
154
+
155
+ def test_no_dead_links(self):
156
+ """Check that all links in markdown files are valid."""
157
+ md_files = find_markdown_files()
158
+
159
+ if not md_files:
160
+ pytest.skip("No markdown files found")
161
+
162
+ all_urls = set()
163
+ url_sources = {} # Track which file each URL came from
164
+
165
+ for md_file in md_files:
166
+ urls = extract_urls_from_markdown(md_file)
167
+ for url in urls:
168
+ all_urls.add(url)
169
+ if url not in url_sources:
170
+ url_sources[url] = set()
171
+ url_sources[url].add(md_file.name)
172
+
173
+ if not all_urls:
174
+ pytest.skip("No URLs found in markdown files")
175
+
176
+ dead_links = []
177
+
178
+ for url in sorted(all_urls):
179
+ is_valid, http_code = is_url_valid(url)
180
+ if not is_valid:
181
+ sources = ", ".join(sorted(url_sources[url]))
182
+ dead_links.append(f"{url} (HTTP {http_code}) in {sources}")
183
+
184
+ if dead_links:
185
+ error_msg = f"Found {len(dead_links)} dead link(s):\n"
186
+ error_msg += "\n".join(f" - {link}" for link in dead_links)
187
+ pytest.fail(error_msg)
188
+
189
+
190
+ class TestMpyCrossCompilation:
191
+ """Test that all Python files compile with mpy-cross."""
192
+
193
+ def test_mpy_cross_installed(self):
194
+ """Check that mpy-cross is available."""
195
+ if not check_mpy_cross_available():
196
+ pytest.fail(
197
+ "mpy-cross is not installed or not available in PATH.\n"
198
+ "Install with: pip install mpy-cross"
199
+ )
200
+
201
+ def test_all_files_compile(self):
202
+ """Check that all Python files in btbricks/ compile with mpy-cross."""
203
+ if not check_mpy_cross_available():
204
+ pytest.skip("mpy-cross not available")
205
+
206
+ py_files = find_python_files()
207
+
208
+ if not py_files:
209
+ pytest.skip("No Python files found in btbricks/")
210
+
211
+ failed_compilations = []
212
+
213
+ for py_file in py_files:
214
+ success, error = compile_with_mpy_cross(py_file)
215
+ if not success:
216
+ failed_compilations.append(f"{py_file.name}: {error}")
217
+
218
+ if failed_compilations:
219
+ error_msg = f"Failed to compile {len(failed_compilations)} file(s) with mpy-cross:\n"
220
+ error_msg += "\n".join(f" - {failure}" for failure in failed_compilations)
221
+ pytest.fail(error_msg)