1+ # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
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 (
23+ r"MP_OBJ_NEW_QSTR\(MP_QSTR_(.*?)\), MP_ROM_PTR" , line
24+ )
25+ if search is None :
26+ continue
27+
28+ board_member = search .group (1 )
29+
30+ board_type_search = re .search (r"MP_ROM_PTR\(&pin_(.*?)\)" , line )
31+ if board_type_search :
32+ board_type = "pin"
33+ else :
34+ if board_type_search is None :
35+ board_type_search = re .search (r"MP_ROM_PTR\(&(.*?)_obj" , line )
36+ if board_type_search is None :
37+ board_type_search = re .search (
38+ r"MP_ROM_PTR\(&(.*?)\[0\].[display|epaper_display]" , line
39+ )
40+ if board_type_search is None :
41+ records .append (["unmapped" , None , line ])
42+ continue
43+
44+ board_type = board_type_search .group (1 )
45+
46+ extra_search = None
47+ extra = None
48+
49+ if board_type == "pin" :
50+ extra_search = re .search (r"&pin_(.*?)\)" , line )
51+
52+ elif board_type == "displays" :
53+ extra_search = re .search (r"&displays\[0\].(.*?)\)" , line )
54+
55+ if extra_search :
56+ extra = extra_search .group (1 )
57+
58+ records .append ([board_type , board_member , extra ])
59+
60+ return records
61+
62+
63+ def create_board_stubs (board_id , records , mappings , board_filename ):
64+ pins = []
65+ members = []
66+ unmapped = []
67+
68+ needs_busio = False
69+ needs_displayio = False
70+ needs_microcontroller = False
71+
72+ for board_type , board_member , extra in records :
73+ if board_type == "pin" :
74+ needs_microcontroller = True
75+ comment = f" # { extra } "
76+ pins .append (f"{ board_member } : microcontroller.Pin{ comment } " )
77+
78+ elif board_type == "board_i2c" :
79+ needs_busio = True
80+ import_name = "I2C"
81+ class_name = f"busio.{ import_name } "
82+ member_data = f"def { board_member } () -> { class_name } :\n "
83+ member_data += f' """Returns the `{ class_name } ` object for the board\' s designated I2C bus(es).\n '
84+ member_data += f" The object created is a singleton, and uses the default parameter values for `{ class_name } `.\n "
85+ member_data += ' """\n '
86+ members .append (member_data )
87+
88+ elif board_type == "board_spi" :
89+ needs_busio = True
90+ import_name = "SPI"
91+ class_name = f"busio.{ import_name } "
92+ member_data = f"def { board_member } () -> { class_name } :\n "
93+ member_data += f' """Returns the `{ class_name } ` object for the board\' s designated SPI bus(es).\n '
94+ member_data += f" The object created is a singleton, and uses the default parameter values for `{ class_name } `.\n "
95+ member_data += ' """\n '
96+ members .append (member_data )
97+
98+ elif board_type == "board_uart" :
99+ needs_busio = True
100+ import_name = "UART"
101+ class_name = f"busio.{ import_name } "
102+ member_data = f"def { board_member } () -> { class_name } :\n "
103+ member_data += f' """Returns the `{ class_name } ` object for the board\' s designated UART bus(es).\n '
104+ member_data += f" The object created is a singleton, and uses the default parameter values for `{ class_name } `.\n "
105+ member_data += ' """\n '
106+ members .append (member_data )
107+
108+ elif board_type == "displays" :
109+ needs_displayio = True
110+ if extra == "display" :
111+ import_name = "Display"
112+ elif extra == "epaper_display" :
113+ import_name = "EPaperDisplay"
114+ class_name = f"displayio.{ import_name } "
115+ member_data = f'"""Returns the `{ class_name } ` object for the board\' s built in display.\n '
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 (
134+ "# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries\n "
135+ )
136+ boards_file .write ("#\n " )
137+ boards_file .write ("# SPDX-License-Identifier: MIT\n " )
138+ boards_file .write ('"""\n ' )
139+ boards_file .write (f'Board stub for { mappings ["board_name" ]} \n ' )
140+ boards_file .write (f' - port: { mappings ["port" ]} \n ' )
141+ boards_file .write (f" - board_id: { board_id } \n " )
142+ boards_file .write (f' - NVM size: { mappings ["nvm_size" ]} \n ' )
143+ boards_file .write (f" - Included modules: { included_modules } \n " )
144+ boards_file .write (f" - Frozen libraries: { frozen_libraries } \n " )
145+ boards_file .write ('"""\n \n ' )
146+ boards_file .write ("# Imports\n " )
147+ if needs_busio :
148+ boards_file .write ("import busio\n " )
149+ if needs_displayio :
150+ boards_file .write ("import displayio\n " )
151+ if needs_microcontroller :
152+ boards_file .write ("import microcontroller\n " )
153+
154+ boards_file .write ("\n \n " )
155+ boards_file .write ("# Board Info:\n " )
156+ boards_file .write ("board_id: str\n " )
157+
158+ boards_file .write ("\n \n " )
159+ boards_file .write ("# Pins:\n " )
160+ for pin in pins :
161+ boards_file .write (f"{ pin } \n " )
162+
163+ boards_file .write ("\n \n " )
164+ boards_file .write ("# Members:\n " )
165+ for member in members :
166+ boards_file .write (f"{ member } \n " )
167+
168+ boards_file .write ("\n " )
169+ boards_file .write ("# Unmapped:\n " )
170+ if not unmapped :
171+ boards_file .write ("# none\n " )
172+ for record in unmapped :
173+ boards_file .write (f"# { record } " )
174+
175+
176+ def process (board_mappings , export_dir ):
177+ total_boards = 0
178+ total_pins = 0
179+ total_members = 0
180+ total_unmapped = 0
181+ skipped_boards = []
182+ unmapped_boards = defaultdict (int )
183+ unmapped_values = defaultdict (list )
184+
185+ for board_id , mappings in board_mappings .items ():
186+ total_boards += 1
187+
188+ if "pins_filename" not in mappings :
189+ skipped_boards .append (board_id )
190+ continue
191+
192+ pin_filename = mappings ["pins_filename" ]
193+
194+ sub_dir = f"{ export_dir } /{ board_id } "
195+ if not os .path .exists (sub_dir ):
196+ os .makedirs (sub_dir )
197+
198+ try :
199+ records = get_board_pins (pin_filename )
200+ create_board_stubs (board_id , records , mappings , f"{ sub_dir } /__init__.pyi" )
201+
202+ for board_type , board_member , extra in records :
203+ if board_type == "pin" :
204+ total_pins += 1
205+ elif board_type == "unmapped" :
206+ unmapped_boards [board_id ] += 1
207+ unmapped_values [extra ].append (board_id )
208+ total_unmapped += 1
209+ else :
210+ total_members += 1
211+
212+ except Exception as e :
213+ print (f" - { e } " )
214+
215+ unmapped_percent = total_unmapped / (total_pins + total_members + total_unmapped )
216+
217+ print ("\n Totals:" )
218+ print (f" boards: { total_boards } " )
219+ print (f" pins: { total_pins } " )
220+ print (f" members: { total_members } " )
221+ print (f" unmapped: { total_unmapped } ({ unmapped_percent :.5f} %)" )
222+ print ("\n \n Skipped Boards" )
223+ for board in skipped_boards :
224+ print (f" { board } " )
225+ print ("\n \n Boards with Unmapped Pins:" )
226+ for board , total in unmapped_boards .items ():
227+ print (f" { board } : { total } " )
228+ print ("\n \n Unmapped Pins:" )
229+ for unmapped , boards in unmapped_values .items ():
230+ print (f" { unmapped .strip ()} " )
231+ for board in boards :
232+ print (f" - { board } " )
233+
234+
235+ def build_stubs (circuitpython_dir , circuitpython_org_dir , export_dir , version = "8.2.9" ):
236+ if circuitpython_dir [- 1 ] != "/" :
237+ circuitpython_dir = circuitpython_dir + "/"
238+
239+ sys .path .append (circuitpython_dir )
240+ from docs import shared_bindings_matrix
241+
242+ if not os .path .exists (export_dir ):
243+ os .makedirs (export_dir )
244+
245+ libraries = {}
246+ if circuitpython_org_dir is None :
247+ libraries = shared_bindings_matrix .support_matrix_by_board (use_branded_name = False , withurl = False )
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 (
291+ r"(?<=MICROPY_HW_BOARD_NAME)\s+(.+)" , board_contents
292+ )
293+ if board_name_re :
294+ board_name = board_name_re .group (1 ).strip ('"' )
295+
296+ port_nvm_re = re .search (
297+ r"(?<=#define CIRCUITPY_INTERNAL_NVM_SIZE)\s+(.+)" , port_contents
298+ )
299+ if port_nvm_re :
300+ nvm_size = port_nvm_re .group (1 ).strip ()
301+
302+ nvm_size_re = re .search (r"^[0-9\(\) *]*$" , nvm_size )
303+ if nvm_size_re :
304+ nvm_size = eval (nvm_size_re .group (0 ))
305+
306+ board_mappings [board ]["board_name" ] = board_name
307+ board_mappings [board ]["nvm_size" ] = nvm_size
308+ board_mappings [board ]["libraries" ] = libraries .get (board , None )
309+
310+ process (board_mappings , export_dir )
311+
312+ if __name__ == '__main__' :
313+ build_stubs ("./" , None , "circuitpython-stubs/board_definitions/" )
0 commit comments