Coverage for apio/managers/programmers.py: 78%
193 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""DOC: TODO"""
11from apio.common.apio_console import cout, cwarning, fatal_error
12from apio.common import proto_util
13from apio.common.debug_util import is_debug
14from apio.utils import serial_util, usb_util
15from apio.utils.serial_util import SerialDevice, SerialDeviceFilter
16from apio.utils.usb_util import UsbDevice, UsbDeviceFilter
17from apio.apio_context import ApioContext
19# -- For USB devices
20VID_VAR = "${VID}"
21PID_VAR = "${PID}"
22BUS_VAR = "${BUS}"
23DEV_VAR = "${DEV}"
24SERIAL_NUM_VAR = "${SERIAL_NUM}"
26USB_VARS = [VID_VAR, PID_VAR, BUS_VAR, DEV_VAR, SERIAL_NUM_VAR]
28# -- For serial devices.
29SERIAL_PORT_VAR = "${SERIAL_PORT}"
30SERIAL_VARS = [SERIAL_PORT_VAR]
32# -- The ${BIN_FILE} placed holder is replaced here with $SOURCE and later
33# -- in scons with the bitstream file path. It can appear in both USB and
34# -- serial devices.
35BIN_FILE_VAR = "${BIN_FILE}"
36BIN_FILE_VALUE = "$SOURCE"
38# -- All possible vars.
39ALL_VARS = USB_VARS + SERIAL_VARS + [BIN_FILE_VAR]
42class _DeviceScanner:
43 """Provides usb and serial devices scanning, with caching."""
45 def __init__(self, apio_ctx: ApioContext):
46 self._apio_ctx: ApioContext = apio_ctx
47 self._usb_devices: list[UsbDevice] | None = None
48 self._serial_devices: list[SerialDevice] | None = None
50 def get_usb_devices(self) -> list[UsbDevice]:
51 """Scan usb devices, with caching."""
52 if self._usb_devices is None:
53 self._usb_devices = usb_util.scan_usb_devices(self._apio_ctx)
54 assert isinstance(self._usb_devices, list)
55 return self._usb_devices
57 def get_serial_devices(self) -> list[SerialDevice]:
58 """Scan serial devices, with caching."""
59 if self._serial_devices is None:
60 self._serial_devices = serial_util.scan_serial_devices(
61 self._apio_ctx
62 )
63 assert isinstance(self._serial_devices, list)
64 return self._serial_devices
67def construct_programmer_cmd(
68 apio_ctx: ApioContext,
69 serial_port_flag: str | None,
70 serial_num_flag: str | None,
71) -> str:
72 """Construct the programmer command for an 'apio upload' command."""
74 # -- This is a thin wrapper to allow injecting test scanners in tests.
75 scanner = _DeviceScanner(apio_ctx)
76 return _construct_programmer_cmd(
77 apio_ctx, scanner, serial_port_flag, serial_num_flag
78 )
81def _construct_programmer_cmd(
82 apio_ctx: ApioContext,
83 scanner: _DeviceScanner,
84 serial_port_flag: str | None,
85 serial_num_flag: str | None,
86) -> str:
87 """Construct the programmer command for an 'apio upload' command."""
89 # -- Construct the programmer cmd template for the board. It may or may not
90 # -- contain ${} vars.
91 cmd_template = _construct_cmd_template(apio_ctx)
92 if is_debug(1): 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 cout(f"Cmd template: [{cmd_template}]")
95 # -- Resolved the mandatory ${BIN_FILE} to $SOURCE which will be replaced
96 # -- by scons with the path of the bitstream file.
97 cmd_template = cmd_template.replace(BIN_FILE_VAR, BIN_FILE_VALUE)
99 # -- Determine how to resolve this template.
100 has_usb_vars = any(s in cmd_template for s in USB_VARS)
101 has_serial_vars = any(s in cmd_template for s in SERIAL_VARS)
103 if is_debug(1): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 cout(f"Template has usb vars: {has_usb_vars}]")
105 cout(f"Template has serial vars: {has_serial_vars}]")
107 # -- Can't have both serial and usb vars (OK to have none).
108 if has_usb_vars and has_serial_vars: 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true
109 board = apio_ctx.project.get_str_option("board")
110 fatal_error(
111 f"The programmer cmd template of the board '{board}' has "
112 + "both usb and serial ${} vars. ",
113 info=f"Cmd template: {cmd_template}",
114 )
116 # -- Dispatch to the appropriate template resolver.
117 if has_serial_vars:
118 cmd = _resolve_serial_cmd_template(
119 apio_ctx, scanner, serial_port_flag, serial_num_flag, cmd_template
120 )
122 elif has_usb_vars:
123 _report_unused_flag("--serial-port", serial_port_flag)
124 cmd = _resolve_usb_cmd_template(
125 apio_ctx, scanner, serial_num_flag, cmd_template
126 )
128 else:
129 # -- If there are no vars to resolve, we don't need to match to a
130 # -- specific usb or serial device but just to check that if the board
131 # -- has 'usb' section, there is at least one device that matchs the
132 # -- constraints in that section.
133 _report_unused_flag("--serial-port", serial_port_flag)
134 _report_unused_flag("--serial-num", serial_num_flag)
135 _check_device_presence(apio_ctx, scanner)
137 # -- Template has no vars, we just use it as is.
138 cmd = cmd_template
140 # -- At this point, all vars should be resolved.
141 assert not any(s in cmd for s in ALL_VARS), cmd_template
143 # -- Return the resolved command.
144 return cmd
147def _report_unused_flag(flag_name: str, flag_value: str | None):
148 """If flag_value is not falsy then print a warning message."""
149 if flag_value: 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 cwarning(f"{flag_name} ignored.")
153def _construct_cmd_template(apio_ctx: ApioContext) -> str:
154 """Construct a command template for the board.
155 Example:
156 "openFPGAloader --verify -b ice40_generic --vid ${VID} --pid ${PID}
157 --busdev-num ${BUS}:${DEV} ${BIN_FILE}"
158 """
160 # -- If the project file has a custom programmer command use it instead
161 # -- of the standard definitions.
162 custom_template = apio_ctx.project.get_str_option("programmer-cmd")
163 if custom_template:
164 cout("Using custom programmer cmd.")
165 if BIN_FILE_VALUE in custom_template: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 fatal_error(
167 f"Custom programmer-cmd should not contain '{BIN_FILE_VALUE}'."
168 )
169 return custom_template
171 pr = apio_ctx.project_resources
172 board_definition = pr.board_definition
173 programmer_definition = pr.programmer_definition
175 # -- Here when using the standard command.
177 # -- Start building the template with the programmer binary name.
178 # -- E.g. "openFPGAloader". "command" is a validated required field.
179 proto_util.check_is_required(programmer_definition, "command")
180 cmd_template = programmer_definition.command
182 # -- Append the optional args template from the programmer. The 'args'
183 # -- field is required but may be empty.
184 proto_util.check_is_required(programmer_definition, "args")
185 args = programmer_definition.args
186 if args: 186 ↛ 191line 186 didn't jump to line 191 because the condition on line 186 was always true
187 cmd_template += " "
188 cmd_template += args
190 # -- Append the optional extra args template from the board.
191 proto_util.check_not_required(board_definition, "programmer.extra_args")
192 extra_args = board_definition.programmer.extra_args
193 if extra_args: 193 ↛ 199line 193 didn't jump to line 199 because the condition on line 193 was always true
194 cmd_template += " "
195 cmd_template += extra_args
197 # -- Append the bitstream file placeholder if its' not already in the
198 # -- template.
199 if BIN_FILE_VAR not in cmd_template: 199 ↛ 204line 199 didn't jump to line 204 because the condition on line 199 was always true
200 cmd_template += " "
201 cmd_template += BIN_FILE_VAR
203 # -- All done.
204 return cmd_template
207def _resolve_serial_cmd_template(
208 apio_ctx: ApioContext,
209 scanner: _DeviceScanner,
210 serial_port_arg: str | None,
211 serial_port_num: str | None,
212 cmd_template: str,
213) -> str:
214 """Resolves a programmer command template for a serial device."""
216 # -- Match to a single serial device.
217 device: SerialDevice = _match_serial_device(
218 apio_ctx, scanner, serial_port_arg, serial_port_num
219 )
221 # -- Resolve serial port var.
222 cmd_template = cmd_template.replace(SERIAL_PORT_VAR, device.port)
224 # -- Sanity check, should have no serial vars unresolved.
225 assert not any(s in cmd_template for s in SERIAL_VARS), cmd_template
227 # -- All done.
228 return cmd_template
231def _resolve_usb_cmd_template(
232 apio_ctx: ApioContext,
233 scanner: _DeviceScanner,
234 serial_num_flag: str | None,
235 cmd_template: str,
236) -> str:
237 """Resolves a programmer command template for an USB device."""
239 # -- Match to a single usb device.
240 device: UsbDevice = _match_usb_device(apio_ctx, scanner, serial_num_flag)
242 # -- Substitute vars.
243 cmd_template = cmd_template.replace(VID_VAR, device.vid)
244 cmd_template = cmd_template.replace(PID_VAR, device.pid)
245 cmd_template = cmd_template.replace(BUS_VAR, str(device.bus))
246 cmd_template = cmd_template.replace(DEV_VAR, str(device.device))
247 cmd_template = cmd_template.replace(SERIAL_NUM_VAR, device.serial_number)
249 # -- Sanity check, should have no usb vars unresolved.
250 assert not any(s in cmd_template for s in USB_VARS), cmd_template
252 # -- All done.
253 return cmd_template
256def _match_serial_device(
257 apio_ctx: ApioContext,
258 scanner: _DeviceScanner,
259 serial_port_flag: str | None,
260 serial_num_flag: str | None,
261) -> SerialDevice:
262 """Scans the serial devices and selects and returns a single matching
263 device. Exits with an error if none or multiple matching devices.
264 """
266 # -- Get project resources
267 pr = apio_ctx.project_resources
268 board_definition = pr.board_definition
270 # -- Scan for all serial devices.
271 all_devices: list[SerialDevice] = scanner.get_serial_devices()
273 # -- Get board optional usb constraints
274 proto_util.check_not_required(board_definition, "usb")
275 usb_info = (
276 board_definition.usb if board_definition.HasField("usb") else None
277 )
279 # -- Construct a device filter.
280 serial_filter = SerialDeviceFilter()
281 if usb_info: 281 ↛ 290line 281 didn't jump to line 290 because the condition on line 281 was always true
282 proto_util.check_not_required(usb_info, "vid", "pid", "product_regex")
283 if usb_info.vid: 283 ↛ 285line 283 didn't jump to line 285 because the condition on line 283 was always true
284 serial_filter.set_vid(usb_info.vid.upper())
285 if usb_info.pid: 285 ↛ 287line 285 didn't jump to line 287 because the condition on line 285 was always true
286 serial_filter.set_pid(usb_info.pid.upper())
287 if usb_info.product_regex: 287 ↛ 288line 287 didn't jump to line 288 because the condition on line 287 was never true
288 serial_filter.set_product_regex(usb_info.product_regex)
290 if serial_port_flag: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 serial_filter.set_port(serial_port_flag)
292 if serial_num_flag: 292 ↛ 293line 292 didn't jump to line 293 because the condition on line 292 was never true
293 serial_filter.set_serial_num(serial_num_flag)
295 # -- Inform the user.
296 cout("Scanning for a serial device:")
297 cout(f"- FILTER {serial_filter.summary()}")
299 # -- Get matching devices
300 matching: list[SerialDevice] = serial_filter.filter(all_devices)
302 for dev in matching:
303 cout(f"- DEVICE {dev.summary()}")
305 if is_debug(1): 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true
306 cout(f"Serial device filter: {serial_filter.summary()}")
307 cout(f"Matching serial devices: {matching}")
309 if is_debug(1): 309 ↛ 310line 309 didn't jump to line 310 because the condition on line 309 was never true
310 cout(f"Matching serial devices: {matching}")
312 # -- Error if not exactly one match.
313 if not matching:
314 fatal_error(
315 "No matching serial device.",
316 info="Type 'apio devices scan-serial' for available "
317 + "serial devices.",
318 )
320 # -- Error more than one match
321 if len(matching) > 1:
322 fatal_error(
323 "Found multiple matching serial devices.",
324 info="Type 'apio devices scan-serial' for available "
325 + "serial devices.",
326 )
328 # -- All done. We have a single match.
329 return matching[0]
332def _match_usb_device(
333 apio_ctx: ApioContext, scanner, serial_num_flag: str | None
334) -> UsbDevice:
335 """Scans the USB devices and selects and returns a single matching
336 device. Exits with an error if none or multiple matching devices.
337 """
339 # -- Get project resources.
340 pr = apio_ctx.project_resources
341 board_definition = pr.board_definition
343 # -- Scan for all serial devices.
344 all_devices: list[UsbDevice] = scanner.get_usb_devices()
346 # -- Get board optional usb constraints
347 proto_util.check_not_required(board_definition, "usb")
348 usb_info = (
349 pr.board_definition.usb
350 if pr.board_definition.HasField("usb")
351 else None
352 )
354 # -- Construct a device filter.
355 usb_filter = UsbDeviceFilter()
356 if usb_info: 356 ↛ 365line 356 didn't jump to line 365 because the condition on line 356 was always true
357 proto_util.check_not_required(usb_info, "vid", "pid", "product_regex")
358 if usb_info.vid: 358 ↛ 360line 358 didn't jump to line 360 because the condition on line 358 was always true
359 usb_filter.set_vid(usb_info.vid.upper())
360 if usb_info.pid: 360 ↛ 362line 360 didn't jump to line 362 because the condition on line 360 was always true
361 usb_filter.set_pid(usb_info.pid.upper())
362 if usb_info.product_regex: 362 ↛ 365line 362 didn't jump to line 365 because the condition on line 362 was always true
363 usb_filter.set_product_regex(usb_info.product_regex)
365 if serial_num_flag: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true
366 usb_filter.set_serial_num(serial_num_flag)
368 # -- Inform the user.
369 cout("Scanning for a USB device:")
370 cout(f"- FILTER {usb_filter.summary()}")
372 # -- Get matching devices
373 matching: list[UsbDevice] = usb_filter.filter(all_devices)
375 for dev in matching:
376 cout(f"- DEVICE {dev.summary()}")
378 if is_debug(1): 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true
379 cout(f"USB device filter: {usb_filter.summary()}")
380 cout(f"Matching USB devices: {matching}")
382 if is_debug(1): 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true
383 cout(f"Matching usb devices: {matching}")
385 # -- Error if not exactly one match.
386 if not matching:
387 fatal_error(
388 "No matching USB device.",
389 info="Type 'apio devices scan-usb' for available usb devices.",
390 )
392 # -- Error more than one match
393 if len(matching) > 1:
394 fatal_error(
395 "Found multiple matching usb devices.",
396 info="Type 'apio devices scan-usb' for available usb device.",
397 )
399 # -- All done. We have a single match.
400 return matching[0]
403def _check_device_presence(apio_ctx: ApioContext, scanner: _DeviceScanner):
404 """If the board info has a "usb" section, check that there is at least one
405 usb device that matches the constraints, if any, in the "usb" section.
406 Returns if OK, exits with an error otherwise.
407 """
409 # -- Get project resources.
410 pr = apio_ctx.project_resources
411 board_definition = pr.board_definition
413 # -- If board has no usb constrains than nothing to do.
414 proto_util.check_not_required(board_definition, "usb")
415 if not board_definition.HasField("usb"): 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true
416 return
418 # -- Get board optional usb constraints
419 usb_info = (
420 board_definition.usb if board_definition.HasField("usb") else None
421 )
423 # -- Create a device filter with the constraints. Note that the "usb"
424 # -- section may contain no constrained which will result in a pass-all
425 # -- filter.
426 usb_filter = UsbDeviceFilter()
427 if usb_info: 427 ↛ 436line 427 didn't jump to line 436 because the condition on line 427 was always true
428 proto_util.check_not_required(usb_info, "vid", "pid", "product_regex")
429 if usb_info.vid: 429 ↛ 431line 429 didn't jump to line 431 because the condition on line 429 was always true
430 usb_filter.set_vid(usb_info.vid.upper())
431 if usb_info.pid: 431 ↛ 433line 431 didn't jump to line 433 because the condition on line 431 was always true
432 usb_filter.set_pid(usb_info.pid.upper())
433 if usb_info.product_regex: 433 ↛ 436line 433 didn't jump to line 436 because the condition on line 433 was always true
434 usb_filter.set_product_regex(usb_info.product_regex)
436 cout("Checking device presence...")
437 cout(f"- FILTER {usb_filter.summary()}")
439 # -- Scan the USB devices and filter by the filter.
440 all_devices = scanner.get_usb_devices()
441 matching_devices = usb_filter.filter(all_devices)
443 for device in matching_devices:
444 cout(f"- DEVICE {device.summary()}")
446 # -- If no device passed the filter fail the check.
447 if not matching_devices:
448 fatal_error(
449 "No matching device.",
450 info="Type 'apio devices scan-usb' for available usb devices.",
451 )
453 # -- All OK.