diff options
Diffstat (limited to 'lib/python')
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
| -rw-r--r-- | lib/python/qmk/cli/doctor/linux.py | 57 | ||||
| -rw-r--r-- | lib/python/qmk/cli/flash.py | 120 | ||||
| -rw-r--r-- | lib/python/qmk/constants.py | 48 | ||||
| -rw-r--r-- | lib/python/qmk/flashers.py | 203 |
5 files changed, 336 insertions, 93 deletions
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 8a507677ef..f05b2a746e 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py | |||
| @@ -15,6 +15,7 @@ from milc.questions import yesno | |||
| 15 | import_names = { | 15 | import_names = { |
| 16 | # A mapping of package name to importable name | 16 | # A mapping of package name to importable name |
| 17 | 'pep8-naming': 'pep8ext_naming', | 17 | 'pep8-naming': 'pep8ext_naming', |
| 18 | 'pyserial': 'serial', | ||
| 18 | 'pyusb': 'usb.core', | 19 | 'pyusb': 'usb.core', |
| 19 | 'qmk-dotty-dict': 'dotty_dict', | 20 | 'qmk-dotty-dict': 'dotty_dict', |
| 20 | 'pillow': 'PIL' | 21 | 'pillow': 'PIL' |
diff --git a/lib/python/qmk/cli/doctor/linux.py b/lib/python/qmk/cli/doctor/linux.py index 94683d3307..a803305c0d 100644 --- a/lib/python/qmk/cli/doctor/linux.py +++ b/lib/python/qmk/cli/doctor/linux.py | |||
| @@ -6,7 +6,7 @@ from pathlib import Path | |||
| 6 | 6 | ||
| 7 | from milc import cli | 7 | from milc import cli |
| 8 | 8 | ||
| 9 | from qmk.constants import QMK_FIRMWARE | 9 | from qmk.constants import QMK_FIRMWARE, BOOTLOADER_VIDS_PIDS |
| 10 | from .check import CheckStatus | 10 | from .check import CheckStatus |
| 11 | 11 | ||
| 12 | 12 | ||
| @@ -26,6 +26,18 @@ def _udev_rule(vid, pid=None, *args): | |||
| 26 | return rule | 26 | return rule |
| 27 | 27 | ||
| 28 | 28 | ||
| 29 | def _generate_desired_rules(bootloader_vids_pids): | ||
| 30 | rules = dict() | ||
| 31 | for bl in bootloader_vids_pids.keys(): | ||
| 32 | rules[bl] = set() | ||
| 33 | for vid_pid in bootloader_vids_pids[bl]: | ||
| 34 | if bl == 'caterina' or bl == 'md-boot': | ||
| 35 | rules[bl].add(_udev_rule(vid_pid[0], vid_pid[1], 'ENV{ID_MM_DEVICE_IGNORE}="1"')) | ||
| 36 | else: | ||
| 37 | rules[bl].add(_udev_rule(vid_pid[0], vid_pid[1])) | ||
| 38 | return rules | ||
| 39 | |||
| 40 | |||
| 29 | def _deprecated_udev_rule(vid, pid=None): | 41 | def _deprecated_udev_rule(vid, pid=None): |
| 30 | """ Helper function that return udev rules | 42 | """ Helper function that return udev rules |
| 31 | 43 | ||
| @@ -47,47 +59,8 @@ def check_udev_rules(): | |||
| 47 | Path("/run/udev/rules.d/"), | 59 | Path("/run/udev/rules.d/"), |
| 48 | Path("/etc/udev/rules.d/"), | 60 | Path("/etc/udev/rules.d/"), |
| 49 | ] | 61 | ] |
| 50 | desired_rules = { | 62 | |
| 51 | 'atmel-dfu': { | 63 | desired_rules = _generate_desired_rules(BOOTLOADER_VIDS_PIDS) |
| 52 | _udev_rule("03eb", "2fef"), # ATmega16U2 | ||
| 53 | _udev_rule("03eb", "2ff0"), # ATmega32U2 | ||
| 54 | _udev_rule("03eb", "2ff3"), # ATmega16U4 | ||
| 55 | _udev_rule("03eb", "2ff4"), # ATmega32U4 | ||
| 56 | _udev_rule("03eb", "2ff9"), # AT90USB64 | ||
| 57 | _udev_rule("03eb", "2ffa"), # AT90USB162 | ||
| 58 | _udev_rule("03eb", "2ffb") # AT90USB128 | ||
| 59 | }, | ||
| 60 | 'kiibohd': {_udev_rule("1c11", "b007")}, | ||
| 61 | 'stm32': { | ||
| 62 | _udev_rule("1eaf", "0003"), # STM32duino | ||
| 63 | _udev_rule("0483", "df11") # STM32 DFU | ||
| 64 | }, | ||
| 65 | 'bootloadhid': {_udev_rule("16c0", "05df")}, | ||
| 66 | 'usbasploader': {_udev_rule("16c0", "05dc")}, | ||
| 67 | 'massdrop': {_udev_rule("03eb", "6124", 'ENV{ID_MM_DEVICE_IGNORE}="1"')}, | ||
| 68 | 'caterina': { | ||
| 69 | # Spark Fun Electronics | ||
| 70 | _udev_rule("1b4f", "9203", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 3V3/8MHz | ||
| 71 | _udev_rule("1b4f", "9205", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 5V/16MHz | ||
| 72 | _udev_rule("1b4f", "9207", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # LilyPad 3V3/8MHz (and some Pro Micro clones) | ||
| 73 | # Pololu Electronics | ||
| 74 | _udev_rule("1ffb", "0101", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # A-Star 32U4 | ||
| 75 | # Arduino SA | ||
| 76 | _udev_rule("2341", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo | ||
| 77 | _udev_rule("2341", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Micro | ||
| 78 | # Adafruit Industries LLC | ||
| 79 | _udev_rule("239a", "000c", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Feather 32U4 | ||
| 80 | _udev_rule("239a", "000d", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 3V3/8MHz | ||
| 81 | _udev_rule("239a", "000e", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 5V/16MHz | ||
| 82 | # dog hunter AG | ||
| 83 | _udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo | ||
| 84 | _udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro | ||
| 85 | }, | ||
| 86 | 'hid-bootloader': { | ||
| 87 | _udev_rule("03eb", "2067"), # QMK HID | ||
| 88 | _udev_rule("16c0", "0478") # PJRC halfkay | ||
| 89 | } | ||
| 90 | } | ||
| 91 | 64 | ||
| 92 | # These rules are no longer recommended, only use them to check for their presence. | 65 | # These rules are no longer recommended, only use them to check for their presence. |
| 93 | deprecated_rules = { | 66 | deprecated_rules = { |
diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py index ebe739c50e..c39f4b36d4 100644 --- a/lib/python/qmk/cli/flash.py +++ b/lib/python/qmk/cli/flash.py | |||
| @@ -4,6 +4,7 @@ You can compile a keymap already in the repo or using a QMK Configurator export. | |||
| 4 | A bootloader must be specified. | 4 | A bootloader must be specified. |
| 5 | """ | 5 | """ |
| 6 | from subprocess import DEVNULL | 6 | from subprocess import DEVNULL |
| 7 | import sys | ||
| 7 | 8 | ||
| 8 | from argcomplete.completers import FilesCompleter | 9 | from argcomplete.completers import FilesCompleter |
| 9 | from milc import cli | 10 | from milc import cli |
| @@ -12,6 +13,7 @@ import qmk.path | |||
| 12 | from qmk.decorators import automagic_keyboard, automagic_keymap | 13 | from qmk.decorators import automagic_keyboard, automagic_keymap |
| 13 | from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json | 14 | from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json |
| 14 | from qmk.keyboard import keyboard_completer, keyboard_folder | 15 | from qmk.keyboard import keyboard_completer, keyboard_folder |
| 16 | from qmk.flashers import flasher | ||
| 15 | 17 | ||
| 16 | 18 | ||
| 17 | def print_bootloader_help(): | 19 | def print_bootloader_help(): |
| @@ -38,9 +40,10 @@ def print_bootloader_help(): | |||
| 38 | cli.echo('For more info, visit https://docs.qmk.fm/#/flashing') | 40 | cli.echo('For more info, visit https://docs.qmk.fm/#/flashing') |
| 39 | 41 | ||
| 40 | 42 | ||
| 41 | @cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='The configurator export JSON to compile.') | 43 | @cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='A configurator export JSON to be compiled and flashed or a pre-compiled binary firmware file (bin/hex) to be flashed.') |
| 42 | @cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.') | 44 | @cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.') |
| 43 | @cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.') | 45 | @cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.') |
| 46 | @cli.argument('-m', '--mcu', help='The MCU name. Required for HalfKay, HID, USBAspLoader and ISP flashing.') | ||
| 44 | @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') | 47 | @cli.argument('-km', '--keymap', help='The keymap to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') |
| 45 | @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') | 48 | @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='The keyboard to build a firmware for. Use this if you dont have a configurator file. Ignored when a configurator file is supplied.') |
| 46 | @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") | 49 | @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.") |
| @@ -53,6 +56,8 @@ def print_bootloader_help(): | |||
| 53 | def flash(cli): | 56 | def flash(cli): |
| 54 | """Compile and or flash QMK Firmware or keyboard/layout | 57 | """Compile and or flash QMK Firmware or keyboard/layout |
| 55 | 58 | ||
| 59 | If a binary firmware is supplied, try to flash that. | ||
| 60 | |||
| 56 | If a Configurator JSON export is supplied this command will create a new keymap. Keymap and Keyboard arguments | 61 | If a Configurator JSON export is supplied this command will create a new keymap. Keymap and Keyboard arguments |
| 57 | will be ignored. | 62 | will be ignored. |
| 58 | 63 | ||
| @@ -60,56 +65,69 @@ def flash(cli): | |||
| 60 | 65 | ||
| 61 | If bootloader is omitted the make system will use the configured bootloader for that keyboard. | 66 | If bootloader is omitted the make system will use the configured bootloader for that keyboard. |
| 62 | """ | 67 | """ |
| 63 | if cli.args.clean and not cli.args.filename and not cli.args.dry_run: | 68 | if cli.args.filename and cli.args.filename.suffix in ['.bin', '.hex']: |
| 64 | if cli.config.flash.keyboard and cli.config.flash.keymap: | 69 | # Try to flash binary firmware |
| 65 | command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, 'clean') | 70 | cli.echo('Flashing binary firmware...\nPlease reset your keyboard into bootloader mode now!\nPress Ctrl-C to exit.\n') |
| 66 | cli.run(command, capture_output=False, stdin=DEVNULL) | 71 | try: |
| 67 | 72 | err, msg = flasher(cli.args.mcu, cli.args.filename) | |
| 68 | # Build the environment vars | 73 | if err: |
| 69 | envs = {} | 74 | cli.log.error(msg) |
| 70 | for env in cli.args.env: | 75 | return False |
| 71 | if '=' in env: | 76 | except KeyboardInterrupt: |
| 72 | key, value = env.split('=', 1) | 77 | cli.log.info('Ctrl-C was pressed, exiting...') |
| 73 | envs[key] = value | 78 | sys.exit(0) |
| 74 | else: | ||
| 75 | cli.log.warning('Invalid environment variable: %s', env) | ||
| 76 | |||
| 77 | # Determine the compile command | ||
| 78 | command = '' | ||
| 79 | |||
| 80 | if cli.args.bootloaders: | ||
| 81 | # Provide usage and list bootloaders | ||
| 82 | cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]') | ||
| 83 | print_bootloader_help() | ||
| 84 | return False | ||
| 85 | |||
| 86 | if cli.args.filename: | ||
| 87 | # Handle compiling a configurator JSON | ||
| 88 | user_keymap = parse_configurator_json(cli.args.filename) | ||
| 89 | keymap_path = qmk.path.keymap(user_keymap['keyboard']) | ||
| 90 | command = compile_configurator_json(user_keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs) | ||
| 91 | |||
| 92 | cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap']) | ||
| 93 | 79 | ||
| 94 | else: | 80 | else: |
| 95 | if cli.config.flash.keyboard and cli.config.flash.keymap: | 81 | if cli.args.clean and not cli.args.filename and not cli.args.dry_run: |
| 96 | # Generate the make command for a specific keyboard/keymap. | 82 | if cli.config.flash.keyboard and cli.config.flash.keymap: |
| 97 | command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs) | 83 | command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, 'clean') |
| 98 | 84 | cli.run(command, capture_output=False, stdin=DEVNULL) | |
| 99 | elif not cli.config.flash.keyboard: | 85 | |
| 100 | cli.log.error('Could not determine keyboard!') | 86 | # Build the environment vars |
| 101 | elif not cli.config.flash.keymap: | 87 | envs = {} |
| 102 | cli.log.error('Could not determine keymap!') | 88 | for env in cli.args.env: |
| 103 | 89 | if '=' in env: | |
| 104 | # Compile the firmware, if we're able to | 90 | key, value = env.split('=', 1) |
| 105 | if command: | 91 | envs[key] = value |
| 106 | cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command)) | 92 | else: |
| 107 | if not cli.args.dry_run: | 93 | cli.log.warning('Invalid environment variable: %s', env) |
| 108 | cli.echo('\n') | 94 | |
| 109 | compile = cli.run(command, capture_output=False, stdin=DEVNULL) | 95 | # Determine the compile command |
| 110 | return compile.returncode | 96 | command = '' |
| 97 | |||
| 98 | if cli.args.bootloaders: | ||
| 99 | # Provide usage and list bootloaders | ||
| 100 | cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]') | ||
| 101 | print_bootloader_help() | ||
| 102 | return False | ||
| 103 | |||
| 104 | if cli.args.filename: | ||
| 105 | # Handle compiling a configurator JSON | ||
| 106 | user_keymap = parse_configurator_json(cli.args.filename) | ||
| 107 | keymap_path = qmk.path.keymap(user_keymap['keyboard']) | ||
| 108 | command = compile_configurator_json(user_keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs) | ||
| 109 | |||
| 110 | cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap']) | ||
| 111 | 111 | ||
| 112 | else: | 112 | else: |
| 113 | cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') | 113 | if cli.config.flash.keyboard and cli.config.flash.keymap: |
| 114 | cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]') | 114 | # Generate the make command for a specific keyboard/keymap. |
| 115 | return False | 115 | command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs) |
| 116 | |||
| 117 | elif not cli.config.flash.keyboard: | ||
| 118 | cli.log.error('Could not determine keyboard!') | ||
| 119 | elif not cli.config.flash.keymap: | ||
| 120 | cli.log.error('Could not determine keymap!') | ||
| 121 | |||
| 122 | # Compile the firmware, if we're able to | ||
| 123 | if command: | ||
| 124 | cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command)) | ||
| 125 | if not cli.args.dry_run: | ||
| 126 | cli.echo('\n') | ||
| 127 | compile = cli.run(command, capture_output=False, stdin=DEVNULL) | ||
| 128 | return compile.returncode | ||
| 129 | |||
| 130 | else: | ||
| 131 | cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.') | ||
| 132 | cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]') | ||
| 133 | return False | ||
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 7da9df1d8a..10da5e7e8e 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py | |||
| @@ -64,6 +64,54 @@ LEGACY_KEYCODES = { # Comment here is to force multiline formatting | |||
| 64 | 'RESET': 'QK_BOOT' | 64 | 'RESET': 'QK_BOOT' |
| 65 | } | 65 | } |
| 66 | 66 | ||
| 67 | # Map VID:PID values to bootloaders | ||
| 68 | BOOTLOADER_VIDS_PIDS = { | ||
| 69 | 'atmel-dfu': { | ||
| 70 | ("03eb", "2fef"), # ATmega16U2 | ||
| 71 | ("03eb", "2ff0"), # ATmega32U2 | ||
| 72 | ("03eb", "2ff3"), # ATmega16U4 | ||
| 73 | ("03eb", "2ff4"), # ATmega32U4 | ||
| 74 | ("03eb", "2ff9"), # AT90USB64 | ||
| 75 | ("03eb", "2ffa"), # AT90USB162 | ||
| 76 | ("03eb", "2ffb") # AT90USB128 | ||
| 77 | }, | ||
| 78 | 'kiibohd': {("1c11", "b007")}, | ||
| 79 | 'stm32-dfu': { | ||
| 80 | ("1eaf", "0003"), # STM32duino | ||
| 81 | ("0483", "df11") # STM32 DFU | ||
| 82 | }, | ||
| 83 | 'apm32-dfu': {("314b", "0106")}, | ||
| 84 | 'gd32v-dfu': {("28e9", "0189")}, | ||
| 85 | 'bootloadhid': {("16c0", "05df")}, | ||
| 86 | 'usbasploader': {("16c0", "05dc")}, | ||
| 87 | 'usbtinyisp': {("1782", "0c9f")}, | ||
| 88 | 'md-boot': {("03eb", "6124")}, | ||
| 89 | 'caterina': { | ||
| 90 | # pid.codes shared PID | ||
| 91 | ("1209", "9203"), # Keyboardio Atreus 2 Bootloader | ||
| 92 | # Spark Fun Electronics | ||
| 93 | ("1b4f", "9203"), # Pro Micro 3V3/8MHz | ||
| 94 | ("1b4f", "9205"), # Pro Micro 5V/16MHz | ||
| 95 | ("1b4f", "9207"), # LilyPad 3V3/8MHz (and some Pro Micro clones) | ||
| 96 | # Pololu Electronics | ||
| 97 | ("1ffb", "0101"), # A-Star 32U4 | ||
| 98 | # Arduino SA | ||
| 99 | ("2341", "0036"), # Leonardo | ||
| 100 | ("2341", "0037"), # Micro | ||
| 101 | # Adafruit Industries LLC | ||
| 102 | ("239a", "000c"), # Feather 32U4 | ||
| 103 | ("239a", "000d"), # ItsyBitsy 32U4 3V3/8MHz | ||
| 104 | ("239a", "000e"), # ItsyBitsy 32U4 5V/16MHz | ||
| 105 | # dog hunter AG | ||
| 106 | ("2a03", "0036"), # Leonardo | ||
| 107 | ("2a03", "0037") # Micro | ||
| 108 | }, | ||
| 109 | 'hid-bootloader': { | ||
| 110 | ("03eb", "2067"), # QMK HID | ||
| 111 | ("16c0", "0478") # PJRC halfkay | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 67 | # Common format strings | 115 | # Common format strings |
| 68 | DATE_FORMAT = '%Y-%m-%d' | 116 | DATE_FORMAT = '%Y-%m-%d' |
| 69 | DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' | 117 | DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' |
diff --git a/lib/python/qmk/flashers.py b/lib/python/qmk/flashers.py new file mode 100644 index 0000000000..a9cf726b44 --- /dev/null +++ b/lib/python/qmk/flashers.py | |||
| @@ -0,0 +1,203 @@ | |||
| 1 | import shutil | ||
| 2 | import time | ||
| 3 | import os | ||
| 4 | import signal | ||
| 5 | |||
| 6 | import usb.core | ||
| 7 | |||
| 8 | from qmk.constants import BOOTLOADER_VIDS_PIDS | ||
| 9 | from milc import cli | ||
| 10 | |||
| 11 | # yapf: disable | ||
| 12 | _PID_TO_MCU = { | ||
| 13 | '2fef': 'atmega16u2', | ||
| 14 | '2ff0': 'atmega32u2', | ||
| 15 | '2ff3': 'atmega16u4', | ||
| 16 | '2ff4': 'atmega32u4', | ||
| 17 | '2ff9': 'at90usb64', | ||
| 18 | '2ffa': 'at90usb162', | ||
| 19 | '2ffb': 'at90usb128' | ||
| 20 | } | ||
| 21 | |||
| 22 | AVRDUDE_MCU = { | ||
| 23 | 'atmega32a': 'm32', | ||
| 24 | 'atmega328p': 'm328p', | ||
| 25 | 'atmega328': 'm328', | ||
| 26 | } | ||
| 27 | # yapf: enable | ||
| 28 | |||
| 29 | |||
| 30 | class DelayedKeyboardInterrupt: | ||
| 31 | # Custom interrupt handler to delay the processing of Ctrl-C | ||
| 32 | # https://stackoverflow.com/a/21919644 | ||
| 33 | def __enter__(self): | ||
| 34 | self.signal_received = False | ||
| 35 | self.old_handler = signal.signal(signal.SIGINT, self.handler) | ||
| 36 | |||
| 37 | def handler(self, sig, frame): | ||
| 38 | self.signal_received = (sig, frame) | ||
| 39 | |||
| 40 | def __exit__(self, type, value, traceback): | ||
| 41 | signal.signal(signal.SIGINT, self.old_handler) | ||
| 42 | if self.signal_received: | ||
| 43 | self.old_handler(*self.signal_received) | ||
| 44 | |||
| 45 | |||
| 46 | # TODO: Make this more generic, so cli/doctor/check.py and flashers.py can share the code | ||
| 47 | def _check_dfu_programmer_version(): | ||
| 48 | # Return True if version is higher than 0.7.0: supports '--force' | ||
| 49 | check = cli.run(['dfu-programmer', '--version'], combined_output=True, timeout=5) | ||
| 50 | first_line = check.stdout.split('\n')[0] | ||
| 51 | version_number = first_line.split()[1] | ||
| 52 | maj, min_, bug = version_number.split('.') | ||
| 53 | if int(maj) >= 0 and int(min_) >= 7: | ||
| 54 | return True | ||
| 55 | else: | ||
| 56 | return False | ||
| 57 | |||
| 58 | |||
| 59 | def _find_bootloader(): | ||
| 60 | # To avoid running forever in the background, only look for bootloaders for 10min | ||
| 61 | start_time = time.time() | ||
| 62 | while time.time() - start_time < 600: | ||
| 63 | for bl in BOOTLOADER_VIDS_PIDS: | ||
| 64 | for vid, pid in BOOTLOADER_VIDS_PIDS[bl]: | ||
| 65 | vid_hex = int(f'0x{vid}', 0) | ||
| 66 | pid_hex = int(f'0x{pid}', 0) | ||
| 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 | dev = usb.core.find(idVendor=vid_hex, idProduct=pid_hex) | ||
| 72 | if dev: | ||
| 73 | if bl == 'atmel-dfu': | ||
| 74 | details = _PID_TO_MCU[pid] | ||
| 75 | elif bl == 'caterina': | ||
| 76 | details = (vid_hex, pid_hex) | ||
| 77 | elif bl == 'hid-bootloader': | ||
| 78 | if vid == '16c0' and pid == '0478': | ||
| 79 | details = 'halfkay' | ||
| 80 | else: | ||
| 81 | details = 'qmk-hid' | ||
| 82 | elif bl == 'stm32-dfu' or bl == 'apm32-dfu' or bl == 'gd32v-dfu' or bl == 'kiibohd': | ||
| 83 | details = (vid, pid) | ||
| 84 | else: | ||
| 85 | details = None | ||
| 86 | return (bl, details) | ||
| 87 | time.sleep(0.1) | ||
| 88 | return (None, None) | ||
| 89 | |||
| 90 | |||
| 91 | def _find_serial_port(vid, pid): | ||
| 92 | if 'windows' in cli.platform.lower(): | ||
| 93 | from serial.tools.list_ports_windows import comports | ||
| 94 | platform = 'windows' | ||
| 95 | else: | ||
| 96 | from serial.tools.list_ports_posix import comports | ||
| 97 | platform = 'posix' | ||
| 98 | |||
| 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 | |||
| 119 | |||
| 120 | def _flash_caterina(details, file): | ||
| 121 | port = _find_serial_port(details[0], details[1]) | ||
| 122 | if port: | ||
| 123 | cli.run(['avrdude', '-p', 'atmega32u4', '-c', 'avr109', '-U', f'flash:w:{file}:i', '-P', port], capture_output=False) | ||
| 124 | return False | ||
| 125 | else: | ||
| 126 | return True | ||
| 127 | |||
| 128 | |||
| 129 | def _flash_atmel_dfu(mcu, file): | ||
| 130 | force = '--force' if _check_dfu_programmer_version() else '' | ||
| 131 | cli.run(['dfu-programmer', mcu, 'erase', force], capture_output=False) | ||
| 132 | cli.run(['dfu-programmer', mcu, 'flash', force, file], capture_output=False) | ||
| 133 | cli.run(['dfu-programmer', mcu, 'reset'], capture_output=False) | ||
| 134 | |||
| 135 | |||
| 136 | def _flash_hid_bootloader(mcu, details, file): | ||
| 137 | if details == 'halfkay': | ||
| 138 | if shutil.which('teensy-loader-cli'): | ||
| 139 | cmd = 'teensy-loader-cli' | ||
| 140 | elif shutil.which('teensy_loader_cli'): | ||
| 141 | cmd = 'teensy_loader_cli' | ||
| 142 | |||
| 143 | # Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay | ||
| 144 | if not cmd: | ||
| 145 | if shutil.which('hid_bootloader_cli'): | ||
| 146 | cmd = 'hid_bootloader_cli' | ||
| 147 | else: | ||
| 148 | return True | ||
| 149 | |||
| 150 | cli.run([cmd, f'-mmcu={mcu}', '-w', '-v', file], capture_output=False) | ||
| 151 | |||
| 152 | |||
| 153 | def _flash_dfu_util(details, file): | ||
| 154 | # STM32duino | ||
| 155 | if details[0] == '1eaf' and details[1] == '0003': | ||
| 156 | cli.run(['dfu-util', '-a', '2', '-d', f'{details[0]}:{details[1]}', '-R', '-D', file], capture_output=False) | ||
| 157 | # kiibohd | ||
| 158 | elif details[0] == '1c11' and details[1] == 'b007': | ||
| 159 | cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-D', file], capture_output=False) | ||
| 160 | # STM32, APM32, or GD32V DFU | ||
| 161 | else: | ||
| 162 | cli.run(['dfu-util', '-a', '0', '-d', f'{details[0]}:{details[1]}', '-s', '0x08000000:leave', '-D', file], capture_output=False) | ||
| 163 | |||
| 164 | |||
| 165 | def _flash_isp(mcu, programmer, file): | ||
| 166 | programmer = 'usbasp' if programmer == 'usbasploader' else 'usbtiny' | ||
| 167 | # Check if the provide mcu has an avrdude-specific name, otherwise pass on what the user provided | ||
| 168 | mcu = AVRDUDE_MCU.get(mcu, mcu) | ||
| 169 | cli.run(['avrdude', '-p', mcu, '-c', programmer, '-U', f'flash:w:{file}:i'], capture_output=False) | ||
| 170 | |||
| 171 | |||
| 172 | def _flash_mdloader(file): | ||
| 173 | cli.run(['mdloader', '--first', '--download', file, '--restart'], capture_output=False) | ||
| 174 | |||
| 175 | |||
| 176 | def flasher(mcu, file): | ||
| 177 | bl, details = _find_bootloader() | ||
| 178 | # Add a small sleep to avoid race conditions | ||
| 179 | time.sleep(1) | ||
| 180 | if bl == 'atmel-dfu': | ||
| 181 | _flash_atmel_dfu(details, file.name) | ||
| 182 | elif bl == 'caterina': | ||
| 183 | if _flash_caterina(details, file.name): | ||
| 184 | return (True, "The Caterina bootloader was found but is not writable. Check 'qmk doctor' output for advice.") | ||
| 185 | elif bl == 'hid-bootloader': | ||
| 186 | if mcu: | ||
| 187 | if _flash_hid_bootloader(mcu, details, file.name): | ||
| 188 | return (True, "Please make sure 'teensy_loader_cli' or 'hid_bootloader_cli' is available on your system.") | ||
| 189 | else: | ||
| 190 | return (True, "Specifying the MCU with '-m' is necessary for HalfKay/HID bootloaders!") | ||
| 191 | elif bl == 'stm32-dfu' or bl == 'apm32-dfu' or bl == 'gd32v-dfu' or bl == 'kiibohd': | ||
| 192 | _flash_dfu_util(details, file.name) | ||
| 193 | elif bl == 'usbasploader' or bl == 'usbtinyisp': | ||
| 194 | if mcu: | ||
| 195 | _flash_isp(mcu, bl, file.name) | ||
| 196 | else: | ||
| 197 | return (True, "Specifying the MCU with '-m' is necessary for ISP flashing!") | ||
| 198 | elif bl == 'md-boot': | ||
| 199 | _flash_mdloader(file.name) | ||
| 200 | else: | ||
| 201 | return (True, "Known bootloader found but flashing not currently supported!") | ||
| 202 | |||
| 203 | return (False, None) | ||
