Skip to content

Commit 1501de7

Browse files
authored
Merge pull request #8882 from FoamyGuy/board_stubs
Board Specific Stubs
2 parents 650004e + 863f925 commit 1501de7

File tree

4 files changed

+347
-0
lines changed

4 files changed

+347
-0
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,8 @@ stubs:
271271
@cp setup.py-stubs circuitpython-stubs/setup.py
272272
@cp README.rst-stubs circuitpython-stubs/README.rst
273273
@cp MANIFEST.in-stubs circuitpython-stubs/MANIFEST.in
274+
@$(PYTHON) tools/board_stubs/build_board_specific_stubs/board_stub_builder.py
275+
@cp -r tools/board_stubs/circuitpython_setboard circuitpython-stubs/circuitpython_setboard
274276
@$(PYTHON) -m build circuitpython-stubs
275277

276278
.PHONY: check-stubs

setup.py-stubs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def build_package_data() -> Dict[str, List[str]]:
2424
result = {}
2525
for package in packages:
2626
result[f"{package}-stubs"] = ["*.pyi", "*/*.pyi"]
27+
result['circuitpython_setboard'] = ["*.py", "*/*.py"]
28+
result['board_definitions'] = ["*.pyi", "*/*.pyi"]
2729
return result
2830

2931
package_data=build_package_data()
@@ -35,6 +37,11 @@ setup(
3537
maintainer_email="[email protected]",
3638
author_email="[email protected]",
3739
license="MIT",
40+
entry_points = {
41+
'console_scripts': [
42+
'circuitpython_setboard = circuitpython_setboard:set_board'
43+
]
44+
},
3845
packages=list(package_data.keys()),
3946
package_data=package_data,
4047
package_dir = package_dir,
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
# SPDX-FileCopyrightText: 2024 Justin Myers
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
import json
6+
import os
7+
import re
8+
import sys
9+
from collections import defaultdict
10+
11+
12+
def get_board_pins(pin_filename):
13+
records = []
14+
15+
with open(pin_filename, encoding="utf-8") as pin_file:
16+
for line in pin_file:
17+
if line.strip()[0:2] == "//":
18+
continue
19+
20+
search = re.search(r"MP_ROM_QSTR\(MP_QSTR_(.*?)\), MP_ROM_PTR", line)
21+
if search is None:
22+
search = re.search(r"MP_OBJ_NEW_QSTR\(MP_QSTR_(.*?)\), MP_ROM_PTR", line)
23+
if search is None:
24+
continue
25+
26+
board_member = search.group(1)
27+
28+
board_type_search = re.search(r"MP_ROM_PTR\(&pin_(.*?)\)", line)
29+
if board_type_search:
30+
board_type = "pin"
31+
else:
32+
if board_type_search is None:
33+
board_type_search = re.search(r"MP_ROM_PTR\(&(.*?)_obj", line)
34+
if board_type_search is None:
35+
board_type_search = re.search(
36+
r"MP_ROM_PTR\(&(.*?)\[0\].[display|epaper_display]", line
37+
)
38+
if board_type_search is None:
39+
records.append(["unmapped", None, line])
40+
continue
41+
42+
board_type = board_type_search.group(1)
43+
44+
extra_search = None
45+
extra = None
46+
47+
if board_type == "pin":
48+
extra_search = re.search(r"&pin_(.*?)\)", line)
49+
50+
elif board_type == "displays":
51+
extra_search = re.search(r"&displays\[0\].(.*?)\)", line)
52+
53+
if extra_search:
54+
extra = extra_search.group(1)
55+
56+
records.append([board_type, board_member, extra])
57+
58+
return records
59+
60+
61+
def create_board_stubs(board_id, records, mappings, board_filename):
62+
pins = []
63+
members = []
64+
unmapped = []
65+
66+
needs_busio = False
67+
needs_displayio = False
68+
needs_microcontroller = False
69+
70+
for board_type, board_member, extra in records:
71+
if board_type == "pin":
72+
needs_microcontroller = True
73+
comment = f" # {extra}"
74+
pins.append(f"{board_member}: microcontroller.Pin{comment}")
75+
76+
elif board_type == "board_i2c":
77+
needs_busio = True
78+
import_name = "I2C"
79+
class_name = f"busio.{import_name}"
80+
member_data = f"def {board_member}() -> {class_name}:\n"
81+
member_data += f' """Returns the `{class_name}` object for the board\'s designated I2C bus(es).\n'
82+
member_data += f" The object created is a singleton, and uses the default parameter values for `{class_name}`.\n"
83+
member_data += ' """\n'
84+
members.append(member_data)
85+
86+
elif board_type == "board_spi":
87+
needs_busio = True
88+
import_name = "SPI"
89+
class_name = f"busio.{import_name}"
90+
member_data = f"def {board_member}() -> {class_name}:\n"
91+
member_data += f' """Returns the `{class_name}` object for the board\'s designated SPI bus(es).\n'
92+
member_data += f" The object created is a singleton, and uses the default parameter values for `{class_name}`.\n"
93+
member_data += ' """\n'
94+
members.append(member_data)
95+
96+
elif board_type == "board_uart":
97+
needs_busio = True
98+
import_name = "UART"
99+
class_name = f"busio.{import_name}"
100+
member_data = f"def {board_member}() -> {class_name}:\n"
101+
member_data += f' """Returns the `{class_name}` object for the board\'s designated UART bus(es).\n'
102+
member_data += f" The object created is a singleton, and uses the default parameter values for `{class_name}`.\n"
103+
member_data += ' """\n'
104+
members.append(member_data)
105+
106+
elif board_type == "displays":
107+
needs_displayio = True
108+
if extra == "display":
109+
import_name = "Display"
110+
elif extra == "epaper_display":
111+
import_name = "EPaperDisplay"
112+
class_name = f"displayio.{import_name}"
113+
member_data = (
114+
f'"""Returns the `{class_name}` object for the board\'s built in display.\n'
115+
)
116+
member_data += f"The object created is a singleton, and uses the default parameter values for `{class_name}`.\n"
117+
member_data += '"""\n'
118+
member_data += f"{board_member}: {class_name}\n"
119+
members.append(member_data)
120+
121+
elif board_type == "unmapped":
122+
unmapped.append(extra)
123+
124+
libraries = mappings["libraries"]
125+
included_modules = "Unknown"
126+
frozen_libraries = "Unknown"
127+
if "modules" in libraries:
128+
included_modules = ", ".join(libraries["modules"])
129+
if "frozen_libraries" in libraries:
130+
frozen_libraries = ", ".join(libraries["frozen_libraries"])
131+
132+
with open(board_filename, "w") as boards_file:
133+
boards_file.write("# SPDX-FileCopyrightText: 2024 Justin Myers\n")
134+
boards_file.write("#\n")
135+
boards_file.write("# SPDX-License-Identifier: MIT\n")
136+
boards_file.write('"""\n')
137+
boards_file.write(f'Board stub for {mappings["board_name"]}\n')
138+
boards_file.write(f' - port: {mappings["port"]}\n')
139+
boards_file.write(f" - board_id: {board_id}\n")
140+
boards_file.write(f' - NVM size: {mappings["nvm_size"]}\n')
141+
boards_file.write(f" - Included modules: {included_modules}\n")
142+
boards_file.write(f" - Frozen libraries: {frozen_libraries}\n")
143+
boards_file.write('"""\n\n')
144+
boards_file.write("# Imports\n")
145+
if needs_busio:
146+
boards_file.write("import busio\n")
147+
if needs_displayio:
148+
boards_file.write("import displayio\n")
149+
if needs_microcontroller:
150+
boards_file.write("import microcontroller\n")
151+
152+
boards_file.write("\n\n")
153+
boards_file.write("# Board Info:\n")
154+
boards_file.write("board_id: str\n")
155+
156+
boards_file.write("\n\n")
157+
boards_file.write("# Pins:\n")
158+
for pin in pins:
159+
boards_file.write(f"{pin}\n")
160+
161+
boards_file.write("\n\n")
162+
boards_file.write("# Members:\n")
163+
for member in members:
164+
boards_file.write(f"{member}\n")
165+
166+
boards_file.write("\n")
167+
boards_file.write("# Unmapped:\n")
168+
if not unmapped:
169+
boards_file.write("# none\n")
170+
for record in unmapped:
171+
boards_file.write(f"# {record}")
172+
173+
174+
def process(board_mappings, export_dir):
175+
total_boards = 0
176+
total_pins = 0
177+
total_members = 0
178+
total_unmapped = 0
179+
skipped_boards = []
180+
unmapped_boards = defaultdict(int)
181+
unmapped_values = defaultdict(list)
182+
183+
for board_id, mappings in board_mappings.items():
184+
total_boards += 1
185+
186+
if "pins_filename" not in mappings:
187+
skipped_boards.append(board_id)
188+
continue
189+
190+
pin_filename = mappings["pins_filename"]
191+
192+
sub_dir = f"{export_dir}/{board_id}"
193+
if not os.path.exists(sub_dir):
194+
os.makedirs(sub_dir)
195+
196+
try:
197+
records = get_board_pins(pin_filename)
198+
create_board_stubs(board_id, records, mappings, f"{sub_dir}/__init__.pyi")
199+
200+
for board_type, board_member, extra in records:
201+
if board_type == "pin":
202+
total_pins += 1
203+
elif board_type == "unmapped":
204+
unmapped_boards[board_id] += 1
205+
unmapped_values[extra].append(board_id)
206+
total_unmapped += 1
207+
else:
208+
total_members += 1
209+
210+
except Exception as e:
211+
print(f" - {e}")
212+
213+
unmapped_percent = total_unmapped / (total_pins + total_members + total_unmapped)
214+
215+
print("\nTotals:")
216+
print(f" boards: {total_boards}")
217+
print(f" pins: {total_pins}")
218+
print(f" members: {total_members}")
219+
print(f" unmapped: {total_unmapped} ({unmapped_percent:.5f}%)")
220+
print("\n\nSkipped Boards")
221+
for board in skipped_boards:
222+
print(f" {board}")
223+
print("\n\nBoards with Unmapped Pins:")
224+
for board, total in unmapped_boards.items():
225+
print(f" {board}: {total}")
226+
print("\n\nUnmapped Pins:")
227+
for unmapped, boards in unmapped_values.items():
228+
print(f" {unmapped.strip()}")
229+
for board in boards:
230+
print(f" - {board}")
231+
232+
233+
def build_stubs(circuitpython_dir, circuitpython_org_dir, export_dir, version="8.2.9"):
234+
if circuitpython_dir[-1] != "/":
235+
circuitpython_dir = circuitpython_dir + "/"
236+
237+
sys.path.append(circuitpython_dir)
238+
from docs import shared_bindings_matrix
239+
240+
if not os.path.exists(export_dir):
241+
os.makedirs(export_dir)
242+
243+
libraries = {}
244+
if circuitpython_org_dir is None:
245+
libraries = shared_bindings_matrix.support_matrix_by_board(
246+
use_branded_name=False, withurl=False
247+
)
248+
else:
249+
with open(f"{circuitpython_org_dir}/_data/files.json") as libraries_file:
250+
libraries_list = json.load(libraries_file)
251+
for library in libraries_list:
252+
board = library["id"]
253+
for version_data in library["versions"]:
254+
if version_data["version"] == version:
255+
libraries[board] = version_data
256+
257+
aliases = {}
258+
for board, renames in shared_bindings_matrix.ALIASES_BY_BOARD.items():
259+
for rename in renames:
260+
aliases[rename] = board
261+
262+
board_mappings = shared_bindings_matrix.get_board_mapping()
263+
for board, board_data in board_mappings.items():
264+
if board in aliases:
265+
lookup = aliases[board]
266+
else:
267+
lookup = board
268+
269+
port_path = f'{circuitpython_dir}ports/{board_data["port"]}/'
270+
board_path = f"{port_path}boards/{lookup}/"
271+
pins_path = f"{board_path}pins.c"
272+
if not os.path.isfile(pins_path):
273+
print(f"Could not find pin file for {lookup}")
274+
continue
275+
276+
board_mappings[board]["pins_filename"] = pins_path
277+
278+
nvm_size = "Unknown"
279+
with open(f"{port_path}/mpconfigport.h") as get_name:
280+
port_contents = get_name.read()
281+
port_nvm_re = re.search(
282+
r"(?<=#define CIRCUITPY_INTERNAL_NVM_SIZE)\s+(.+)", port_contents
283+
)
284+
if port_nvm_re:
285+
nvm_size = port_nvm_re.group(1).strip()
286+
287+
board_name = board
288+
with open(f"{board_path}/mpconfigboard.h") as get_name:
289+
board_contents = get_name.read()
290+
board_name_re = re.search(r"(?<=MICROPY_HW_BOARD_NAME)\s+(.+)", board_contents)
291+
if board_name_re:
292+
board_name = board_name_re.group(1).strip('"')
293+
294+
port_nvm_re = re.search(
295+
r"(?<=#define CIRCUITPY_INTERNAL_NVM_SIZE)\s+(.+)", port_contents
296+
)
297+
if port_nvm_re:
298+
nvm_size = port_nvm_re.group(1).strip()
299+
300+
nvm_size_re = re.search(r"^[0-9\(\) *]*$", nvm_size)
301+
if nvm_size_re:
302+
nvm_size = eval(nvm_size_re.group(0))
303+
304+
board_mappings[board]["board_name"] = board_name
305+
board_mappings[board]["nvm_size"] = nvm_size
306+
board_mappings[board]["libraries"] = libraries.get(board, None)
307+
308+
process(board_mappings, export_dir)
309+
310+
311+
if __name__ == "__main__":
312+
build_stubs("./", None, "circuitpython-stubs/board_definitions/")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import sys
5+
import os
6+
import shutil
7+
8+
9+
def set_board():
10+
chosen_board = sys.argv[1]
11+
print(f"setting board: {chosen_board}")
12+
board_defs_path = (
13+
os.path.sep.join(__file__.split("/")[:-2]) + f"{os.path.sep}board_definitions{os.path.sep}"
14+
)
15+
board_stubs_path = (
16+
os.path.sep.join(__file__.split("/")[:-2])
17+
+ f"{os.path.sep}board-stubs{os.path.sep}__init__.pyi"
18+
)
19+
20+
if chosen_board not in os.listdir(board_defs_path):
21+
print(f"Board: '{chosen_board}' was not found")
22+
return
23+
24+
shutil.copyfile(
25+
board_defs_path + f"{os.path.sep}{chosen_board}{os.path.sep}__init__.pyi", board_stubs_path
26+
)

0 commit comments

Comments
 (0)