Skip to content

Commit c96c040

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 16e461e commit c96c040

1 file changed

Lines changed: 150 additions & 0 deletions

File tree

tools/collect_bt_patches.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.home() / ".cache" / "pybricks" / "virtualhub" / "bt_firmware"
18+
19+
20+
def sparse_checkout(
21+
subdir: str, repo_url: str, paths: list[str], branch: str = "master"
22+
):
23+
"""
24+
Perform sparse checkout of specified paths from a git repository.
25+
26+
Args:
27+
subdir: Subdirectory name under DEST_DIR
28+
repo_url: URL of the git repository
29+
paths: List of paths to checkout (e.g., ['brcm/', 'rtl_bt/'])
30+
branch: Git branch to pull from (default: 'master')
31+
"""
32+
checkout_dir = DEST_DIR / subdir
33+
git_dir = checkout_dir / ".git"
34+
35+
# Check if repo already exists
36+
if git_dir.exists():
37+
# Just pull the latest changes
38+
subprocess.run(["git", "pull", "origin", branch], cwd=checkout_dir, check=True)
39+
return
40+
41+
# Create the directory
42+
checkout_dir.mkdir(parents=True, exist_ok=True)
43+
44+
# Initialize git repo
45+
subprocess.run(["git", "init"], cwd=checkout_dir, check=True)
46+
47+
# Add remote
48+
subprocess.run(
49+
["git", "remote", "add", "origin", repo_url], cwd=checkout_dir, check=True
50+
)
51+
52+
# Enable sparse checkout
53+
subprocess.run(
54+
["git", "config", "core.sparseCheckout", "true"], cwd=checkout_dir, check=True
55+
)
56+
57+
# Specify the paths to checkout
58+
sparse_checkout_file = checkout_dir / ".git" / "info" / "sparse-checkout"
59+
sparse_checkout_file.parent.mkdir(parents=True, exist_ok=True)
60+
sparse_checkout_file.write_text("\n".join(paths) + "\n")
61+
62+
# Pull the files
63+
subprocess.run(["git", "pull", "origin", branch], cwd=checkout_dir, check=True)
64+
65+
66+
def collect_firmware(subdir: str, pattern: str):
67+
"""
68+
Create symbolic links for firmware files in DEST_DIR.
69+
70+
Args:
71+
subdir: Subdirectory under DEST_DIR containing firmware files
72+
pattern: Regex pattern to match and optionally extract parts for renaming.
73+
- If pattern has 2 capture groups, uses them concatenated as the link name
74+
- If pattern has no capture groups, uses original filename for matches
75+
"""
76+
firmware_dir = DEST_DIR / subdir
77+
78+
if not firmware_dir.exists():
79+
return
80+
81+
# Compile pattern
82+
regex = re.compile(pattern)
83+
84+
for firmware_file in firmware_dir.iterdir():
85+
if not firmware_file.is_file():
86+
continue
87+
88+
# Check if filename matches pattern
89+
match = regex.match(firmware_file.name)
90+
if not match:
91+
continue
92+
93+
# Determine link name based on capture groups
94+
groups = match.groups()
95+
if len(groups) == 2:
96+
# Two groups: concatenate them for the link name
97+
link_name = DEST_DIR / "".join(groups)
98+
else:
99+
# No groups: use original filename
100+
link_name = DEST_DIR / firmware_file.name
101+
102+
# Skip if link already exists
103+
if link_name.exists() or link_name.is_symlink():
104+
continue
105+
106+
# Create the symbolic link
107+
link_name.symlink_to(firmware_file)
108+
109+
110+
def main():
111+
"""Main entry point for collecting bluetooth patch files."""
112+
parser = argparse.ArgumentParser(
113+
description="Collect bluetooth patch files in the user's cache directory"
114+
)
115+
parser.add_argument(
116+
"--clean",
117+
action="store_true",
118+
help="Delete the entire destination directory before collecting",
119+
)
120+
args = parser.parse_args()
121+
122+
# Clean destination directory if requested
123+
if args.clean and DEST_DIR.exists():
124+
shutil.rmtree(DEST_DIR)
125+
126+
# Checkout brcm directory from broadcom-bt-firmware repo
127+
sparse_checkout(
128+
subdir="brcm",
129+
repo_url="https://github.com/winterheart/broadcom-bt-firmware",
130+
paths=["brcm/"],
131+
branch="master",
132+
)
133+
134+
# Checkout rtl_bt and intel directories from linux-firmware repo
135+
sparse_checkout(
136+
subdir="linux_firmware",
137+
repo_url="https://gitlab.com/kernel-firmware/linux-firmware.git",
138+
paths=["rtl_bt/", "intel/"],
139+
branch="main",
140+
)
141+
142+
# Collect firmware files into a single directory. Rename the brcm firmware
143+
# files to match btstack's filename expectations.
144+
collect_firmware("brcm/brcm", r"([^-]+)[^.]*(\..+)")
145+
collect_firmware("linux_firmware/intel", r"^ibt.*(?:(ddc|sfi))$")
146+
collect_firmware("linux_firmware/rtl_bt", r"^.*\.bin$")
147+
148+
149+
if __name__ == "__main__":
150+
main()

0 commit comments

Comments
 (0)