summaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
authorErovia <Erovia@users.noreply.github.com>2022-08-20 06:39:19 +0100
committerGitHub <noreply@github.com>2022-08-20 15:39:19 +1000
commit5e2ffe7d8f4187109514147469d7db93e075f6f0 (patch)
tree83552a38351b50f6ab175c6abd6070abcc3d78a0 /lib/python
parent3bf36e8b04be5af9826fe3241d227efb185a4a31 (diff)
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>
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/doctor/linux.py57
-rw-r--r--lib/python/qmk/cli/flash.py120
-rw-r--r--lib/python/qmk/constants.py48
-rw-r--r--lib/python/qmk/flashers.py203
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
15import_names = { 15import_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
7from milc import cli 7from milc import cli
8 8
9from qmk.constants import QMK_FIRMWARE 9from qmk.constants import QMK_FIRMWARE, BOOTLOADER_VIDS_PIDS
10from .check import CheckStatus 10from .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
29def _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
29def _deprecated_udev_rule(vid, pid=None): 41def _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.
4A bootloader must be specified. 4A bootloader must be specified.
5""" 5"""
6from subprocess import DEVNULL 6from subprocess import DEVNULL
7import sys
7 8
8from argcomplete.completers import FilesCompleter 9from argcomplete.completers import FilesCompleter
9from milc import cli 10from milc import cli
@@ -12,6 +13,7 @@ import qmk.path
12from qmk.decorators import automagic_keyboard, automagic_keymap 13from qmk.decorators import automagic_keyboard, automagic_keymap
13from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json 14from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
14from qmk.keyboard import keyboard_completer, keyboard_folder 15from qmk.keyboard import keyboard_completer, keyboard_folder
16from qmk.flashers import flasher
15 17
16 18
17def print_bootloader_help(): 19def 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():
53def flash(cli): 56def 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
68BOOTLOADER_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
68DATE_FORMAT = '%Y-%m-%d' 116DATE_FORMAT = '%Y-%m-%d'
69DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' 117DATETIME_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 @@
1import shutil
2import time
3import os
4import signal
5
6import usb.core
7
8from qmk.constants import BOOTLOADER_VIDS_PIDS
9from 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
22AVRDUDE_MCU = {
23 'atmega32a': 'm32',
24 'atmega328p': 'm328p',
25 'atmega328': 'm328',
26}
27# yapf: enable
28
29
30class 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
47def _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
59def _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
91def _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
120def _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
129def _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
136def _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
153def _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
165def _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
172def _flash_mdloader(file):
173 cli.run(['mdloader', '--first', '--download', file, '--restart'], capture_output=False)
174
175
176def 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)