flashers.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import shutil
  2. import time
  3. import os
  4. import signal
  5. import usb.core
  6. from qmk.constants import BOOTLOADER_VIDS_PIDS
  7. from milc import cli
  8. # yapf: disable
  9. _PID_TO_MCU = {
  10. '2fef': 'atmega16u2',
  11. '2ff0': 'atmega32u2',
  12. '2ff3': 'atmega16u4',
  13. '2ff4': 'atmega32u4',
  14. '2ff9': 'at90usb64',
  15. '2ffa': 'at90usb162',
  16. '2ffb': 'at90usb128'
  17. }
  18. AVRDUDE_MCU = {
  19. 'atmega32a': 'm32',
  20. 'atmega328p': 'm328p',
  21. 'atmega328': 'm328',
  22. }
  23. # yapf: enable
  24. class DelayedKeyboardInterrupt:
  25. # Custom interrupt handler to delay the processing of Ctrl-C
  26. # https://stackoverflow.com/a/21919644
  27. def __enter__(self):
  28. self.signal_received = False
  29. self.old_handler = signal.signal(signal.SIGINT, self.handler)
  30. def handler(self, sig, frame):
  31. self.signal_received = (sig, frame)
  32. def __exit__(self, type, value, traceback):
  33. signal.signal(signal.SIGINT, self.old_handler)
  34. if self.signal_received:
  35. self.old_handler(*self.signal_received)
  36. # TODO: Make this more generic, so cli/doctor/check.py and flashers.py can share the code
  37. def _check_dfu_programmer_version():
  38. # Return True if version is higher than 0.7.0: supports '--force'
  39. check = cli.run(['dfu-programmer', '--version'], combined_output=True, timeout=5)
  40. first_line = check.stdout.split('\n')[0]
  41. version_number = first_line.split()[1]
  42. maj, min_, bug = version_number.split('.')
  43. if int(maj) >= 0 and int(min_) >= 7:
  44. return True
  45. else:
  46. return False
  47. def _find_bootloader():
  48. # To avoid running forever in the background, only look for bootloaders for 10min
  49. start_time = time.time()
  50. while time.time() - start_time < 600:
  51. for bl in BOOTLOADER_VIDS_PIDS:
  52. for vid, pid in BOOTLOADER_VIDS_PIDS[bl]:
  53. vid_hex = int(f'0x{vid}', 0)
  54. pid_hex = int(f'0x{pid}', 0)
  55. with DelayedKeyboardInterrupt():
  56. # PyUSB does not like to be interrupted by Ctrl-C
  57. # therefore we catch the interrupt with a custom handler
  58. # and only process it once pyusb finished
  59. dev = usb.core.find(idVendor=vid_hex, idProduct=pid_hex)
  60. if dev:
  61. if bl == 'atmel-dfu':
  62. details = _PID_TO_MCU[pid]
  63. elif bl == 'caterina':
  64. details = (vid_hex, pid_hex)
  65. elif bl == 'hid-bootloader':
  66. if vid == '16c0' and pid == '0478':
  67. details = 'halfkay'
  68. else:
  69. details = 'qmk-hid'
  70. elif bl == 'stm32-dfu' or bl == 'apm32-dfu' or bl == 'gd32v-dfu' or bl == 'kiibohd':
  71. details = (vid, pid)
  72. else:
  73. details = None
  74. return (bl, details)
  75. time.sleep(0.1)
  76. return (None, None)
  77. def _find_serial_port(vid, pid):
  78. if 'windows' in cli.platform.lower():
  79. from serial.tools.list_ports_windows import comports
  80. platform = 'windows'
  81. else:
  82. from serial.tools.list_ports_posix import comports
  83. platform = 'posix'
  84. start_time = time.time()
  85. # Caterina times out after 8 seconds
  86. while time.time() - start_time < 8:
  87. for port in comports():
  88. port, desc, hwid = port
  89. if f'{vid:04x}:{pid:04x}' in hwid.casefold():
  90. if platform == 'windows':
  91. time.sleep(1)
  92. return port
  93. else:
  94. start_time = time.time()
  95. # Wait until the port becomes writable before returning
  96. while time.time() - start_time < 8:
  97. if os.access(port, os.W_OK):
  98. return port
  99. else:
  100. time.sleep(0.5)
  101. return None
  102. return None
  103. def _flash_caterina(details, file):
  104. port = _find_serial_port(details[0], details[1])
  105. if port:
  106. cli.run(['avrdude', '-p', 'atmega32u4', '-c', 'avr109', '-U', f'flash:w:{file}:i', '-P', port], capture_output=False)
  107. return False
  108. else:
  109. return True
  110. def _flash_atmel_dfu(mcu, file):
  111. force = '--force' if _check_dfu_programmer_version() else ''
  112. cli.run(['dfu-programmer', mcu, 'erase', force], capture_output=False)
  113. cli.run(['dfu-programmer', mcu, 'flash', force, file], capture_output=False)
  114. cli.run(['dfu-programmer', mcu, 'reset'], capture_output=False)
  115. def _flash_hid_bootloader(mcu, details, file):
  116. if details == 'halfkay':
  117. if shutil.which('teensy-loader-cli'):
  118. cmd = 'teensy-loader-cli'
  119. elif shutil.which('teensy_loader_cli'):
  120. cmd = 'teensy_loader_cli'
  121. # Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay
  122. if not cmd:
  123. if shutil.which('hid_bootloader_cli'):
  124. cmd = 'hid_bootloader_cli'
  125. else:
  126. return True
  127. cli.run([cmd, f'-mmcu={mcu}', '-w', '-v', file], capture_output=False)
  128. def _flash_dfu_util(details, file):
  129. # STM32duino
  130. if details[0] == '1eaf' and details[1] == '0003':
  131. cli.run(['dfu-util', '-a', '2', '-d', f'{details[0]}:{details[1]}', '-R', '-D', file], capture_output=False)
  132. # kiibohd
  133. elif details[0] == '1c11' and details[1] == 'b007':
  134. cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-D', file], capture_output=False)
  135. # STM32, APM32, or GD32V DFU
  136. else:
  137. cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-s', '0x08000000:leave', '-D', file], capture_output=False)
  138. def _flash_isp(mcu, programmer, file):
  139. programmer = 'usbasp' if programmer == 'usbasploader' else 'usbtiny'
  140. # Check if the provide mcu has an avrdude-specific name, otherwise pass on what the user provided
  141. mcu = AVRDUDE_MCU.get(mcu, mcu)
  142. cli.run(['avrdude', '-p', mcu, '-c', programmer, '-U', f'flash:w:{file}:i'], capture_output=False)
  143. def _flash_mdloader(file):
  144. cli.run(['mdloader', '--first', '--download', file, '--restart'], capture_output=False)
  145. def flasher(mcu, file):
  146. bl, details = _find_bootloader()
  147. # Add a small sleep to avoid race conditions
  148. time.sleep(1)
  149. if bl == 'atmel-dfu':
  150. _flash_atmel_dfu(details, file.name)
  151. elif bl == 'caterina':
  152. if _flash_caterina(details, file.name):
  153. return (True, "The Caterina bootloader was found but is not writable. Check 'qmk doctor' output for advice.")
  154. elif bl == 'hid-bootloader':
  155. if mcu:
  156. if _flash_hid_bootloader(mcu, details, file.name):
  157. return (True, "Please make sure 'teensy_loader_cli' or 'hid_bootloader_cli' is available on your system.")
  158. else:
  159. return (True, "Specifying the MCU with '-m' is necessary for HalfKay/HID bootloaders!")
  160. elif bl == 'stm32-dfu' or bl == 'apm32-dfu' or bl == 'gd32v-dfu' or bl == 'kiibohd':
  161. _flash_dfu_util(details, file.name)
  162. elif bl == 'usbasploader' or bl == 'usbtinyisp':
  163. if mcu:
  164. _flash_isp(mcu, bl, file.name)
  165. else:
  166. return (True, "Specifying the MCU with '-m' is necessary for ISP flashing!")
  167. elif bl == 'md-boot':
  168. _flash_mdloader(file.name)
  169. else:
  170. return (True, "Known bootloader found but flashing not currently supported!")
  171. return (False, None)