Skip to content

Commit 222ae61

Browse files
committed
tools: Tool to collect bt firmware.
collect_bt_patches.py collects bluetooth module firmware patch files for the most popular Broadcom and Realtek modules into a directory where they can be used by the virtualhub for initializing bluetooth modules.
1 parent 350db6e commit 222ae61

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,7 @@ Pipfile.lock
7272
# Jupyter Notebook
7373
######################
7474
.ipynb_checkpoints
75+
76+
# BT Patches fetched by tools/collect_bt_patches.py
77+
##########################################
78+
.bt_firmware/

tools/collect_bt_patches.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: MIT
3+
# Copyright (c) 2025 The Pybricks Authors
4+
5+
"""
6+
Tool to collect bluetooth patch files in the user's cache directory.
7+
"""
8+
9+
import argparse
10+
import os
11+
import re
12+
import subprocess
13+
import shutil
14+
from pathlib import Path
15+
16+
# Destination directory in user's cache directory
17+
DEST_DIR = Path.cwd() / ".bt_firmware"
18+
19+
20+
def _fetch_and_checkout(checkout_dir: Path, ref_to_fetch: str):
21+
"""Fetch a specific ref (branch/tag/sha) from origin and check it out.
22+
23+
Uses a shallow fetch (--depth=1) and then forces checkout of FETCH_HEAD so
24+
callers don't need to duplicate the subprocess logic.
25+
"""
26+
subprocess.run(
27+
["git", "fetch", "--depth=1", "origin", ref_to_fetch],
28+
cwd=checkout_dir,
29+
check=True,
30+
)
31+
subprocess.run(
32+
[
33+
"git",
34+
"checkout",
35+
"--force",
36+
"FETCH_HEAD",
37+
],
38+
cwd=checkout_dir,
39+
check=True,
40+
)
41+
42+
43+
def sparse_checkout(
44+
subdir: str,
45+
repo_url: str,
46+
paths: list[str],
47+
branch: str = "master",
48+
ref: str | None = None,
49+
):
50+
"""
51+
Perform sparse checkout of specified paths from a git repository.
52+
53+
Args:
54+
subdir: Subdirectory name under DEST_DIR
55+
repo_url: URL of the git repository
56+
paths: List of paths to checkout (e.g., ['brcm/', 'rtl_bt/'])
57+
branch: Git branch to pull from (default: 'master')
58+
ref: Optional git ref (commit sha or tag) to checkout. If provided,
59+
this ref will be fetched and checked out instead of pulling the
60+
branch. If None, the function will pull the specified branch as
61+
before.
62+
"""
63+
checkout_dir = DEST_DIR / subdir
64+
git_dir = checkout_dir / ".git"
65+
66+
# Check if repo already exists
67+
if git_dir.exists():
68+
# If a specific ref was requested, fetch and checkout that ref.
69+
if ref:
70+
_fetch_and_checkout(checkout_dir, ref)
71+
return
72+
73+
# No ref requested: just pull the latest changes from the branch
74+
subprocess.run(["git", "pull", "origin", branch], cwd=checkout_dir, check=True)
75+
return
76+
77+
# Create the directory
78+
checkout_dir.mkdir(parents=True, exist_ok=True)
79+
80+
# Initialize git repo
81+
subprocess.run(["git", "init"], cwd=checkout_dir, check=True)
82+
83+
# Add remote
84+
subprocess.run(
85+
["git", "remote", "add", "origin", repo_url], cwd=checkout_dir, check=True
86+
)
87+
88+
# Enable sparse checkout
89+
subprocess.run(
90+
["git", "config", "core.sparseCheckout", "true"], cwd=checkout_dir, check=True
91+
)
92+
93+
# Specify the paths to checkout
94+
sparse_checkout_file = checkout_dir / ".git" / "info" / "sparse-checkout"
95+
sparse_checkout_file.parent.mkdir(parents=True, exist_ok=True)
96+
sparse_checkout_file.write_text("\n".join(paths) + "\n")
97+
98+
# Fetch and checkout either the requested ref or the branch tip.
99+
ref_to_fetch = ref if ref is not None else branch
100+
_fetch_and_checkout(checkout_dir, ref_to_fetch)
101+
102+
103+
def collect_firmware(subdir: str, pattern: str):
104+
"""
105+
Create symbolic links for firmware files in DEST_DIR.
106+
107+
Args:
108+
subdir: Subdirectory under DEST_DIR containing firmware files
109+
pattern: Regex pattern to match and optionally extract parts for renaming.
110+
- If pattern has 2 capture groups, uses them concatenated as the link name
111+
- If pattern has no capture groups, uses original filename for matches
112+
"""
113+
firmware_dir = DEST_DIR / subdir
114+
115+
if not firmware_dir.exists():
116+
return
117+
118+
# Compile pattern
119+
regex = re.compile(pattern)
120+
121+
for firmware_file in firmware_dir.iterdir():
122+
if not firmware_file.is_file():
123+
continue
124+
125+
# Check if filename matches pattern
126+
match = regex.match(firmware_file.name)
127+
if not match:
128+
continue
129+
130+
# Determine link name based on capture groups
131+
groups = match.groups()
132+
if len(groups) == 2:
133+
# Two groups: concatenate them for the link name
134+
link_name = DEST_DIR / "".join(groups)
135+
else:
136+
# No groups: use original filename
137+
link_name = DEST_DIR / firmware_file.name
138+
139+
# Skip if link already exists
140+
if link_name.exists() or link_name.is_symlink():
141+
continue
142+
143+
# Create the symbolic link
144+
link_name.symlink_to(firmware_file)
145+
146+
147+
def main():
148+
"""Main entry point for collecting bluetooth patch files."""
149+
parser = argparse.ArgumentParser(
150+
description="Collect bluetooth patch files in the user's cache directory"
151+
)
152+
parser.add_argument(
153+
"--clean",
154+
action="store_true",
155+
help="Delete the entire destination directory before collecting",
156+
)
157+
args = parser.parse_args()
158+
159+
# Clean destination directory if requested
160+
if args.clean and DEST_DIR.exists():
161+
shutil.rmtree(DEST_DIR)
162+
163+
# Checkout brcm directory from broadcom-bt-firmware repo
164+
sparse_checkout(
165+
subdir="brcm",
166+
repo_url="https://github.com/winterheart/broadcom-bt-firmware",
167+
paths=["brcm/"],
168+
branch="master",
169+
ref="v12.0.1.1105_p4",
170+
)
171+
172+
# Checkout rtl_bt and intel directories from linux-firmware repo
173+
sparse_checkout(
174+
subdir="linux_firmware",
175+
repo_url="https://gitlab.com/kernel-firmware/linux-firmware.git",
176+
paths=["rtl_bt/", "intel/"],
177+
branch="main",
178+
ref="20251125",
179+
)
180+
181+
# Collect firmware files into a single directory. Rename the brcm firmware
182+
# files to match btstack's filename expectations.
183+
collect_firmware("brcm/brcm", r"([^-]+)[^.]*(\..+)")
184+
collect_firmware("linux_firmware/intel", r"^ibt.*(?:(ddc|sfi))$")
185+
collect_firmware("linux_firmware/rtl_bt", r"^.*\.bin$")
186+
187+
188+
if __name__ == "__main__":
189+
main()

0 commit comments

Comments
 (0)