Coverage for apio/managers/drivers.py: 22%

148 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2019 FPGAwars 

4# -- Author Jesús Arroyo 

5# -- License GPLv2 

6"""Manage board drivers""" 

7 

8 

9import os 

10import shutil 

11import subprocess 

12from pathlib import Path 

13from apio.utils import util 

14from apio.common.apio_console import cout, cerror, cmarkdown 

15from apio.common.apio_styles import INFO, SUCCESS, EMPH1, EMPH3 

16from apio.apio_context import ApioContext 

17 

18 

19# -- Style shortcuts 

20E1 = f"[{EMPH1}]" 

21E3 = f"[{EMPH3}]" 

22 

23# -- A message to print when trying to install/uninstall apio drivers 

24# -- on platforms that don't require it. 

25NO_DRIVERS_MSG = "No driver installation is required on this platform." 

26 

27# -- Text in the rich-text format of the python rich library. 

28FTDI_INSTALL_INSTRUCTIONS_WINDOWS = f""" 

29{E3}Please follow these steps:[/] 

30 

31 1. Make sure your {E1}FPGA board is connected[/] to the computer. 

32 

33 2. {E1}Accept the Zadig request[/] to make changes to your computer. 

34 

35 3. {E1}Find the Zadig window[/] on your screen. You may need to click 

36 on its icon in the task bar for it to appear. 

37 

38 4. {E1}Select your FPGA board[/] from the drop down list, For example 

39 'Alhambra II v1.0A - B09-335 (Interface 0)'. 

40 

41 {E3}VERY IMPORTANT - If your board appears multiple time, make sure 

42 to select its 'interface 0' entry.[/] 

43 

44 5. {E1}Select the 'WinUSB' driver[/] as the target driver. For example 

45 'WinUSB (v6.1.7600.16385)'. 

46 

47 6. {E1}Click 'Replace Driver'[/] and wait for a successful 

48 completion, this can take a minute or two. 

49 

50 7. {E1}Close the Zadig window.[/] 

51 

52 8. {E1}Disconnect and reconnect[/] your FPGA board for the new driver 

53 to take affect. 

54 

55 9. {E1}Run the command 'apio devices usb'[/] and verify that 

56 your board is listed. 

57""" 

58 

59# -- Text in the rich-text format of the python rich library. 

60FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS = f""" 

61{E3}Please follow these steps:[/] 

62 

63 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer. 

64 

65 2. If asked, {E1}allow the Device Manager to make changes to your system.[/] 

66 

67 3. {E1}Find the Device Manager window.[/] 

68 

69 4. {E1}Connect the board[/] to your computer and a new entry will be added 

70 to the device list (though sometimes it may be collapsed and 

71 hidden). 

72 

73 5. {E1}Identify the entry of your board[/] (e.g. in the 'Universal Serial 

74 Bus Devices' section). 

75 

76 {E3}NOTE: Boards with FT2232 ICs have two channels, 'interface 0' 

77 and 'interface 1'. Here we care only about 'interface 0' and 

78 ignore 'interface 1' if it appears as a COM port.[/] 

79 

80 6. {E1}Right click[/] on your board entry and \ 

81{E1}select 'Uninstall device'.[/] 

82 

83 7. If available, check the box {E1}'Delete the driver software for this 

84 device'.[/] 

85 

86 8. Click the {E1}'Uninstall' button[/]. 

87 

88 9. {E1}Close[/] the Device Manager window. 

89""" 

90 

91# -- Text in the rich-text format of the python rich library. 

92SERIAL_INSTALL_INSTRUCTIONS_WINDOWS = f""" 

93{E3}Please follow these steps:[/] 

94 

95 1. Make sure your FPGA {E1}board is connected[/] to the computer. 

96 

97 2. {E1}Accept the Serial Installer request[/] to make changes to your \ 

98computer. 

99 

100 3. Find the Serial installer window and {E1}follow the instructions.[/] 

101 

102 4. To verify, {E1}disconnect and reconnect the board[/] and run the command 

103 {E1}'apio devices serial'.[/] 

104""" 

105 

106# -- Text in the rich-text format of the python rich library. 

107SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS = f""" 

108{E3}Please follow these steps:[/] 

109 

110 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer. 

111 

112 2. If asked, {E1}allow the Device Manager to make changes[/] to your system. 

113 

114 3. {E1}Find the Device Manager window.[/] 

115 

116 4. {E1}Connect the board[/] to your computer and a new entry will be added 

117 to the device list (though sometimes it may be collapsed). 

118 

119 5. {E1}Identify the entry of your board[/] (typically in the Ports section). 

120 

121 {E3} NOTE: If your board does not show up as a COM port, it may not 

122 have the 'apio drivers --serial-install' applied to it.[/] 

123 

124 6. {E1}Right click[/] on your board entry \ 

125and {E1}select 'Uninstall device'.[/] 

126 

127 7. If available, check the box \ 

128{E1}'Delete the driver software for this device'.[/] 

129 

130 8. Click the {E1}'Uninstall' button.[/] 

131 

132 9. {E1}Close the Device Manager window.[/] 

133""" 

134 

135 

136class Drivers: 

137 """Class for managing the board drivers""" 

138 

139 # -- The driver installation on linux consist of copying the rule files 

140 # -- to the /etc/udev/rules.d folder 

141 

142 # -- FTDI source rules file paths 

143 resources_dir = util.get_path_in_apio_package("resources") 

144 ftdi_rules_local_path = resources_dir / "80-fpga-ftdi.rules" 

145 

146 # -- Target rule file 

147 ftdi_rules_system_path = Path("/etc/udev/rules.d/80-fpga-ftdi.rules") 

148 

149 # Serial rules files paths 

150 serial_rules_local_path = resources_dir / "80-fpga-serial.rules" 

151 serial_rules_system_path = Path("/etc/udev/rules.d/80-fpga-serial.rules") 

152 

153 # Driver to restore: mac os 

154 driver_c = "" 

155 

156 def __init__(self, apio_ctx: ApioContext) -> None: 

157 

158 self.apio_ctx = apio_ctx 

159 

160 def ftdi_install(self) -> int: 

161 """Installs the FTDI driver. Function is platform dependent. 

162 Returns a process exit code. 

163 """ 

164 

165 if self.apio_ctx.is_linux: 

166 return self._ftdi_install_linux() 

167 

168 if self.apio_ctx.is_darwin: 

169 return self._ftdi_install_darwin() 

170 

171 if self.apio_ctx.is_windows: 

172 return self._ftdi_install_windows() 

173 

174 cerror(f"Unknown platform type '{self.apio_ctx.platform_id}'.") 

175 return 1 

176 

177 def ftdi_uninstall(self) -> int: 

178 """Uninstalls the FTDI driver. Function is platform dependent. 

179 Returns a process exit code. 

180 """ 

181 if self.apio_ctx.is_linux: 

182 return self._ftdi_uninstall_linux() 

183 

184 if self.apio_ctx.is_darwin: 

185 return self._ftdi_uninstall_darwin() 

186 

187 if self.apio_ctx.is_windows: 

188 return self._ftdi_uninstall_windows() 

189 

190 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") 

191 return 1 

192 

193 def serial_install(self) -> int: 

194 """Installs the serial driver. Function is platform dependent. 

195 Returns a process exit code. 

196 """ 

197 

198 if self.apio_ctx.is_linux: 

199 return self._serial_install_linux() 

200 

201 if self.apio_ctx.is_darwin: 

202 return self._serial_install_darwin() 

203 

204 if self.apio_ctx.is_windows: 

205 return self._serial_install_windows() 

206 

207 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") 

208 return 1 

209 

210 def serial_uninstall(self) -> int: 

211 """Uninstalls the serial driver. Function is platform dependent. 

212 Returns a process exit code. 

213 """ 

214 if self.apio_ctx.is_linux: 

215 return self._serial_uninstall_linux() 

216 

217 if self.apio_ctx.is_darwin: 

218 return self._serial_uninstall_darwin() 

219 

220 if self.apio_ctx.is_windows: 

221 return self._serial_uninstall_windows() 

222 

223 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.") 

224 return 1 

225 

226 def _ftdi_install_linux(self) -> int: 

227 """Drivers install on Linux. It copies the .rules file into 

228 the corresponding folder. Return process exit code.""" 

229 

230 cout("Configure FTDI drivers for FPGA") 

231 

232 # -- Check if the target rules file already exists 

233 if not self.ftdi_rules_system_path.exists(): 

234 

235 # -- The file does not exist. Copy! 

236 # -- Execute the cmd: sudo cp src_file target_file, exit on error. 

