logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

flashers.py (8968B)


  1. import platform
  2. import shutil
  3. import time
  4. import os
  5. import signal
  6. import usb.core
  7. from qmk.constants import BOOTLOADER_VIDS_PIDS
  8. from milc import cli
  9. # yapf: disable
  10. _PID_TO_MCU = {
  11. '2fef': 'atmega16u2',
  12. '2ff0': 'atmega32u2',
  13. '2ff3': 'atmega16u4',
  14. '2ff4': 'atmega32u4',
  15. '2ff9': 'at90usb64',
  16. '2ffa': 'at90usb162',
  17. '2ffb': 'at90usb128'
  18. }
  19. AVRDUDE_MCU = {
  20. 'atmega32a': 'm32',
  21. 'atmega328p': 'm328p',
  22. 'atmega328': 'm328',
  23. }
  24. # yapf: enable
  25. class DelayedKeyboardInterrupt:
  26. # Custom interrupt handler to delay the processing of Ctrl-C
  27. # https://stackoverflow.com/a/21919644
  28. def __enter__(self):
  29. self.signal_received = False
  30. self.old_handler = signal.signal(signal.SIGINT, self.handler)
  31. def handler(self, sig, frame):
  32. self.signal_received = (sig, frame)
  33. def __exit__(self, type, value, traceback):
  34. signal.signal(signal.SIGINT, self.old_handler)
  35. if self.signal_received:
  36. self.old_handler(*self.signal_received)
  37. # TODO: Make this more generic, so cli/doctor/check.py and flashers.py can share the code
  38. def _check_dfu_programmer_version():
  39. # Return True if version is higher than 0.7.0: supports '--force'
  40. check = cli.run(['dfu-programmer', '--version'], combined_output=True, timeout=5)
  41. first_line = check.stdout.split('\n')[0]
  42. version_number = first_line.split()[1]
  43. maj, min_, bug = version_number.split('.')
  44. if int(maj) >= 0 and int(min_) >= 7:
  45. return True
  46. else:
  47. return False
  48. def _find_usb_device(vid_hex, pid_hex):
  49. # WSL doesnt have access to USB - use powershell instead...?
  50. if 'microsoft' in platform.uname().release.lower():
  51. ret = cli.run(['powershell.exe', '-command', 'Get-PnpDevice -PresentOnly | Select-Object -Property InstanceId'])
  52. if f'USB\\VID_{vid_hex:04X}&PID_{pid_hex:04X}' in ret.stdout:
  53. return (vid_hex, pid_hex)
  54. else:
  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. return usb.core.find(idVendor=vid_hex, idProduct=pid_hex)
  60. def _find_uf2_devices():
  61. """Delegate to uf2conv.py as VID:PID pairs can potentially fluctuate more than other bootloaders
  62. """
  63. return cli.run(['util/uf2conv.py', '--list']).stdout.splitlines()
  64. def _find_bootloader():
  65. # To avoid running forever in the background, only look for bootloaders for 10min
  66. start_time = time.time()
  67. while time.time() - start_time < 600:
  68. for bl in BOOTLOADER_VIDS_PIDS:
  69. for vid, pid in BOOTLOADER_VIDS_PIDS[bl]:
  70. vid_hex = int(f'0x{vid}', 0)
  71. pid_hex = int(f'0x{pid}', 0)
  72. dev = _find_usb_device(vid_hex, pid_hex)
  73. if dev:
  74. if bl == 'atmel-dfu':
  75. details = _PID_TO_MCU[pid]
  76. elif bl == 'caterina':
  77. details = (vid_hex, pid_hex)
  78. elif bl == 'hid-bootloader':
  79. if vid == '16c0' and pid == '0478':
  80. details = 'halfkay'
  81. else:
  82. details = 'qmk-hid'
  83. elif bl in {'apm32-dfu', 'gd32v-dfu', 'kiibohd', 'stm32-dfu'}:
  84. details = (vid, pid)
  85. else:
  86. details = None
  87. return (bl, details)
  88. if _find_uf2_devices():
  89. return ('_uf2_compatible_', None)
  90. time.sleep(0.1)
  91. return (None, None)
  92. def _find_serial_port(vid, pid):
  93. if 'windows' in cli.platform.lower():
  94. from serial.tools.list_ports_windows import comports
  95. platform = 'windows'
  96. else:
  97. from serial.tools.list_ports_posix import comports
  98. platform = 'posix'
  99. start_time = time.time()
  100. # Caterina times out after 8 seconds
  101. while time.time() - start_time < 8:
  102. for port in comports():
  103. port, desc, hwid = port
  104. if f'{vid:04x}:{pid:04x}' in hwid.casefold():
  105. if platform == 'windows':
  106. time.sleep(1)
  107. return port
  108. else:
  109. start_time = time.time()
  110. # Wait until the port becomes writable before returning
  111. while time.time() - start_time < 8:
  112. if os.access(port, os.W_OK):
  113. return port
  114. else:
  115. time.sleep(0.5)
  116. return None
  117. return None
  118. def _flash_caterina(details, file):
  119. port = _find_serial_port(details[0], details[1])
  120. if port:
  121. cli.run(['avrdude', '-p', 'atmega32u4', '-c', 'avr109', '-U', f'flash:w:{file}:i', '-P', port], capture_output=False)
  122. return False
  123. else:
  124. return True
  125. def _flash_atmel_dfu(mcu, file):
  126. force = '--force' if _check_dfu_programmer_version() else ''
  127. cli.run(['dfu-programmer', mcu, 'erase', force], capture_output=False)
  128. cli.run(['dfu-programmer', mcu, 'flash', force, file], capture_output=False)
  129. cli.run(['dfu-programmer', mcu, 'reset'], capture_output=False)
  130. def _flash_hid_bootloader(mcu, details, file):
  131. if details == 'halfkay':
  132. if shutil.which('teensy-loader-cli'):
  133. cmd = 'teensy-loader-cli'
  134. elif shutil.which('teensy_loader_cli'):
  135. cmd = 'teensy_loader_cli'
  136. # Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay
  137. if not cmd:
  138. if shutil.which('hid_bootloader_cli'):
  139. cmd = 'hid_bootloader_cli'
  140. else:
  141. return True
  142. cli.run([cmd, f'-mmcu={mcu}', '-w', '-v', file], capture_output=False)
  143. def _flash_dfu_util(details, file):
  144. # STM32duino
  145. if details[0] == '1eaf' and details[1] == '0003':
  146. cli.run(['dfu-util', '-a', '2', '-d', f'{details[0]}:{details[1]}', '-R', '-D', file], capture_output=False)
  147. # kiibohd
  148. elif details[0] == '1c11' and details[1] == 'b007':
  149. cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-D', file], capture_output=False)
  150. # STM32, APM32, or GD32V DFU
  151. else:
  152. cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-s', '0x08000000:leave', '-D', file], capture_output=False)
  153. def _flash_wb32_dfu_updater(file):
  154. if shutil.which('wb32-dfu-updater_cli'):
  155. cmd = 'wb32-dfu-updater_cli'
  156. else:
  157. return True
  158. cli.run([cmd, '-t', '-s', '0x08000000', '-D', file], capture_output=False)
  159. def _flash_isp(mcu, programmer, file):
  160. programmer = 'usbasp' if programmer == 'usbasploader' else 'usbtiny'
  161. # Check if the provided mcu has an avrdude-specific name, otherwise pass on what the user provided
  162. mcu = AVRDUDE_MCU.get(mcu, mcu)
  163. cli.run(['avrdude', '-p', mcu, '-c', programmer, '-U', f'flash:w:{file}:i'], capture_output=False)
  164. def _flash_mdloader(file):
  165. cli.run(['mdloader', '--first', '--download', file, '--restart'], capture_output=False)
  166. def _flash_uf2(file):
  167. output = cli.run(['util/uf2conv.py', '--info', file]).stdout
  168. if 'UF2 File' not in output:
  169. return True
  170. cli.run(['util/uf2conv.py', '--deploy', file], capture_output=False)
  171. def flasher(mcu, file):
  172. # Avoid "expected string or bytes-like object, got 'WindowsPath" issues
  173. file = file.as_posix()
  174. bl, details = _find_bootloader()
  175. # Add a small sleep to avoid race conditions
  176. time.sleep(1)
  177. if bl == 'atmel-dfu':
  178. _flash_atmel_dfu(details, file)
  179. elif bl == 'caterina':
  180. if _flash_caterina(details, file):
  181. return (True, "The Caterina bootloader was found but is not writable. Check 'qmk doctor' output for advice.")
  182. elif bl == 'hid-bootloader':
  183. if mcu:
  184. if _flash_hid_bootloader(mcu, details, file):
  185. return (True, "Please make sure 'teensy_loader_cli' or 'hid_bootloader_cli' is available on your system.")
  186. else:
  187. return (True, "Specifying the MCU with '-m' is necessary for HalfKay/HID bootloaders!")
  188. elif bl in {'apm32-dfu', 'gd32v-dfu', 'kiibohd', 'stm32-dfu'}:
  189. _flash_dfu_util(details, file)
  190. elif bl == 'wb32-dfu':
  191. if _flash_wb32_dfu_updater(file):
  192. return (True, "Please make sure 'wb32-dfu-updater_cli' is available on your system.")
  193. elif bl == 'usbasploader' or bl == 'usbtinyisp':
  194. if mcu:
  195. _flash_isp(mcu, bl, file)
  196. else:
  197. return (True, "Specifying the MCU with '-m' is necessary for ISP flashing!")
  198. elif bl == 'md-boot':
  199. _flash_mdloader(file)
  200. elif bl == '_uf2_compatible_':
  201. if _flash_uf2(file):
  202. return (True, "Flashing only supports uf2 format files.")
  203. else:
  204. return (True, "Known bootloader found but flashing not currently supported!")
  205. return (False, None)