import platform import shutil import time import os import signal import usb.core from qmk.constants import BOOTLOADER_VIDS_PIDS from milc import cli # yapf: disable _PID_TO_MCU = { '2fef': 'atmega16u2', '2ff0': 'atmega32u2', '2ff3': 'atmega16u4', '2ff4': 'atmega32u4', '2ff9': 'at90usb64', '2ffa': 'at90usb162', '2ffb': 'at90usb128' } AVRDUDE_MCU = { 'atmega32a': 'm32', 'atmega328p': 'm328p', 'atmega328': 'm328', } # yapf: enable class DelayedKeyboardInterrupt: # Custom interrupt handler to delay the processing of Ctrl-C # https://stackoverflow.com/a/21919644 def __enter__(self): self.signal_received = False self.old_handler = signal.signal(signal.SIGINT, self.handler) def handler(self, sig, frame): self.signal_received = (sig, frame) def __exit__(self, type, value, traceback): signal.signal(signal.SIGINT, self.old_handler) if self.signal_received: self.old_handler(*self.signal_received) # TODO: Make this more generic, so cli/doctor/check.py and flashers.py can share the code def _check_dfu_programmer_version(): # Return True if version is higher than 0.7.0: supports '--force' check = cli.run(['dfu-programmer', '--version'], combined_output=True, timeout=5) first_line = check.stdout.split('\n')[0] version_number = first_line.split()[1] maj, min_, bug = version_number.split('.') if int(maj) >= 0 and int(min_) >= 7: return True else: return False def _find_usb_device(vid_hex, pid_hex): # WSL doesnt have access to USB - use powershell instead...? if 'microsoft' in platform.uname().release.lower(): ret = cli.run(['powershell.exe', '-command', 'Get-PnpDevice -PresentOnly | Select-Object -Property InstanceId']) if f'USB\\VID_{vid_hex:04X}&PID_{pid_hex:04X}' in ret.stdout: return (vid_hex, pid_hex) else: with DelayedKeyboardInterrupt(): # PyUSB does not like to be interrupted by Ctrl-C # therefore we catch the interrupt with a custom handler # and only process it once pyusb finished return usb.core.find(idVendor=vid_hex, idProduct=pid_hex) def _find_uf2_devices(): """Delegate to uf2conv.py as VID:PID pairs can potentially fluctuate more than other bootloaders """ return cli.run(['util/uf2conv.py', '--list']).stdout.splitlines() def _find_bootloader(): # To avoid running forever in the background, only look for bootloaders for 10min start_time = time.time() while time.time() - start_time < 600: for bl in BOOTLOADER_VIDS_PIDS: for vid, pid in BOOTLOADER_VIDS_PIDS[bl]: vid_hex = int(f'0x{vid}', 0) pid_hex = int(f'0x{pid}', 0) dev = _find_usb_device(vid_hex, pid_hex) if dev: if bl == 'atmel-dfu': details = _PID_TO_MCU[pid] elif bl == 'caterina': details = (vid_hex, pid_hex) elif bl == 'hid-bootloader': if vid == '16c0' and pid == '0478': details = 'halfkay' else: details = 'qmk-hid' elif bl in {'apm32-dfu', 'gd32v-dfu', 'kiibohd', 'stm32-dfu'}: details = (vid, pid) else: details = None return (bl, details) if _find_uf2_devices(): return ('_uf2_compatible_', None) time.sleep(0.1) return (None, None) def _find_serial_port(vid, pid): if 'windows' in cli.platform.lower(): from serial.tools.list_ports_windows import comports platform = 'windows' else: from serial.tools.list_ports_posix import comports platform = 'posix' start_time = time.time() # Caterina times out after 8 seconds while time.time() - start_time < 8: for port in comports(): port, desc, hwid = port if f'{vid:04x}:{pid:04x}' in hwid.casefold(): if platform == 'windows': time.sleep(1) return port else: start_time = time.time() # Wait until the port becomes writable before returning while time.time() - start_time < 8: if os.access(port, os.W_OK): return port else: time.sleep(0.5) return None return None def _flash_caterina(details, file): port = _find_serial_port(details[0], details[1]) if port: cli.run(['avrdude', '-p', 'atmega32u4', '-c', 'avr109', '-U', f'flash:w:{file}:i', '-P', port], capture_output=False) return False else: return True def _flash_atmel_dfu(mcu, file): force = '--force' if _check_dfu_programmer_version() else '' cli.run(['dfu-programmer', mcu, 'erase', force], capture_output=False) cli.run(['dfu-programmer', mcu, 'flash', force, file], capture_output=False) cli.run(['dfu-programmer', mcu, 'reset'], capture_output=False) def _flash_hid_bootloader(mcu, details, file): if details == 'halfkay': if shutil.which('teensy-loader-cli'): cmd = 'teensy-loader-cli' elif shutil.which('teensy_loader_cli'): cmd = 'teensy_loader_cli' # Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay if not cmd: if shutil.which('hid_bootloader_cli'): cmd = 'hid_bootloader_cli' else: return True cli.run([cmd, f'-mmcu={mcu}', '-w', '-v', file], capture_output=False) def _flash_dfu_util(details, file): # STM32duino if details[0] == '1eaf' and details[1] == '0003': cli.run(['dfu-util', '-a', '2', '-d', f'{details[0]}:{details[1]}', '-R', '-D', file], capture_output=False) # kiibohd elif details[0] == '1c11' and details[1] == 'b007': cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-D', file], capture_output=False) # STM32, APM32, or GD32V DFU else: cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-s', '0x08000000:leave', '-D', file], capture_output=False) def _flash_wb32_dfu_updater(file): if shutil.which('wb32-dfu-updater_cli'): cmd = 'wb32-dfu-updater_cli' else: return True cli.run([cmd, '-t', '-s', '0x08000000', '-D', file], capture_output=False) def _flash_isp(mcu, programmer, file): programmer = 'usbasp' if programmer == 'usbasploader' else 'usbtiny' # Check if the provided mcu has an avrdude-specific name, otherwise pass on what the user provided mcu = AVRDUDE_MCU.get(mcu, mcu) cli.run(['avrdude', '-p', mcu, '-c', programmer, '-U', f'flash:w:{file}:i'], capture_output=False) def _flash_mdloader(file): cli.run(['mdloader', '--first', '--download', file, '--restart'], capture_output=False) def _flash_uf2(file): cli.run(['util/uf2conv.py', '--deploy', file], capture_output=False) def flasher(mcu, file): # Avoid "expected string or bytes-like object, got 'WindowsPath" issues file = file.as_posix() bl, details = _find_bootloader() # Add a small sleep to avoid race conditions time.sleep(1) if bl == 'atmel-dfu': _flash_atmel_dfu(details, file) elif bl == 'caterina': if _flash_caterina(details, file): return (True, "The Caterina bootloader was found but is not writable. Check 'qmk doctor' output for advice.") elif bl == 'hid-bootloader': if mcu: if _flash_hid_bootloader(mcu, details, file): return (True, "Please make sure 'teensy_loader_cli' or 'hid_bootloader_cli' is available on your system.") else: return (True, "Specifying the MCU with '-m' is necessary for HalfKay/HID bootloaders!") elif bl in {'apm32-dfu', 'gd32v-dfu', 'kiibohd', 'stm32-dfu'}: _flash_dfu_util(details, file) elif bl == 'wb32-dfu': if _flash_wb32_dfu_updater(file): return (True, "Please make sure 'wb32-dfu-updater_cli' is available on your system.") elif bl == 'usbasploader' or bl == 'usbtinyisp': if mcu: _flash_isp(mcu, bl, file) else: return (True, "Specifying the MCU with '-m' is necessary for ISP flashing!") elif bl == 'md-boot': _flash_mdloader(file) elif bl == '_uf2_compatible_': _flash_uf2(file) else: return (True, "Known bootloader found but flashing not currently supported!") return (False, None)