CLI: Teaching the CLI to flash binaries (#16584)
Co-authored-by: Ryan <fauxpark@gmail.com> Co-authored-by: Sergey Vlasov <sigprof@gmail.com> Co-authored-by: Joel Challis <git@zvecr.com> Co-authored-by: Nick Brassel <nick@tzarc.org>master
parent
3bf36e8b04
commit
5e2ffe7d8f
@ -0,0 +1,203 @@ |
|||||||
|
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_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) |
||||||
|
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 |
||||||
|
dev = usb.core.find(idVendor=vid_hex, idProduct=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 == 'stm32-dfu' or bl == 'apm32-dfu' or bl == 'gd32v-dfu' or bl == 'kiibohd': |
||||||
|
details = (vid, pid) |
||||||
|
else: |
||||||
|
details = None |
||||||
|
return (bl, details) |
||||||
|
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_isp(mcu, programmer, file): |
||||||
|
programmer = 'usbasp' if programmer == 'usbasploader' else 'usbtiny' |
||||||
|
# Check if the provide 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 flasher(mcu, file): |
||||||
|
bl, details = _find_bootloader() |
||||||
|
# Add a small sleep to avoid race conditions |
||||||
|
time.sleep(1) |
||||||
|
if bl == 'atmel-dfu': |
||||||
|
_flash_atmel_dfu(details, file.name) |
||||||
|
elif bl == 'caterina': |
||||||
|
if _flash_caterina(details, file.name): |
||||||
|
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.name): |
||||||
|
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 == 'stm32-dfu' or bl == 'apm32-dfu' or bl == 'gd32v-dfu' or bl == 'kiibohd': |
||||||
|
_flash_dfu_util(details, file.name) |
||||||
|
elif bl == 'usbasploader' or bl == 'usbtinyisp': |
||||||
|
if mcu: |
||||||
|
_flash_isp(mcu, bl, file.name) |
||||||
|
else: |
||||||
|
return (True, "Specifying the MCU with '-m' is necessary for ISP flashing!") |
||||||
|
elif bl == 'md-boot': |
||||||
|
_flash_mdloader(file.name) |
||||||
|
else: |
||||||
|
return (True, "Known bootloader found but flashing not currently supported!") |
||||||
|
|
||||||
|
return (False, None) |
Loading…
Reference in new issue