Coverage for apio/managers/programmers.py: 78%
193 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
1"""DOC: TODO"""
3# -*- coding: utf-8 -*-
4# -- This file is part of the Apio project
5# -- (C) 2016-2019 FPGAwars
6# -- Author Jesús Arroyo
7# -- License GPLv2
9from apio.common.apio_console import cout, cwarning, fatal_error
10from apio.common import proto_util
11from apio.common.debug_util import is_debug
12from apio.utils import serial_util, usb_util
13from apio.utils.serial_util import SerialDevice, SerialDeviceFilter
14from apio.utils.usb_util import UsbDevice, UsbDeviceFilter
15from apio.apio_context import ApioContext
17# -- For USB devices
18VID_VAR = "${VID}"
19PID_VAR = "${PID}"
20BUS_VAR = "${BUS}"
21DEV_VAR = "${DEV}"
22SERIAL_NUM_VAR = "${SERIAL_NUM}"
24USB_VARS = [VID_VAR, PID_VAR, BUS_VAR, DEV_VAR, SERIAL_NUM_VAR]
26# -- For serial devices.
27SERIAL_PORT_VAR = "${SERIAL_PORT}"
28SERIAL_VARS = [SERIAL_PORT_VAR]
30# -- The ${BIN_FILE} placed holder is replaced here with $SOURCE and later
31# -- in scons with the bitstream file path. It can appear in both USB and
32# -- serial devices.
33BIN_FILE_VAR = "${BIN_FILE}"
34BIN_FILE_VALUE = "$SOURCE"
36# -- All possible vars.
37ALL_VARS = USB_VARS + SERIAL_VARS + [BIN_FILE_VAR]
40class _DeviceScanner:
41 """Provides usb and serial devices scanning, with caching."""
43 def __init__(self, apio_ctx: ApioContext):
44 self._apio_ctx: ApioContext = apio_ctx
45 self._usb_devices: list[UsbDevice] | None = None
46 self._serial_devices: list[SerialDevice] | None = None
48 def get_usb_devices(self) -> list[UsbDevice]:
49 """Scan usb devices, with caching."""
50 if self._usb_devices is None:
51 self._usb_devices = usb_util.scan_usb_devices(self._apio_ctx)
52 assert isinstance(self._usb_devices, list)
53 return self._usb_devices
55 def get_serial_devices(self) -> list[SerialDevice]:
56 """Scan serial devices, with caching."""
57 if self._serial_devices is None:
58 self._serial_devices = serial_util.scan_serial_devices(
59 self._apio_ctx
60 )
61 assert isinstance(self._serial_devices, list)
62 return self._serial_devices
65def construct_programmer_cmd(
66 apio_ctx: ApioContext,
67 serial_port_flag: str | None,
68 serial_num_flag: str | None,
69) -> str:
70 """Construct the programmer command for an 'apio upload' command."""
72 # -- This is a thin wrapper to allow injecting test scanners in tests.
73 scanner = _DeviceScanner(apio_ctx)
74 return _construct_programmer_cmd(
75 apio_ctx, scanner, serial_port_flag, serial_num_flag
76 )
79def _construct_programmer_cmd(
80 apio_ctx: ApioContext,
81 scanner: _DeviceScanner,
82 serial_port_flag: str | None,
83 serial_num_flag: str | None,
84) -> str:
85 """Construct the programmer command for an 'apio upload' command."""
87 # -- Construct the programmer cmd template for the board. It may or may not
88 # -- contain ${} vars.
89 cmd_template = _construct_cmd_template(apio_ctx)
90 if is_debug(1): 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 cout(f"Cmd template: [{cmd_template}]")
93 # -- Resolved the mandatory ${BIN_FILE} to $SOURCE which will be replaced
94 # -- by scons with the path of the bitstream file.
95 cmd_template = cmd_template.replace(BIN_FILE_VAR, BIN_FILE_VALUE)
97 # -- Determine how to resolve this template.
98 has_usb_vars = any(s in cmd_template for s in USB_VARS)
99 has_serial_vars = any(s in cmd_template for s in SERIAL_VARS)
101 if is_debug(1): 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 cout(f"Template has usb vars: {has_usb_vars}]")
103 cout(f"Template has serial vars: {has_serial_vars}]")
105 # -- Can't have both serial and usb vars (OK to have none).
106 if has_usb_vars and has_serial_vars: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 board = apio_ctx.project.get_str_option("board")
108 fatal_error(
109 f"The programmer cmd template of the board '{board}' has "
110 + "both usb and serial ${} vars. ",
111 info=f"Cmd template: {cmd_template}",
112 )
114 # -- Dispatch to the appropriate template resolver.
115 if has_serial_vars:
116 cmd = _resolve_serial_cmd_template(
117 apio_ctx, scanner, serial_port_flag, serial_num_flag, cmd_template
118 )
120 elif has_usb_vars:
121 _report_unused_flag("--serial-port", serial_port_flag)
122 cmd = _resolve_usb_cmd_template(
123 apio_ctx, scanner, serial_num_flag, cmd_template
124 )
126 else:
127 # -- If there are no vars to resolve, we don't need to match to a
128 # -- specific usb or serial device but just to check that if the board
129 # -- has 'usb' section, there is at least one device that matchs the
130 # -- constraints in that section.
131 _report_unused_flag("--serial-port", serial_port_flag)
132 _report_unused_flag("--serial-num", serial_num_flag)
133 _check_device_presence(apio_ctx, scanner)
135 # -- Template has no vars, we just use it as is.
136 cmd = cmd_template
138 # -- At this point, all vars should be resolved.
139 assert not any(s in cmd for s in ALL_VARS), cmd_template
141 # -- Return the resolved command.
142 return cmd
145def _report_unused_flag(flag_name: str, flag_value: str | None):
146 """If flag_value is not falsy then print a warning message."""
147 if flag_value: 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 cwarning(f"{flag_name} ignored.")
151def _construct_cmd_template(apio_ctx: ApioContext) -> str:
152 """Construct a command template for the board.
153 Example:
154 "openFPGAloader --verify -b ice40_generic --vid ${VID} --pid ${PID}
155 --busdev-num ${BUS}:${DEV} ${BIN_FILE}"
156 """
158 # -- If the project file has a custom programmer command use it instead
159 # -- of the standard definitions.
160 custom_template = apio_ctx.project.get_str_option("programmer-cmd")
161 if custom_template:
162 cout("Using custom programmer cmd.")
163 if BIN_FILE_VALUE in custom_template: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 fatal_error(
165 f"Custom programmer-cmd should not contain '{BIN_FILE_VALUE}'."
166 )
167 return custom_template
169 pr = apio_ctx.project_resources
170 board_definition = pr.board_definition
171 programmer_definition = pr.programmer_definition
173 # -- Here when using the standard command.
175 # -- Start building the template with the programmer binary name.
176 # -- E.g. "openFPGAloader". "command" is a validated required field.
177 proto_util.check_is_required(programmer_definition, "command")
178 cmd_template = programmer_definition.command
180 # -- Append the optional args template from the programmer. The 'args'
181 # -- field is required but may be empty.
182 proto_util.check_is_required(programmer_definition, "args")
183 args = programmer_definition.args
184 if args: 184 ↛ 189line 184 didn't jump to line 189 because the condition on line 184 was always true
185 cmd_template += " "
186 cmd_template += args
188 # -- Append the optional extra args template from the board.
189 proto_util.check_not_required(board_definition, "programmer.extra_args")
190 extra_args = board_definition.programmer.extra_args
191 if extra_args: 191 ↛ 197line 191 didn't jump to line 197 because the condition on line 191 was always true
192 cmd_template += " "
193 cmd_template += extra_args
195 # -- Append the bitstream file placeholder if its' not already in the
196 # -- template.
197 if BIN_FILE_VAR not in cmd_template: 197 ↛ 202line 197 didn't jump to line 202 because the condition on line 197 was always true
198 cmd_template += " "
199 cmd_template += BIN_FILE_VAR
201 # -- All done.
202 return cmd_template
205def _resolve_serial_cmd_template(
206 apio_ctx: ApioContext,
207 scanner: _DeviceScanner,
208 serial_port_arg: str | None,
209 serial_port_num: str | None,
210 cmd_template: str,
211) -> str:
212 """Resolves a programmer command template for a serial device."""
214 # -- Match to a single serial device.
215 device: SerialDevice = _match_serial_device(
216 apio_ctx, scanner, serial_port_arg, serial_port_num
217 )
219 # -- Resolve serial port var.
220 cmd_template = cmd_template.replace(SERIAL_PORT_VAR, device.port)
222 # -- Sanity check, should have no serial vars unresolved.
223 assert not any(s in cmd_template for s in SERIAL_VARS), cmd_template
225 # -- All done.
226 return cmd_template
229def _resolve_usb_cmd_template(
230 apio_ctx: ApioContext,
231 scanner: _DeviceScanner,
232 serial_num_flag: str | None,
233 cmd_template: str,
234) -> str:
235 """Resolves a programmer command template for an USB device."""
237 # -- Match to a single usb device.
238 device: UsbDevice = _match_usb_device(apio_ctx, scanner, serial_num_flag)
240 # -- Substitute vars.
241 cmd_template = cmd_template.replace(VID_VAR, device.vid)
242 cmd_template = cmd_template.replace(PID_VAR, device.pid)
243 cmd_template = cmd_template.replace(BUS_VAR, str(device.bus))
244 cmd_template = cmd_template.replace(DEV_VAR, str(device.device))
245 cmd_template = cmd_template.replace(SERIAL_NUM_VAR, device.serial_number)
247 # -- Sanity check, should have no usb vars unresolved.
248 assert not any(s in cmd_template for s in USB_VARS), cmd_template
250 # -- All done.
251 return cmd_template
254def _match_serial_device(
255 apio_ctx: ApioContext,
256 scanner: _DeviceScanner,
257 serial_port_flag: str | None,
258 serial_num_flag: str | None,
259) -> SerialDevice:
260 """Scans the serial devices and selects and returns a single matching
261 device. Exits with an error if none or multiple matching devices.
262 """
264 # -- Get project resources
265 pr = apio_ctx.project_resources
266 board_definition = pr.board_definition
268 # -- Scan for all serial devices.
269 all_devices: list[SerialDevice] = scanner.get_serial_devices()
271 # -- Get board optional usb constraints
272 proto_util.check_not_required(board_definition, "usb")
273 usb_info = (
274 board_definition.usb if board_definition.HasField("usb") else None
275 )
277 # -- Construct a device filter.
278 serial_filter = SerialDeviceFilter()
279 if usb_info: 279 ↛ 288line 279 didn't jump to line 288 because the condition on line 279 was always true
280 proto_util.check_not_required(usb_info, "vid", "pid", "product_regex")
281 if usb_info.vid: 281 ↛ 283line 281 didn't jump to line 283 because the condition on line 281 was always true
282 serial_filter.set_vid(usb_info.vid.upper())
283 if usb_info.pid: 283 ↛ 285line 283 didn't jump to line 285 because the condition on line 283 was always true
284 serial_filter.set_pid(usb_info.pid.upper())
285 if usb_info.product_regex: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 serial_filter.set_product_regex(usb_info.product_regex)
288 if serial_port_flag: 288 ↛ 289line 288 didn't jump to line 289 because the condition on line 288 was never true
289 serial_filter.set_port(serial_port_flag)
290 if serial_num_flag: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 serial_filter.set_serial_num(serial_num_flag)
293 # -- Inform the user.
294 cout("Scanning for a serial device:")
295 cout(f"- FILTER {serial_filter.summary()}")
297 # -- Get matching devices
298 matching: list[SerialDevice] = serial_filter.filter(all_devices)
300 for dev in matching:
301 cout(f"- DEVICE {dev.summary()}")
303 if is_debug(1): 303 ↛ 304line 303 didn't jump to line 304 because the condition on line 303 was never true
304 cout(f"Serial device filter: {serial_filter.summary()}")
305 cout(f"Matching serial devices: {matching}")
307 if is_debug(1): 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true
308 cout(f"Matching serial devices: {matching}")
310 # -- Error if not exactly one match.
311 if not matching:
312 fatal_error(
313 "No matching serial device.",
314 info="Type 'apio devices scan-serial' for available "
315 + "serial devices.",
316 )
318 # -- Error more than one match
319 if len(matching) > 1:
320 fatal_error(
321 "Found multiple matching serial devices.",
322 info="Type 'apio devices scan-serial' for available "
323 + "serial devices.",
324 )
326 # -- All done. We have a single match.
327 return matching[0]
330def _match_usb_device(
331 apio_ctx: ApioContext, scanner, serial_num_flag: str | None
332) -> UsbDevice:
333 """Scans the USB devices and selects and returns a single matching
334 device. Exits with an error if none or multiple matching devices.
335 """
337 # -- Get project resources.
338 pr = apio_ctx.project_resources
339 board_definition = pr.board_definition
341 # -- Scan for all serial devices.
342 all_devices: list[UsbDevice] = scanner.get_usb_devices()
344 # -- Get board optional usb constraints
345 proto_util.check_not_required(board_definition, "usb")
346 usb_info = (
347 pr.board_definition.usb
348 if pr.board_definition.HasField("usb")
349 else None
350 )
352 # -- Construct a device filter.
353 usb_filter = UsbDeviceFilter()
354 if usb_info: 354 ↛ 363line 354 didn't jump to line 363 because the condition on line 354 was always true
355 proto_util.check_not_required(usb_info, "vid", "pid", "product_regex")
356 if usb_info.vid: 356 ↛ 358line 356 didn't jump to line 358 because the condition on line 356 was always true
357 usb_filter.set_vid(usb_info.vid.upper())
358 if usb_info.pid: 358 ↛ 360line 358 didn't jump to line 360 because the condition on line 358 was always true
359 usb_filter.set_pid(usb_info.pid.upper())
360 if usb_info.product_regex: 360 ↛ 363line 360 didn't jump to line 363 because the condition on line 360 was always true
361 usb_filter.set_product_regex(usb_info.product_regex)
363 if serial_num_flag: 363 ↛ 364line 363 didn't jump to line 364 because the condition on line 363 was never true
364 usb_filter.set_serial_num(serial_num_flag)
366 # -- Inform the user.
367 cout("Scanning for a USB device:")
368 cout(f"- FILTER {usb_filter.summary()}")
370 # -- Get matching devices
371 matching: list[UsbDevice] = usb_filter.filter(all_devices)
373 for dev in matching:
374 cout(f"- DEVICE {dev.summary()}")
376 if is_debug(1): 376 ↛ 377line 376 didn't jump to line 377 because the condition on line 376 was never true
377 cout(f"USB device filter: {usb_filter.summary()}")
378 cout(f"Matching USB devices: {matching}")
380 if is_debug(1): 380 ↛ 381line 380 didn't jump to line 381 because the condition on line 380 was never true
381 cout(f"Matching usb devices: {matching}")
383 # -- Error if not exactly one match.
384 if not matching:
385 fatal_error(
386 "No matching USB device.",
387 info="Type 'apio devices scan-usb' for available usb devices.",
388 )
390 # -- Error more than one match
391 if len(matching) > 1:
392 fatal_error(
393 "Found multiple matching usb devices.",
394 info="Type 'apio devices scan-usb' for available usb device.",
395 )
397 # -- All done. We have a single match.
398 return matching[0]
401def _check_device_presence(apio_ctx: ApioContext, scanner: _DeviceScanner):
402 """If the board info has a "usb" section, check that there is at least one
403 usb device that matches the constraints, if any, in the "usb" section.
404 Returns if OK, exits with an error otherwise.
405 """
407 # -- Get project resources.
408 pr = apio_ctx.project_resources
409 board_definition = pr.board_definition
411 # -- If board has no usb constrains than nothing to do.
412 proto_util.check_not_required(board_definition, "usb")
413 if not board_definition.HasField("usb"): 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true
414 return
416 # -- Get board optional usb constraints
417 usb_info = (
418 board_definition.usb if board_definition.HasField("usb") else None
419 )
421 # -- Create a device filter with the constraints. Note that the "usb"
422 # -- section may contain no constrained which will result in a pass-all
423 # -- filter.
424 usb_filter = UsbDeviceFilter()
425 if usb_info: 425 ↛ 434line 425 didn't jump to line 434 because the condition on line 425 was always true
426 proto_util.check_not_required(usb_info, "vid", "pid", "product_regex")
427 if usb_info.vid: 427 ↛ 429line 427 didn't jump to line 429 because the condition on line 427 was always true
428 usb_filter.set_vid(usb_info.vid.upper())
429 if usb_info.pid: 429 ↛ 431line 429 didn't jump to line 431 because the condition on line 429 was always true
430 usb_filter.set_pid(usb_info.pid.upper())
431 if usb_info.product_regex: 431 ↛ 434line 431 didn't jump to line 434 because the condition on line 431 was always true
432 usb_filter.set_product_regex(usb_info.product_regex)
434 cout("Checking device presence...")
435 cout(f"- FILTER {usb_filter.summary()}")
437 # -- Scan the USB devices and filter by the filter.
438 all_devices = scanner.get_usb_devices()
439 matching_devices = usb_filter.filter(all_devices)
441 for device in matching_devices:
442 cout(f"- DEVICE {device.summary()}")
444 # -- If no device passed the filter fail the check.
445 if not matching_devices:
446 fatal_error(
447 "No matching device.",
448 info="Type 'apio devices scan-usb' for available usb devices.",
449 )
451 # -- All OK.