237 util.subprocess_call( 

238 [ 

239 "sudo", 

240 "cp", 

241 str(self.ftdi_rules_local_path), 

242 str(self.ftdi_rules_system_path), 

243 ] 

244 ) 

245 

246 # -- Execute the commands for reloading the udev system 

247 self._reload_rules_linux() 

248 

249 cout("FTDI drivers installed", style=SUCCESS) 

250 cout("Unplug and reconnect your board", style=INFO) 

251 else: 

252 cout("Already installed", style=INFO) 

253 

254 return 0 

255 

256 def _ftdi_uninstall_linux(self): 

257 """Uninstall the FTDI drivers on linux. Returns process exist code.""" 

258 

259 # -- For disabling the FTDI driver the .rules files should be 

260 # -- removed from the /etc/udev/rules.d/ folder 

261 

262 # -- Remove the .rules file, if it exists 

263 if self.ftdi_rules_system_path.exists(): 

264 cout("Revert FTDI drivers configuration") 

265 

266 # -- Execute the sudo rm rules_file command 

267 subprocess.call(["sudo", "rm", str(self.ftdi_rules_system_path)]) 

268 

269 # -- # -- Execute the commands for reloading the udev system 

270 self._reload_rules_linux() 

271 

272 cout("FTDI drivers uninstalled", style=SUCCESS) 

273 cout("Unplug and reconnect your board", style=INFO) 

274 else: 

275 cout("Already uninstalled", style=INFO) 

276 

277 return 0 

278 

279 def _serial_install_linux(self): 

280 """Serial drivers install on Linux. Returns process exit code.""" 

281 

282 cout("Configure Serial drivers for FPGA") 

283 

284 # -- Check if the target rules file already exists 

285 if not self.serial_rules_system_path.exists(): 

286 # -- Add the user to the dialout group for 

287 # -- having access to the serial port 

288 group_added = self._add_dialout_group_linux() 

289 

290 # -- The file does not exist. Copy! 

291 # -- Execute the cmd: sudo cp src_file target_file, exit on error. 

292 util.subprocess_call( 

293 [ 

294 "sudo", 

295 "cp", 

296 str(self.serial_rules_local_path), 

297 str(self.serial_rules_system_path), 

298 ] 

299 ) 

300 

301 # -- Execute the commands for reloading the udev system 

302 self._reload_rules_linux() 

303 

304 cout("Serial drivers installed", style=SUCCESS) 

305 cout("Unplug and reconnect your board", style=INFO) 

306 if group_added: 

307 cout( 

308 "Restart your machine to install the dialout group", 

309 style=INFO, 

310 ) 

311 else: 

312 cout("Already installed", style=INFO) 

313 

314 return 0 

315 

316 def _serial_uninstall_linux(self) -> int: 

317 """Uninstall the serial driver on Linux. Return process exit code.""" 

318 

319 # -- For disabling the serial driver the corresponding .rules file 

320 # -- should be removed, it it exists 

321 if self.serial_rules_system_path.exists(): 

322 cout("Revert Serial drivers configuration") 

323 

324 # -- Execute the sudo rm rule_file cmd 

325 subprocess.call(["sudo", "rm", str(self.serial_rules_system_path)]) 

326 

327 # -- Execute the commands for reloading the udev system 

328 self._reload_rules_linux() 

329 cout("Serial drivers uninstalled", style=SUCCESS) 

330 cout("Unplug and reconnect your board", style=INFO) 

331 else: 

332 cout("Already uninstalled", style=INFO) 

333 

334 return 0 

335 

336 def _reload_rules_linux(self): 

337 """Execute the commands for reloading the udev system""" 

338 

339 # -- These are Linux commands that should be executed on 

340 # -- the shell 

341 subprocess.call(["sudo", "udevadm", "control", "--reload-rules"]) 

342 subprocess.call(["sudo", "udevadm", "trigger"]) 

343 subprocess.call(["sudo", "service", "udev", "restart"]) 

344 

345 def _add_dialout_group_linux(self): 

346 """Add the current user to the dialout group on Linux systems""" 

347 

348 # -- This operation is needed for granting access to the serial port 

349 

350 # -- Get the current groups of the user 

351 groups = subprocess.check_output("groups") 

352 

353 # -- If it does not belong to the dialout group, add it!! 

354 if "dialout" not in groups.decode(): 

355 # -- Command for adding the user to the dialout group 

