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

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 assert isinstance(self._serial_devices, list) 

61 return self._serial_devices 

62 

63 

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

70 

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 ) 

76 

77 

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

85 

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

91 

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) 

95 

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) 

99 

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

103 

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) 

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

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

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

166 ) 

167 sys.exit(1) 

168 return custom_template 

169 

170 pr = apio_ctx.project_resources 

171 # -- Here when using the standard command. 

172 

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

176 

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 

182 

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 

188 

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 

194 

195 # -- All done. 

196 return cmd_template 

197 

198 

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

207 

208 # -- Match to a single serial device. 

209 device: SerialDevice = _match_serial_device( 

210 apio_ctx, scanner, serial_port_arg, serial_port_num 

211 ) 

212 

213 # -- Resolve serial port var. 

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

215 

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

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

218 

219 # -- All done. 

220 return cmd_template 

221 

222 

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

230 

231 # -- Match to a single usb device. 

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

233 

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) 

240 

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

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

243 

244 # -- All done. 

245 return cmd_template 

246 

247 

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

257 

258 # -- Get project resources 

259 pr = apio_ctx.project_resources 

260 

261 # -- Scan for all serial devices. 

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

263 

264 # -- Get board optional usb constraints 

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

266 

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) 

279 

280 # -- Inform the user. 

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

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

283 

284 # -- Get matching devices 

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

286 

287 for dev in matching: 

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

289 

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

293 

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

296 

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) 

305 

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) 

314 

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

316 return matching[0] 

317 

318 

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

325 

326 # -- Get project resources. 

327 pr = apio_ctx.project_resources 

328 

329 # -- Scan for all serial devices. 

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

331 

332 # -- Get board optional usb constraints 

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

334 

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) 

345 

346 # -- Inform the user. 

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

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

349 

350 # -- Get matching devices 

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

352 

353 for dev in matching: 

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

355 

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

359 

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

362 

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) 

371 

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) 

380 

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

382 return matching[0] 

383 

384 

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

390 

391 # -- Get project resources. 

392 pr = apio_ctx.project_resources 

393 

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

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

396 

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 

401 

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

412 

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

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

415 

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) 

419 

420 for device in matching_devices: 

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

422 

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) 

431 

432 # -- All OK.