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

1"""DOC: TODO""" 

2 

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 

8 

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 

16 

17# -- For USB devices 

18VID_VAR = "${VID}" 

19PID_VAR = "${PID}" 

20BUS_VAR = "${BUS}" 

21DEV_VAR = "${DEV}" 

22SERIAL_NUM_VAR = "${SERIAL_NUM}" 

23 

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

25 

26# -- For serial devices. 

27SERIAL_PORT_VAR = "${SERIAL_PORT}" 

28SERIAL_VARS = [SERIAL_PORT_VAR] 

29 

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" 

35 

36# -- All possible vars. 

37ALL_VARS = USB_VARS + SERIAL_VARS + [BIN_FILE_VAR] 

38 

39 

40class _DeviceScanner: 

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

42 

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 

47 

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 

54 

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 

63 

64 

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

71 

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 ) 

77 

78 

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

86 

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

92 

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) 

96 

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) 

100 

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

104 

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 ) 

113 

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 ) 

119 

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 ) 

125 

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) 

134 

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

136 cmd = cmd_template 

137 

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

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

140 

141 # -- Return the resolved command. 

142 return cmd 

143 

144 

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

149 

150 

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

157 

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 

168 

169 pr = apio_ctx.project_resources 

170 board_definition = pr.board_definition 

171 programmer_definition = pr.programmer_definition 

172 

173 # -- Here when using the standard command. 

174 

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 

179 

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 

187 

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 

194 

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 

200 

201 # -- All done. 

202 return cmd_template 

203 

204 

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

213 

214 # -- Match to a single serial device. 

215 device: SerialDevice = _match_serial_device( 

216 apio_ctx, scanner, serial_port_arg, serial_port_num 

217 ) 

218 

219 # -- Resolve serial port var. 

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

221 

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

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

224 

225 # -- All done. 

226 return cmd_template 

227 

228 

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

236 

237 # -- Match to a single usb device. 

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

239 

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) 

246 

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

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

249 

250 # -- All done. 

251 return cmd_template 

252 

253 

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

263 

264 # -- Get project resources 

265 pr = apio_ctx.project_resources 

266 board_definition = pr.board_definition 

267 

268 # -- Scan for all serial devices. 

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

270 

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 ) 

276 

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) 

287 

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) 

292 

293 # -- Inform the user. 

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

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

296 

297 # -- Get matching devices 

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

299 

300 for dev in matching: 

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

302 

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

306 

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

309 

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 ) 

317 

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 ) 

325 

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

327 return matching[0] 

328 

329 

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

336 

337 # -- Get project resources. 

338 pr = apio_ctx.project_resources 

339 board_definition = pr.board_definition 

340 

341 # -- Scan for all serial devices. 

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

343 

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 ) 

351 

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) 

362 

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) 

365 

366 # -- Inform the user. 

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

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

369 

370 # -- Get matching devices 

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

372 

373 for dev in matching: 

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

375 

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

379 

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

382 

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 ) 

389 

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 ) 

396 

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

398 return matching[0] 

399 

400 

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

406 

407 # -- Get project resources. 

408 pr = apio_ctx.project_resources 

409 board_definition = pr.board_definition 

410 

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 

415 

416 # -- Get board optional usb constraints 

417 usb_info = ( 

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

419 ) 

420 

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) 

433 

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

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

436 

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) 

440 

441 for device in matching_devices: 

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

443 

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 ) 

450 

451 # -- All OK.