356 subprocess.call("sudo usermod -a -G dialout $USER", shell=True) 

357 return True 

358 return None 

359 

360 def _ftdi_install_darwin(self) -> int: 

361 """Installs FTDI driver on darwin. Returns process status code.""" 

362 # Check homebrew 

363 cout(NO_DRIVERS_MSG, style=SUCCESS) 

364 return 0 

365 

366 def _ftdi_uninstall_darwin(self): 

367 """Uninstalls FTDI driver on darwin. Returns process status code.""" 

368 cout(NO_DRIVERS_MSG, style=SUCCESS) 

369 return 0 

370 

371 def _serial_install_darwin(self): 

372 """Installs serial driver on darwin. Returns process status code.""" 

373 cout(NO_DRIVERS_MSG, style=SUCCESS) 

374 return 0 

375 

376 def _serial_uninstall_darwin(self): 

377 """Uninstalls serial driver on darwin. Returns process status code.""" 

378 cout(NO_DRIVERS_MSG, style=SUCCESS) 

379 return 0 

380 

381 def _ftdi_install_windows(self) -> int: 

382 

383 # -- Get the drivers apio package base folder 

384 drivers_base_dir = self.apio_ctx.get_package_dir("drivers") 

385 

386 # NOTE: Zadig documentation: 

387 # https://github.com/pbatard/libwdi/wiki/Zadig?utm_source=chatgpt.com 

388 

389 # -- Path to the config file zadig.ini. 

390 zadig_ini_src = drivers_base_dir / "share" / "zadig.ini" 

391 

392 # -- Execute in a tmp directory, this way we don't contaminate the 

393 # -- current with zadig.ini, in case the program crashes. 

394 # -- Using a fix tmp location prevents accumulation of leftover 

395 # -- zadig.ini in case the are not cleaned up properly. 

396 # -- We can't store zadig under _build since we don't necessarily 

397 # -- run in a context of a project.. 

398 with util.pushd(self.apio_ctx.get_tmp_dir()): 

399 # -- Bring a copy of zadig.ini 

400 shutil.copyfile(zadig_ini_src, "zadig.ini") 

401 

402 # -- Zadig exe file with full path: 

403 zadig_exe = drivers_base_dir / "bin" / "zadig.exe" 

404 

405 # -- Show messages for the user 

406 cout("", "Launching zadig.exe.") 

407 cmarkdown(FTDI_INSTALL_INSTRUCTIONS_WINDOWS) 

408 

409 # -- Execute zadig! 

410 # -- We execute it using os.system() rather than by 

411 # -- util.exec_command() because zadig required permissions 

412 # -- elevation. 

413 exit_code = os.system(str(zadig_exe)) 

414 

415 # -- All done. 

416 return exit_code 

417 

418 def _ftdi_uninstall_windows(self) -> int: 

419 # -- Check that the required packages exist. 

420 # packages.install_missing_packages_on_the_fly( 

421 # self.apio_ctx.packages_context 

422 # ) 

423 

424 cout("", "Launching the interactive Device Manager.") 

425 cmarkdown(FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS) 

426 

427 # -- We launch the device manager using os.system() rather than with 

428 # -- util.exec_command() because util.exec_command() does not support 

429 # -- elevation. 

430 exit_code = os.system("mmc devmgmt.msc") 

431 return exit_code 

432 

433 def _serial_install_windows(self) -> int: 

434 

435 drivers_base_dir = self.apio_ctx.get_package_dir("drivers") 

436 drivers_bin_dir = drivers_base_dir / "bin" 

437 

438 cout("", "Launching the interactive Serial Installer.") 

439 cmarkdown(SERIAL_INSTALL_INSTRUCTIONS_WINDOWS) 

440 

441 # -- We launch the device manager using os.system() rather than with 

442 # -- util.exec_command() because util.exec_command() does not support 

443 # -- elevation. 

444 exit_code = os.system( 

445 str(Path(drivers_bin_dir) / "serial_install.exe") 

446 ) 

447 

448 return exit_code 

449 

450 def _serial_uninstall_windows(self) -> int: 

451 

452 cout("", "Launching the interactive Device Manager.") 

453 cmarkdown(SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS) 

454 

455 # -- We launch the device manager using os.system() rather than with 

456 # -- util.exec_command() because util.exec_command() does not support 

457 # -- elevation. 

458 exit_code = os.system("mmc devmgmt.msc") 

459 return exit_code