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
« 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"""
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
19# -- Style shortcuts
20E1 = f"[{EMPH1}]"
21E3 = f"[{EMPH3}]"
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."
27# -- Text in the rich-text format of the python rich library.
28FTDI_INSTALL_INSTRUCTIONS_WINDOWS = f"""
29{E3}Please follow these steps:[/]
31 1. Make sure your {E1}FPGA board is connected[/] to the computer.
33 2. {E1}Accept the Zadig request[/] to make changes to your computer.
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.
38 4. {E1}Select your FPGA board[/] from the drop down list, For example
39 'Alhambra II v1.0A - B09-335 (Interface 0)'.
41 {E3}VERY IMPORTANT - If your board appears multiple time, make sure
42 to select its 'interface 0' entry.[/]
44 5. {E1}Select the 'WinUSB' driver[/] as the target driver. For example
45 'WinUSB (v6.1.7600.16385)'.
47 6. {E1}Click 'Replace Driver'[/] and wait for a successful
48 completion, this can take a minute or two.
50 7. {E1}Close the Zadig window.[/]
52 8. {E1}Disconnect and reconnect[/] your FPGA board for the new driver
53 to take affect.
55 9. {E1}Run the command 'apio devices usb'[/] and verify that
56 your board is listed.
57"""
59# -- Text in the rich-text format of the python rich library.
60FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS = f"""
61{E3}Please follow these steps:[/]
63 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer.
65 2. If asked, {E1}allow the Device Manager to make changes to your system.[/]
67 3. {E1}Find the Device Manager window.[/]
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).
73 5. {E1}Identify the entry of your board[/] (e.g. in the 'Universal Serial
74 Bus Devices' section).
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.[/]
80 6. {E1}Right click[/] on your board entry and \
81{E1}select 'Uninstall device'.[/]
83 7. If available, check the box {E1}'Delete the driver software for this
84 device'.[/]
86 8. Click the {E1}'Uninstall' button[/].
88 9. {E1}Close[/] the Device Manager window.
89"""
91# -- Text in the rich-text format of the python rich library.
92SERIAL_INSTALL_INSTRUCTIONS_WINDOWS = f"""
93{E3}Please follow these steps:[/]
95 1. Make sure your FPGA {E1}board is connected[/] to the computer.
97 2. {E1}Accept the Serial Installer request[/] to make changes to your \
98computer.
100 3. Find the Serial installer window and {E1}follow the instructions.[/]
102 4. To verify, {E1}disconnect and reconnect the board[/] and run the command
103 {E1}'apio devices serial'.[/]
104"""
106# -- Text in the rich-text format of the python rich library.
107SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS = f"""
108{E3}Please follow these steps:[/]
110 1. Make sure your FPGA {E1}board is NOT connected[/] to the computer.
112 2. If asked, {E1}allow the Device Manager to make changes[/] to your system.
114 3. {E1}Find the Device Manager window.[/]
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).
119 5. {E1}Identify the entry of your board[/] (typically in the Ports section).
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.[/]
124 6. {E1}Right click[/] on your board entry \
125and {E1}select 'Uninstall device'.[/]
127 7. If available, check the box \
128{E1}'Delete the driver software for this device'.[/]
130 8. Click the {E1}'Uninstall' button.[/]
132 9. {E1}Close the Device Manager window.[/]
133"""
136class Drivers:
137 """Class for managing the board drivers"""
139 # -- The driver installation on linux consist of copying the rule files
140 # -- to the /etc/udev/rules.d folder
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"
146 # -- Target rule file
147 ftdi_rules_system_path = Path("/etc/udev/rules.d/80-fpga-ftdi.rules")
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")
153 # Driver to restore: mac os
154 driver_c = ""
156 def __init__(self, apio_ctx: ApioContext) -> None:
158 self.apio_ctx = apio_ctx
160 def ftdi_install(self) -> int:
161 """Installs the FTDI driver. Function is platform dependent.
162 Returns a process exit code.
163 """
165 if self.apio_ctx.is_linux:
166 return self._ftdi_install_linux()
168 if self.apio_ctx.is_darwin:
169 return self._ftdi_install_darwin()
171 if self.apio_ctx.is_windows:
172 return self._ftdi_install_windows()
174 cerror(f"Unknown platform type '{self.apio_ctx.platform_id}'.")
175 return 1
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()
184 if self.apio_ctx.is_darwin:
185 return self._ftdi_uninstall_darwin()
187 if self.apio_ctx.is_windows:
188 return self._ftdi_uninstall_windows()
190 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.")
191 return 1
193 def serial_install(self) -> int:
194 """Installs the serial driver. Function is platform dependent.
195 Returns a process exit code.
196 """
198 if self.apio_ctx.is_linux:
199 return self._serial_install_linux()
201 if self.apio_ctx.is_darwin:
202 return self._serial_install_darwin()
204 if self.apio_ctx.is_windows:
205 return self._serial_install_windows()
207 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.")
208 return 1
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()
217 if self.apio_ctx.is_darwin:
218 return self._serial_uninstall_darwin()
220 if self.apio_ctx.is_windows:
221 return self._serial_uninstall_windows()
223 cerror(f"Unknown platform '{self.apio_ctx.platform_id}'.")
224 return 1
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."""
230 cout("Configure FTDI drivers for FPGA")
232 # -- Check if the target rules file already exists
233 if not self.ftdi_rules_system_path.exists():
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 )
246 # -- Execute the commands for reloading the udev system
247 self._reload_rules_linux()
249 cout("FTDI drivers installed", style=SUCCESS)
250 cout("Unplug and reconnect your board", style=INFO)
251 else:
252 cout("Already installed", style=INFO)
254 return 0
256 def _ftdi_uninstall_linux(self):
257 """Uninstall the FTDI drivers on linux. Returns process exist code."""
259 # -- For disabling the FTDI driver the .rules files should be
260 # -- removed from the /etc/udev/rules.d/ folder
262 # -- Remove the .rules file, if it exists
263 if self.ftdi_rules_system_path.exists():
264 cout("Revert FTDI drivers configuration")
266 # -- Execute the sudo rm rules_file command
267 subprocess.call(["sudo", "rm", str(self.ftdi_rules_system_path)])
269 # -- # -- Execute the commands for reloading the udev system
270 self._reload_rules_linux()
272 cout("FTDI drivers uninstalled", style=SUCCESS)
273 cout("Unplug and reconnect your board", style=INFO)
274 else:
275 cout("Already uninstalled", style=INFO)
277 return 0
279 def _serial_install_linux(self):
280 """Serial drivers install on Linux. Returns process exit code."""
282 cout("Configure Serial drivers for FPGA")
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()
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 )
301 # -- Execute the commands for reloading the udev system
302 self._reload_rules_linux()
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)
314 return 0
316 def _serial_uninstall_linux(self) -> int:
317 """Uninstall the serial driver on Linux. Return process exit code."""
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")
324 # -- Execute the sudo rm rule_file cmd
325 subprocess.call(["sudo", "rm", str(self.serial_rules_system_path)])
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)
334 return 0
336 def _reload_rules_linux(self):
337 """Execute the commands for reloading the udev system"""
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"])
345 def _add_dialout_group_linux(self):
346 """Add the current user to the dialout group on Linux systems"""
348 # -- This operation is needed for granting access to the serial port
350 # -- Get the current groups of the user
351 groups = subprocess.check_output("groups")
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
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
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
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
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
381 def _ftdi_install_windows(self) -> int:
383 # -- Get the drivers apio package base folder
384 drivers_base_dir = self.apio_ctx.get_package_dir("drivers")
386 # NOTE: Zadig documentation:
387 # https://github.com/pbatard/libwdi/wiki/Zadig?utm_source=chatgpt.com
389 # -- Path to the config file zadig.ini.
390 zadig_ini_src = drivers_base_dir / "share" / "zadig.ini"
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")
402 # -- Zadig exe file with full path:
403 zadig_exe = drivers_base_dir / "bin" / "zadig.exe"
405 # -- Show messages for the user
406 cout("", "Launching zadig.exe.")
407 cmarkdown(FTDI_INSTALL_INSTRUCTIONS_WINDOWS)
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))
415 # -- All done.
416 return exit_code
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 # )
424 cout("", "Launching the interactive Device Manager.")
425 cmarkdown(FTDI_UNINSTALL_INSTRUCTIONS_WINDOWS)
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
433 def _serial_install_windows(self) -> int:
435 drivers_base_dir = self.apio_ctx.get_package_dir("drivers")
436 drivers_bin_dir = drivers_base_dir / "bin"
438 cout("", "Launching the interactive Serial Installer.")
439 cmarkdown(SERIAL_INSTALL_INSTRUCTIONS_WINDOWS)
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 )
448 return exit_code
450 def _serial_uninstall_windows(self) -> int:
452 cout("", "Launching the interactive Device Manager.")
453 cmarkdown(SERIAL_UNINSTALL_INSTRUCTIONS_WINDOWS)
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