Coverage for apio/managers/programmers.py: 77%
190 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +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 assert isinstance(self._serial_devices, list)
61 return self._serial_devices
64def construct_programmer_cmd(
65 apio_ctx: ApioContext,
66 serial_port_flag: Optional[str],
67 serial_num_flag: Optional[str],
68) -> str:
69 """Construct the programmer command for an 'apio upload' command."""
71 # -- This is a thin wrapper to allow injecting test scanners in tests.
72 scanner = _DeviceScanner(apio_ctx)
73 return _construct_programmer_cmd(
74 apio_ctx, scanner, serial_port_flag, serial_num_flag
75 )
78def _construct_programmer_cmd(
79 apio_ctx: ApioContext,
80 scanner: _DeviceScanner,
81 serial_port_flag: Optional[str],
82 serial_num_flag: Optional[str],
83) -> str:
84 """Construct the programmer command for an 'apio upload' command."""
86 # -- Construct the programmer cmd template for the board. It may or may not
87 # -- contain ${} vars.
88 cmd_template = _construct_cmd_template(apio_ctx)
89 if util.is_debug(1): 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true
90 cout(f"Cmd template: [{cmd_template}]")
92 # -- Resolved the mandatory ${BIN_FILE} to $SOURCE which will be replaced
93 # -- by scons with the path of the bitstream file.
94 cmd_template = cmd_template.replace(BIN_FILE_VAR, BIN_FILE_VALUE)
96 # -- Determine how to resolve this template.
97 has_usb_vars = any(s in cmd_template for s in USB_VARS)
98 has_serial_vars = any(s in cmd_template for s in SERIAL_VARS)
100 if util.is_debug(1): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 cout(f"Template has usb vars: {has_usb_vars}]")
102 cout(f"Template has serial vars: {has_serial_vars}]")
104 # -- Can't have both serial and usb vars (OK to have none).
105 if has_usb_vars and has_serial_vars: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 board = apio_ctx.project.get_str_option("board")
107 cerror(
108 f"The programmer cmd template of the board '{board}' has "
109 "both usb and serial ${} vars. "
110 )
111 cout(f"Cmd template: {cmd_template}", style=INFO)
112 sys.exit(1)
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):
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 cerror(
165 f"Custom programmer-cmd should not contain '{BIN_FILE_VALUE}'."
166 )
167 sys.exit(1)
168 return custom_template
170 pr = apio_ctx.project_resources
171 # -- Here when using the standard command.
173 # -- Start building the template with the programmer binary name.
174 # -- E.g. "openFPGAloader". "command" is a validated required field.
175 cmd_template = pr.programmer_info["command"]
177 # -- Append the optional args template from the programmer.
178 args = pr.programmer_info.get("args", "")
179 if args: 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true
180 cmd_template += " "
181 cmd_template += args
183 # -- Append the optional extra args template from the board.
184 extra_args = pr.board_info["programmer"].get("extra-args", "")
185 if extra_args: 185 ↛ 191line 185 didn't jump to line 191 because the condition on line 185 was always true
186 cmd_template += " "
187 cmd_template += extra_args
189 # -- Append the bitstream file placeholder if its' not already in the
190 # -- template.
191 if BIN_FILE_VAR not in cmd_template: 191 ↛ 196line 191 didn't jump to line 196 because the condition on line 191 was always true
192 cmd_template += " "
193 cmd_template += BIN_FILE_VAR
195 # -- All done.
196 return cmd_template
199def _resolve_serial_cmd_template(
200 apio_ctx: ApioContext,
201 scanner: _DeviceScanner,
202 serial_port_arg: Optional[str],
203 serial_port_num: Optional[str],
204 cmd_template: str,
205) -> str:
206 """Resolves a programmer command template for a serial device."""
208 # -- Match to a single serial device.
209 device: SerialDevice = _match_serial_device(
210 apio_ctx, scanner, serial_port_arg, serial_port_num
211 )
213 # -- Resolve serial port var.
214 cmd_template = cmd_template.replace(SERIAL_PORT_VAR, device.port)
216 # -- Sanity check, should have no serial vars unresolved.
217 assert not any(s in cmd_template for s in SERIAL_VARS), cmd_template
219 # -- All done.
220 return cmd_template
223def _resolve_usb_cmd_template(
224 apio_ctx: ApioContext,
225 scanner: _DeviceScanner,
226 serial_num_flag: Optional[str],
227 cmd_template: str,
228) -> str:
229 """Resolves a programmer command template for an USB device."""
231 # -- Match to a single usb device.
232 device: UsbDevice = _match_usb_device(apio_ctx, scanner, serial_num_flag)
234 # -- Substitute vars.
235 cmd_template = cmd_template.replace(VID_VAR, device.vendor_id)
236 cmd_template = cmd_template.replace(PID_VAR, device.product_id)
237 cmd_template = cmd_template.replace(BUS_VAR, str(device.bus))
238 cmd_template = cmd_template.replace(DEV_VAR, str(device.device))
239 cmd_template = cmd_template.replace(SERIAL_NUM_VAR, device.serial_number)
241 # -- Sanity check, should have no usb vars unresolved.
242 assert not any(s in cmd_template for s in USB_VARS), cmd_template
244 # -- All done.
245 return cmd_template
248def _match_serial_device(
249 apio_ctx: ApioContext,
250 scanner: _DeviceScanner,
251 serial_port_flag: Optional[str],
252 serial_num_flag: Optional[str],
253) -> SerialDevice:
254 """Scans the serial devices and selects and returns a single matching
255 device. Exits with an error if none or multiple matching devices.
256 """
258 # -- Get project resources
259 pr = apio_ctx.project_resources
261 # -- Scan for all serial devices.
262 all_devices: List[SerialDevice] = scanner.get_serial_devices()
264 # -- Get board optional usb constraints
265 usb_info = pr.board_info.get("usb", {})
267 # -- Construct a device filter.
268 serial_filter = SerialDeviceFilter()
269 if "vid" in usb_info: 269 ↛ 271line 269 didn't jump to line 271 because the condition on line 269 was always true
270 serial_filter.set_vendor_id(usb_info["vid"].upper())
271 if "pid" 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_product_id(usb_info["pid"].upper())
273 if "product-regex" in usb_info: 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 serial_filter.set_product_regex(usb_info["product-regex"])
275 if serial_port_flag: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 serial_filter.set_port(serial_port_flag)
277 if serial_num_flag: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 serial_filter.set_serial_num(serial_num_flag)
280 # -- Inform the user.
281 cout("Scanning for a serial device:")
282 cout(f"- FILTER {serial_filter.summary()}")
284 # -- Get matching devices
285 matching: List[SerialDevice] = serial_filter.filter(all_devices)
287 for dev in matching:
288 cout(f"- DEVICE {dev.summary()}")
290 if util.is_debug(1): 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 cout(f"Serial device filter: {serial_filter.summary()}")
292 cout(f"Matching serial devices: {matching}")
294 if util.is_debug(1): 294 ↛ 295line 294 didn't jump to line 295 because the condition on line 294 was never true
295 cout(f"Matching serial devices: {matching}")
297 # -- Error if not exactly one match.
298 if not matching:
299 cerror("No matching serial device.")
300 cout(
301 "Type 'apio devices serial' for available serial devices.",
302 style=INFO,
303 )
304 sys.exit(1)
306 # -- Error more than one match
307 if len(matching) > 1:
308 cerror("Found multiple matching serial devices.")
309 cout(
310 "Type 'apio devices serial' for available serial devices.",
311 style=INFO,
312 )
313 sys.exit(1)
315 # -- All done. We have a single match.
316 return matching[0]
319def _match_usb_device(
320 apio_ctx: ApioContext, scanner, serial_num_flag: Optional[str]
321) -> UsbDevice:
322 """Scans the USB devices and selects and returns a single matching
323 device. Exits with an error if none or multiple matching devices.
324 """
326 # -- Get project resources.
327 pr = apio_ctx.project_resources
329 # -- Scan for all serial devices.
330 all_devices: List[UsbDevice] = scanner.get_usb_devices()
332 # -- Get board optional usb constraints
333 usb_info = pr.board_info.get("usb", {})
335 # -- Construct a device filter.
336 usb_filter = UsbDeviceFilter()
337 if "vid" in usb_info: 337 ↛ 339line 337 didn't jump to line 339 because the condition on line 337 was always true
338 usb_filter.set_vendor_id(usb_info["vid"].upper())
339 if "pid" 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_product_id(usb_info["pid"].upper())
341 if "product-regex" 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_regex(usb_info["product-regex"])
343 if serial_num_flag: 343 ↛ 344line 343 didn't jump to line 344 because the condition on line 343 was never true
344 usb_filter.set_serial_num(serial_num_flag)
346 # -- Inform the user.
347 cout("Scanning for a USB device:")
348 cout(f"- FILTER {usb_filter.summary()}")
350 # -- Get matching devices
351 matching: List[UsbDevice] = usb_filter.filter(all_devices)
353 for dev in matching:
354 cout(f"- DEVICE {dev.summary()}")
356 if util.is_debug(1): 356 ↛ 357line 356 didn't jump to line 357 because the condition on line 356 was never true
357 cout(f"USB device filter: {usb_filter.summary()}")
358 cout(f"Matching USB devices: {matching}")
360 if util.is_debug(1): 360 ↛ 361line 360 didn't jump to line 361 because the condition on line 360 was never true
361 cout(f"Matching usb devices: {matching}")
363 # -- Error if not exactly one match.
364 if not matching:
365 cerror("No matching USB device.")
366 cout(
367 "Type 'apio devices usb' for available usb devices.",
368 style=INFO,
369 )
370 sys.exit(1)
372 # -- Error more than one match
373 if len(matching) > 1:
374 cerror("Found multiple matching usb devices.")
375 cout(
376 "Type 'apio devices usb' for available usb device.",
377 style=INFO,
378 )
379 sys.exit(1)
381 # -- All done. We have a single match.
382 return matching[0]
385def _check_device_presence(apio_ctx: ApioContext, scanner: _DeviceScanner):
386 """If the board info has a "usb" section, check that there is at least one
387 usb device that matches the constraints, if any, in the "usb" section.
388 Returns if OK, exits with an error otherwise.
389 """
391 # -- Get project resources.
392 pr = apio_ctx.project_resources
394 # -- Get the optional "usb" section of the board.
395 usb_info: Dict[str, str] = pr.board_info.get("usb", None)
397 # -- If no "usb" section there are no constrained to check. We don't
398 # -- even check that any usb device exists.
399 if usb_info is None: 399 ↛ 400line 399 didn't jump to line 400 because the condition on line 399 was never true
400 return
402 # -- Create a device filter with the constraints. Note that the "usb"
403 # -- section may contain no constrained which will result in a pass-all
404 # -- filter.
405 usb_filter = UsbDeviceFilter()
406 if "vid" in usb_info: 406 ↛ 408line 406 didn't jump to line 408 because the condition on line 406 was always true
407 usb_filter.set_vendor_id(usb_info["vid"].upper())
408 if "pid" 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_product_id(usb_info["pid"].upper())
410 if "product-regex" in usb_info: 410 ↛ 413line 410 didn't jump to line 413 because the condition on line 410 was always true
411 usb_filter.set_product_regex(usb_info["product-regex"])
413 cout("Checking device presence...")
414 cout(f"- FILTER {usb_filter.summary()}")
416 # -- Scan the USB devices and filter by the filter.
417 all_devices = scanner.get_usb_devices()
418 matching_devices = usb_filter.filter(all_devices)
420 for device in matching_devices:
421 cout(f"- DEVICE {device.summary()}")
423 # -- If no device passed the filter fail the check.
424 if not matching_devices:
425 cerror("No matching device.")
426 cout(
427 "Type 'apio devices usb' for available usb devices.",
428 style=INFO,
429 )
430 sys.exit(1)
432 # -- All OK.