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

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 

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 

17 

18# -- For USB devices 

19VID_VAR = "${VID}" 

20PID_VAR = "${PID}" 

21BUS_VAR = "${BUS}" 

22DEV_VAR = "${DEV}" 

23SERIAL_NUM_VAR = "${SERIAL_NUM}" 

24 

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

26 

27# -- For serial devices. 

28SERIAL_PORT_VAR = "${SERIAL_PORT}" 

29SERIAL_VARS = [SERIAL_PORT_VAR] 

30 

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" 

36 

37# -- All possible vars. 

38ALL_VARS = USB_VARS + SERIAL_VARS + [BIN_FILE_VAR] 

39 

40 

41class _DeviceScanner: 

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

43 

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 

48 

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 

55 

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 

64 

65 

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

72 

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 ) 

78 

79 

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

87 

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

93 

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) 

97 

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) 

101 

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

105 

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) 

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): 

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 cerror( 

167 f"Custom programmer-cmd should not contain '{BIN_FILE_VALUE}'." 

168 ) 

169 sys.exit(1) 

170 return custom_template 

171 

172 pr = apio_ctx.project_resources 

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 cmd_template = pr.programmer_info["command"] 

178 

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 

184 

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 

190 

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 

196 

197 # -- All done. 

198 return cmd_template 

199 

200 

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

209 

210 # -- Match to a single serial device. 

211 device: SerialDevice = _match_serial_device( 

212 apio_ctx, scanner, serial_port_arg, serial_port_num 

213 ) 

214 

215 # -- Resolve serial port var. 

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

217 

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

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

220 

221 # -- All done. 

222 return cmd_template 

223 

224 

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

232 

233 # -- Match to a single usb device. 

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

235 

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) 

242 

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

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

245 

246 # -- All done. 

247 return cmd_template 

248 

249 

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

259 

260 # -- Get project resources 

261 pr = apio_ctx.project_resources 

262 

263 # -- Scan for all serial devices. 

264 all_devices: List[SerialDevice] = scanner.get_serial_devices() 

265 

266 # -- Get board optional usb constraints 

267 usb_info = pr.board_info.get("usb", {}) 

268 

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) 

281 

282 # -- Inform the user. 

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

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

285 

286 # -- Get matching devices 

287 matching: List[SerialDevice] = serial_filter.filter(all_devices) 

288 

289 for dev in matching: 

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

291 

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

295 

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

298 

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) 

307 

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) 

316 

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

318 return matching[0] 

319 

320 

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

327 

328 # -- Get project resources. 

329 pr = apio_ctx.project_resources 

330 

331 # -- Scan for all serial devices. 

332 all_devices: List[UsbDevice] = scanner.get_usb_devices() 

333 

334 # -- Get board optional usb constraints 

335 usb_info = pr.board_info.get("usb", {}) 

336 

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) 

347 

348 # -- Inform the user. 

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

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

351 

352 # -- Get matching devices 

353 matching: List[UsbDevice] = usb_filter.filter(all_devices) 

354 

355 for dev in matching: 

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

357 

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

361 

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

364 

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) 

373 

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) 

382 

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

384 return matching[0] 

385 

386 

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

392 

393 # -- Get project resources. 

394 pr = apio_ctx.project_resources 

395 

396 # -- Get the optional "usb" section of the board. 

397 usb_info: Dict[str, str] = pr.board_info.get("usb", None) 

398 

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 

403 

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

414 

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

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

417 

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) 

421 

422 for device in matching_devices: 

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

424 

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) 

433 

434 # -- All OK.