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

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 

8 

9"""DOC: TODO""" 

10 

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 

18 

19# -- For USB devices 

20VID_VAR = "${VID}" 

21PID_VAR = "${PID}" 

22BUS_VAR = "${BUS}" 

23DEV_VAR = "${DEV}" 

24SERIAL_NUM_VAR = "${SERIAL_NUM}" 

25 

26USB_VARS = [VID_VAR, PID_VAR, BUS_VAR, DEV_VAR, SERIAL_NUM_VAR] 

27 

28# -- For serial devices. 

29SERIAL_PORT_VAR = "${SERIAL_PORT}" 

30SERIAL_VARS = [SERIAL_PORT_VAR] 

31 

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" 

37 

38# -- All possible vars. 

39ALL_VARS = USB_VARS + SERIAL_VARS + [BIN_FILE_VAR] 

40 

41 

42class _DeviceScanner: 

43 """Provides usb and serial devices scanning, with caching.""" 

44 

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 

49 

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 

56 

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 

65 

66 

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.""" 

73 

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 ) 

79 

80 

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.""" 

88 

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}]") 

94 

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) 

98 

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) 

102 

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}]") 

106 

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 ) 

115 

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 ) 

121 

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 ) 

127 

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) 

136 

137 # -- Template has no vars, we just use it as is. 

138 cmd = cmd_template 

139 

140 # -- At this point, all vars should be resolved. 

141 assert not any(s in cmd for s in ALL_VARS), cmd_template 

142 

143 # -- Return the resolved command. 

144 return cmd 

145 

146 

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.") 

151 

152 

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 """ 

159 

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 

170 

171 pr = apio_ctx.project_resources 

172 board_definition = pr.board_definition 

173 programmer_definition = pr.programmer_definition 

174 

175 # -- Here when using the standard command. 

176 

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 

181 

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 

189 

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 

196 

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 

202 

203 # -- All done. 

204 return cmd_template 

205 

206 

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.""" 

215 

216 # -- Match to a single serial device. 

217 device: SerialDevice = _match_serial_device( 

218 apio_ctx, scanner, serial_port_arg, serial_port_num 

219 ) 

220 

221 # -- Resolve serial port var. 

222 cmd_template = cmd_template.replace(SERIAL_PORT_VAR, device.port) 

223 

224 # -- Sanity check, should have no serial vars unresolved. 

225 assert not any(s in cmd_template for s in SERIAL_VARS), cmd_template 

226 

227 # -- All done. 

228 return cmd_template 

229 

230 

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.""" 

238 

239 # -- Match to a single usb device. 

240 device: UsbDevice = _match_usb_device(apio_ctx, scanner, serial_num_flag) 

241 

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) 

248 

249 # -- Sanity check, should have no usb vars unresolved. 

250 assert not any(s in cmd_template for s in USB_VARS), cmd_template 

251 

252 # -- All done. 

253 return cmd_template 

254 

255 

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 """ 

265 

266 # -- Get project resources 

267 pr = apio_ctx.project_resources 

268 board_definition = pr.board_definition 

269 

270 # -- Scan for all serial devices. 

271 all_devices: list[SerialDevice] = scanner.get_serial_devices() 

272 

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 ) 

278 

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) 

289 

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) 

294 

295 # -- Inform the user. 

296 cout("Scanning for a serial device:") 

297 cout(f"- FILTER {serial_filter.summary()}") 

298 

299 # -- Get matching devices 

300 matching: list[SerialDevice] = serial_filter.filter(all_devices) 

301 

302 for dev in matching: 

303 cout(f"- DEVICE {dev.summary()}") 

304 

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}") 

308 

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}") 

311 

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 ) 

319 

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 ) 

327 

328 # -- All done. We have a single match. 

329 return matching[0] 

330 

331 

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 """ 

338 

339 # -- Get project resources. 

340 pr = apio_ctx.project_resources 

341 board_definition = pr.board_definition 

342 

343 # -- Scan for all serial devices. 

344 all_devices: list[UsbDevice] = scanner.get_usb_devices() 

345 

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 ) 

353 

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) 

364 

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) 

367 

368 # -- Inform the user. 

369 cout("Scanning for a USB device:") 

370 cout(f"- FILTER {usb_filter.summary()}") 

371 

372 # -- Get matching devices 

373 matching: list[UsbDevice] = usb_filter.filter(all_devices) 

374 

375 for dev in matching: 

376 cout(f"- DEVICE {dev.summary()}") 

377 

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}") 

381 

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}") 

384 

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 ) 

391 

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 ) 

398 

399 # -- All done. We have a single match. 

400 return matching[0] 

401 

402 

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 """ 

408 

409 # -- Get project resources. 

410 pr = apio_ctx.project_resources 

411 board_definition = pr.board_definition 

412 

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 

417 

418 # -- Get board optional usb constraints 

419 usb_info = ( 

420 board_definition.usb if board_definition.HasField("usb") else None 

421 ) 

422 

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) 

435 

436 cout("Checking device presence...") 

437 cout(f"- FILTER {usb_filter.summary()}") 

438 

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) 

442 

443 for device in matching_devices: 

444 cout(f"- DEVICE {device.summary()}") 

445 

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 ) 

452 

453 # -- All OK.