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