Coverage for apio / managers / programmers.py: 77%
190 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-10 03:35 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-10 03:35 +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
9import sys
10from typing import Optional, List, Dict
11from apio.common.apio_console import cout, cerror, cwarning
12from apio.common.apio_styles import INFO
13from apio.utils import util, serial_util, usb_util
14from apio.utils.serial_util import SerialDevice, SerialDeviceFilter
15from apio.utils.usb_util import UsbDevice, UsbDeviceFilter
16from apio.apio_context import ApioContext
18# -- For USB devices
19VID_VAR = "${VID}"
20PID_VAR = "${PID}"
21BUS_VAR = "${BUS}"
22DEV_VAR = "${DEV}"
23SERIAL_NUM_VAR = "${SERIAL_NUM}"
25USB_VARS = [VID_VAR, PID_VAR, BUS_VAR, DEV_VAR, SERIAL_NUM_VAR]
27# -- For serial devices.
28SERIAL_PORT_VAR = "${SERIAL_PORT}"
29SERIAL_VARS = [SERIAL_PORT_VAR]
31# -- The ${BIN_FILE} placed holder is replaced here with $SOURCE and later
32# -- in scons with the bitstream file path. It can appear in both USB and
33# -- serial devices.
34BIN_FILE_VAR = "${BIN_FILE}"
35BIN_FILE_VALUE = "$SOURCE"
37# -- All possible vars.
38ALL_VARS = USB_VARS + SERIAL_VARS + [BIN_FILE_VAR]
41class _DeviceScanner:
42 """Provides usb and serial devices scanning, with caching."""
44 def __init__(self, apio_ctx: ApioContext):
45 self._apio_ctx: ApioContext = apio_ctx
46 self._usb_devices: List[UsbDevice] = None
47 self._serial_devices: List[SerialDevice] = None
49 def get_usb_devices(self) -> List[UsbDevice]:
50 """Scan usb devices, with caching."""
51 if self._usb_devices is None:
52 self._usb_devices = usb_util.scan_usb_devices(self._apio_ctx)
53 assert isinstance(self._usb_devices, list)
54 return self._usb_devices
56 def get_serial_devices(self) -> List[UsbDevice]:
57 """Scan serial devices, with caching."""
58 if self._serial_devices is None:
59 self._serial_devices = serial_util.scan_serial_devices(
60 self._apio_ctx
61 )
62 assert isinstance(self._serial_devices, list)
63 return self._serial_devices
66def construct_programmer_cmd(
67 apio_ctx: ApioContext,
68 serial_port_flag: Optional[str],
69 serial_num_flag: Optional[str],
70) -> str:
71 """Construct the programmer command for an 'apio upload' command."""
73 # -- This is a thin wrapper to allow injecting test scanners in tests.
74 scanner = _DeviceScanner(apio_ctx)
75 return _construct_programmer_cmd(
76 apio_ctx, scanner, serial_port_flag, serial_num_flag
77 )
80def _construct_programmer_cmd(
81 apio_ctx: ApioContext,
82 scanner: _DeviceScanner,
83 serial_port_flag: Optional[str],
84 serial_num_flag: Optional[str],
85) -> str:
86 """Construct the programmer command for an 'apio upload' command."""
88 # -- Construct the programmer cmd template for the board. It may or may not
89 # -- contain ${} vars.
90 cmd_template = _construct_cmd_template(apio_ctx)
91 if util.is_debug(1): 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true
92 cout(f"Cmd template: [{cmd_template}]")
94 # -- Resolved the mandatory ${BIN_FILE} to $SOURCE which will be replaced
95 # -- by scons with the path of the bitstream file.
96 cmd_template = cmd_template.replace(BIN_FILE_VAR, BIN_FILE_VALUE)
98 # -- Determine how to resolve this template.
99 has_usb_vars = any(s in cmd_template for s in USB_VARS)
100 has_serial_vars = any(s in cmd_template for s in SERIAL_VARS)
102 if util.is_debug(1): 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 cout(f"Template has usb vars: {has_usb_vars}]")
104 cout(f"Template has serial vars: {has_serial_vars}]")
106 # -- Can't have both serial and usb vars (OK to have none).
107 if has_usb_vars and has_serial_vars: 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 board = apio_ctx.project.get_str_option("board")
109 cerror(
110 f"The programmer cmd template of the board '{board}' has "
111 "both usb and serial ${} vars. "
112 )
113 cout(f"Cmd template: {cmd_template}", style=INFO)
114 sys.exit(1)
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):
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 cerror(
167 f"Custom programmer-cmd should not contain '{BIN_FILE_VALUE}'."
168 )
169 sys.exit(1)
170 return custom_template
172 pr = apio_ctx.project_resources
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 cmd_template = pr.programmer_info["command"]
179 # -- Append the optional args template from the programmer.
180 args = pr.programmer_info.get("args", "")
181 if args: 181 ↛ 186line 181 didn't jump to line 186 because the condition on line 181 was always true
182 cmd_template += " "
183 cmd_template += args
185 # -- Append the optional extra args template from the board.
186 extra_args = pr.board_info["programmer"].get("extra-args", "")
187 if extra_args: 187 ↛ 193line 187 didn't jump to line 193 because the condition on line 187 was always true
188 cmd_template += " "
189 cmd_template += extra_args
191 # -- Append the bitstream file placeholder if its' not already in the
192 # -- template.
193 if BIN_FILE_VAR not in cmd_template: 193 ↛ 198line 193 didn't jump to line 198 because the condition on line 193 was always true
194 cmd_template += " "
195 cmd_template += BIN_FILE_VAR
197 # -- All done.
198 return cmd_template
201def _resolve_serial_cmd_template(
202 apio_ctx: ApioContext,
203 scanner: _DeviceScanner,
204 serial_port_arg: Optional[str],
205 serial_port_num: Optional[str],
206 cmd_template: str,
207) -> str:
208 """Resolves a programmer command template for a serial device."""
210 # -- Match to a single serial device.
211 device: SerialDevice = _match_serial_device(
212 apio_ctx, scanner, serial_port_arg, serial_port_num
213 )
215 # -- Resolve serial port var.
216 cmd_template = cmd_template.replace(SERIAL_PORT_VAR, device.port)
218 # -- Sanity check, should have no serial vars unresolved.
219 assert not any(s in cmd_template for s in SERIAL_VARS), cmd_template
221 # -- All done.
222 return cmd_template
225def _resolve_usb_cmd_template(
226 apio_ctx: ApioContext,
227 scanner: _DeviceScanner,
228 serial_num_flag: Optional[str],
229 cmd_template: str,
230) -> str:
231 """Resolves a programmer command template for an USB device."""
233 # -- Match to a single usb device.
234 device: UsbDevice = _match_usb_device(apio_ctx, scanner, serial_num_flag)
236 # -- Substitute vars.
237 cmd_template = cmd_template.replace(VID_VAR, device.vendor_id)
238 cmd_template = cmd_template.replace(PID_VAR, device.product_id)
239 cmd_template = cmd_template.replace(BUS_VAR, str(device.bus))
240 cmd_template = cmd_template.replace(DEV_VAR, str(device.device))
241 cmd_template = cmd_template.replace(SERIAL_NUM_VAR, device.serial_number)
243 # -- Sanity check, should have no usb vars unresolved.
244 assert not any(s in cmd_template for s in USB_VARS), cmd_template
246 # -- All done.
247 return cmd_template
250def _match_serial_device(
251 apio_ctx: ApioContext,
252 scanner: _DeviceScanner,
253 serial_port_flag: Optional[str],
254 serial_num_flag: Optional[str],
255) -> SerialDevice:
256 """Scans the serial devices and selects and returns a single matching
257 device. Exits with an error if none or multiple matching devices.
258 """
260 # -- Get project resources
261 pr = apio_ctx.project_resources
263 # -- Scan for all serial devices.
264 all_devices: List[SerialDevice] = scanner.get_serial_devices()
266 # -- Get board optional usb constraints
267 usb_info = pr.board_info.get("usb", {})
269 # -- Construct a device filter.
270 serial_filter = SerialDeviceFilter()
271 if "vid" in usb_info: 271 ↛ 273line 271 didn't jump to line 273 because the condition on line 271 was always true
272 serial_filter.set_vendor_id(usb_info["vid"].upper())
273 if "pid" in usb_info: 273 ↛ 275line 273 didn't jump to line 275 because the condition on line 273 was always true
274 serial_filter.set_product_id(usb_info["pid"].upper())
275 if "product-regex" in usb_info: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 serial_filter.set_product_regex(usb_info["product-regex"])
277 if serial_port_flag: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 serial_filter.set_port(serial_port_flag)
279 if serial_num_flag: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 serial_filter.set_serial_num(serial_num_flag)
282 # -- Inform the user.
283 cout("Scanning for a serial device:")
284 cout(f"- FILTER {serial_filter.summary()}")
286 # -- Get matching devices
287 matching: List[SerialDevice] = serial_filter.filter(all_devices)
289 for dev in matching:
290 cout(f"- DEVICE {dev.summary()}")
292 if util.is_debug(1): 292 ↛ 293line 292 didn't jump to line 293 because the condition on line 292 was never true
293 cout(f"Serial device filter: {serial_filter.summary()}")
294 cout(f"Matching serial devices: {matching}")
296 if util.is_debug(1): 296 ↛ 297line 296 didn't jump to line 297 because the condition on line 296 was never true
297 cout(f"Matching serial devices: {matching}")
299 # -- Error if not exactly one match.
300 if not matching:
301 cerror("No matching serial device.")
302 cout(
303 "Type 'apio devices scan-serial' for available serial devices.",
304 style=INFO,
305 )
306 sys.exit(1)
308 # -- Error more than one match
309 if len(matching) > 1:
310 cerror("Found multiple matching serial devices.")
311 cout(
312 "Type 'apio devices scan-serial' for available serial devices.",
313 style=INFO,
314 )
315 sys.exit(1)
317 # -- All done. We have a single match.
318 return matching[0]
321def _match_usb_device(
322 apio_ctx: ApioContext, scanner, serial_num_flag: Optional[str]
323) -> UsbDevice:
324 """Scans the USB devices and selects and returns a single matching
325 device. Exits with an error if none or multiple matching devices.
326 """
328 # -- Get project resources.
329 pr = apio_ctx.project_resources
331 # -- Scan for all serial devices.
332 all_devices: List[UsbDevice] = scanner.get_usb_devices()
334 # -- Get board optional usb constraints
335 usb_info = pr.board_info.get("usb", {})
337 # -- Construct a device filter.
338 usb_filter = UsbDeviceFilter()
339 if "vid" in usb_info: 339 ↛ 341line 339 didn't jump to line 341 because the condition on line 339 was always true
340 usb_filter.set_vendor_id(usb_info["vid"].upper())
341 if "pid" in usb_info: 341 ↛ 343line 341 didn't jump to line 343 because the condition on line 341 was always true
342 usb_filter.set_product_id(usb_info["pid"].upper())
343 if "product-regex" in usb_info: 343 ↛ 345line 343 didn't jump to line 345 because the condition on line 343 was always true
344 usb_filter.set_product_regex(usb_info["product-regex"])
345 if serial_num_flag: 345 ↛ 346line 345 didn't jump to line 346 because the condition on line 345 was never true
346 usb_filter.set_serial_num(serial_num_flag)
348 # -- Inform the user.
349 cout("Scanning for a USB device:")
350 cout(f"- FILTER {usb_filter.summary()}")
352 # -- Get matching devices
353 matching: List[UsbDevice] = usb_filter.filter(all_devices)
355 for dev in matching:
356 cout(f"- DEVICE {dev.summary()}")
358 if util.is_debug(1): 358 ↛ 359line 358 didn't jump to line 359 because the condition on line 358 was never true
359 cout(f"USB device filter: {usb_filter.summary()}")
360 cout(f"Matching USB devices: {matching}")
362 if util.is_debug(1): 362 ↛ 363line 362 didn't jump to line 363 because the condition on line 362 was never true
363 cout(f"Matching usb devices: {matching}")
365 # -- Error if not exactly one match.
366 if not matching:
367 cerror("No matching USB device.")
368 cout(
369 "Type 'apio devices scan-usb' for available usb devices.",
370 style=INFO,
371 )
372 sys.exit(1)
374 # -- Error more than one match
375 if len(matching) > 1:
376 cerror("Found multiple matching usb devices.")
377 cout(
378 "Type 'apio devices scan-usb' for available usb device.",
379 style=INFO,
380 )
381 sys.exit(1)
383 # -- All done. We have a single match.
384 return matching[0]
387def _check_device_presence(apio_ctx: ApioContext, scanner: _DeviceScanner):
388 """If the board info has a "usb" section, check that there is at least one
389 usb device that matches the constraints, if any, in the "usb" section.
390 Returns if OK, exits with an error otherwise.
391 """
393 # -- Get project resources.
394 pr = apio_ctx.project_resources
396 # -- Get the optional "usb" section of the board.
397 usb_info: Dict[str, str] = pr.board_info.get("usb", None)
399 # -- If no "usb" section there are no constrained to check. We don't
400 # -- even check that any usb device exists.
401 if usb_info is None: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true
402 return
404 # -- Create a device filter with the constraints. Note that the "usb"
405 # -- section may contain no constrained which will result in a pass-all
406 # -- filter.
407 usb_filter = UsbDeviceFilter()
408 if "vid" in usb_info: 408 ↛ 410line 408 didn't jump to line 410 because the condition on line 408 was always true
409 usb_filter.set_vendor_id(usb_info["vid"].upper())
410 if "pid" in usb_info: 410 ↛ 412line 410 didn't jump to line 412 because the condition on line 410 was always true
411 usb_filter.set_product_id(usb_info["pid"].upper())
412 if "product-regex" in usb_info: 412 ↛ 415line 412 didn't jump to line 415 because the condition on line 412 was always true
413 usb_filter.set_product_regex(usb_info["product-regex"])
415 cout("Checking device presence...")
416 cout(f"- FILTER {usb_filter.summary()}")
418 # -- Scan the USB devices and filter by the filter.
419 all_devices = scanner.get_usb_devices()
420 matching_devices = usb_filter.filter(all_devices)
422 for device in matching_devices:
423 cout(f"- DEVICE {device.summary()}")
425 # -- If no device passed the filter fail the check.
426 if not matching_devices:
427 cerror("No matching device.")
428 cout(
429 "Type 'apio devices scan-usb' for available usb devices.",
430 style=INFO,
431 )
432 sys.exit(1)
434 # -- All OK.