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
|
@ -90,6 +90,8 @@ This command is similar to `qmk compile`, but can also target a bootloader. The
|
||||||
|
|
||||||
This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
|
This command is directory aware. It will automatically fill in KEYBOARD and/or KEYMAP if you are in a keyboard or keymap directory.
|
||||||
|
|
||||||
|
This command can also flash binary firmware files (hex or bin) such as the ones produced by [Configurator](https://config.qmk.fm).
|
||||||
|
|
||||||
**Usage for Configurator Exports**:
|
**Usage for Configurator Exports**:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -102,6 +104,21 @@ qmk flash [-bl <bootloader>] [-c] [-e <var>=<value>] [-j <num_jobs>] <configurat
|
||||||
qmk flash -kb <keyboard_name> -km <keymap_name> [-bl <bootloader>] [-c] [-e <var>=<value>] [-j <num_jobs>]
|
qmk flash -kb <keyboard_name> -km <keymap_name> [-bl <bootloader>] [-c] [-e <var>=<value>] [-j <num_jobs>]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Usage for pre-compiled firmwares**:
|
||||||
|
|
||||||
|
**Note**: The microcontroller needs to be specified (`-m` argument) for keyboards with the following bootloaders:
|
||||||
|
* HalfKay
|
||||||
|
* QMK HID
|
||||||
|
* USBaspLoader
|
||||||
|
|
||||||
|
ISP flashing is also supported with the following flashers and require the microcontroller to be specified:
|
||||||
|
* USBasp
|
||||||
|
* USBtinyISP
|
||||||
|
|
||||||
|
```
|
||||||
|
qmk flash [-m <microcontroller>] <compiledFirmware.[bin|hex]>
|
||||||
|
```
|
||||||
|
|
||||||
**Listing the Bootloaders**
|
**Listing the Bootloaders**
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -15,6 +15,7 @@ from milc.questions import yesno
|
||||||
import_names = {
|
import_names = {
|
||||||
# A mapping of package name to importable name
|
# A mapping of package name to importable name
|
||||||
'pep8-naming': 'pep8ext_naming',
|
'pep8-naming': 'pep8ext_naming',
|
||||||
|
'pyserial': 'serial',
|
||||||
'pyusb': 'usb.core',
|
'pyusb': 'usb.core',
|
||||||
'qmk-dotty-dict': 'dotty_dict',
|
'qmk-dotty-dict': 'dotty_dict',
|
||||||
'pillow': 'PIL'
|
'pillow': 'PIL'
|
||||||
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from milc import cli
|
from milc import cli
|
||||||
|
|
||||||
from qmk.constants import QMK_FIRMWARE
|
from qmk.constants import QMK_FIRMWARE, BOOTLOADER_VIDS_PIDS
|
||||||
from .check import CheckStatus
|
from .check import CheckStatus
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,18 @@ def _udev_rule(vid, pid=None, *args):
|
||||||
return rule
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_desired_rules(bootloader_vids_pids):
|
||||||
|
rules = dict()
|
||||||
|
for bl in bootloader_vids_pids.keys():
|
||||||
|
rules[bl] = set()
|
||||||
|
for vid_pid in bootloader_vids_pids[bl]:
|
||||||
|
if bl == 'caterina' or bl == 'md-boot':
|
||||||
|
rules[bl].add(_udev_rule(vid_pid[0], vid_pid[1], 'ENV{ID_MM_DEVICE_IGNORE}="1"'))
|
||||||
|
else:
|
||||||
|
rules[bl].add(_udev_rule(vid_pid[0], vid_pid[1]))
|
||||||
|
return rules
|
||||||
|
|
||||||
|
|
||||||
def _deprecated_udev_rule(vid, pid=None):
|
def _deprecated_udev_rule(vid, pid=None):
|
||||||
""" Helper function that return udev rules
|
""" Helper function that return udev rules
|
||||||
|
|
||||||
|
@ -47,47 +59,8 @@ def check_udev_rules():
|
||||||
Path("/run/udev/rules.d/"),
|
Path("/run/udev/rules.d/"),
|
||||||
Path("/etc/udev/rules.d/"),
|
Path("/etc/udev/rules.d/"),
|
||||||
]
|
]
|
||||||
desired_rules = {
|
|
||||||
'atmel-dfu': {
|
desired_rules = _generate_desired_rules(BOOTLOADER_VIDS_PIDS)
|
||||||
_udev_rule("03eb", "2fef"), # ATmega16U2
|
|
||||||
_udev_rule("03eb", "2ff0"), # ATmega32U2
|
|
||||||
_udev_rule("03eb", "2ff3"), # ATmega16U4
|
|
||||||
_udev_rule("03eb", "2ff4"), # ATmega32U4
|
|
||||||
_udev_rule("03eb", "2ff9"), # AT90USB64
|
|
||||||
_udev_rule("03eb", "2ffa"), # AT90USB162
|
|
||||||
_udev_rule("03eb", "2ffb") # AT90USB128
|
|
||||||
},
|
|
||||||
'kiibohd': {_udev_rule("1c11", "b007")},
|
|
||||||
'stm32': {
|
|
||||||
_udev_rule("1eaf", "0003"), # STM32duino
|
|
||||||
_udev_rule("0483", "df11") # STM32 DFU
|
|
||||||
},
|
|
||||||
'bootloadhid': {_udev_rule("16c0", "05df")},
|
|
||||||
'usbasploader': {_udev_rule("16c0", "05dc")},
|
|
||||||
'massdrop': {_udev_rule("03eb", "6124", 'ENV{ID_MM_DEVICE_IGNORE}="1"')},
|
|
||||||
'caterina': {
|
|
||||||
# Spark Fun Electronics
|
|
||||||
_udev_rule("1b4f", "9203", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 3V3/8MHz
|
|
||||||
_udev_rule("1b4f", "9205", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Pro Micro 5V/16MHz
|
|
||||||
_udev_rule("1b4f", "9207", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # LilyPad 3V3/8MHz (and some Pro Micro clones)
|
|
||||||
# Pololu Electronics
|
|
||||||
_udev_rule("1ffb", "0101", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # A-Star 32U4
|
|
||||||
# Arduino SA
|
|
||||||
_udev_rule("2341", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
|
|
||||||
_udev_rule("2341", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Micro
|
|
||||||
# Adafruit Industries LLC
|
|
||||||
_udev_rule("239a", "000c", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Feather 32U4
|
|
||||||
_udev_rule("239a", "000d", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 3V3/8MHz
|
|
||||||
_udev_rule("239a", "000e", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # ItsyBitsy 32U4 5V/16MHz
|
|
||||||
# dog hunter AG
|
|
||||||
_udev_rule("2a03", "0036", 'ENV{ID_MM_DEVICE_IGNORE}="1"'), # Leonardo
|
|
||||||
_udev_rule("2a03", "0037", 'ENV{ID_MM_DEVICE_IGNORE}="1"') # Micro
|
|
||||||
},
|
|
||||||
'hid-bootloader': {
|
|
||||||
_udev_rule("03eb", "2067"), # QMK HID
|
|
||||||
_udev_rule("16c0", "0478") # PJRC halfkay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# These rules are no longer recommended, only use them to check for their presence.
|
# These rules are no longer recommended, only use them to check for their presence.
|
||||||
deprecated_rules = {
|
deprecated_rules = {
|
||||||
|
|
|
@ -4,6 +4,7 @@ You can compile a keymap already in the repo or using a QMK Configurator export.
|
||||||
A bootloader must be specified.
|
A bootloader must be specified.
|
||||||
"""
|
"""
|
||||||
from subprocess import DEVNULL
|
from subprocess import DEVNULL
|
||||||
|
import sys
|
||||||
|
|
||||||
from argcomplete.completers import FilesCompleter
|
from argcomplete.completers import FilesCompleter
|
||||||
from milc import cli
|
from milc import cli
|
||||||
|
@ -12,6 +13,7 @@ import qmk.path
|
||||||
from qmk.decorators import automagic_keyboard, automagic_keymap
|
from qmk.decorators import automagic_keyboard, automagic_keymap
|
||||||
from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
|
from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
|
||||||
from qmk.keyboard import keyboard_completer, keyboard_folder
|
from qmk.keyboard import keyboard_completer, keyboard_folder
|
||||||
|
from qmk.flashers import flasher
|
||||||
|
|
||||||
|
|
||||||
def print_bootloader_help():
|
def print_bootloader_help():
|
||||||
|
@ -38,9 +40,10 @@ def print_bootloader_help():
|
||||||
cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
|
cli.echo('For more info, visit https://docs.qmk.fm/#/flashing')
|
||||||
|
|
||||||
|
|
||||||
@cli.argument('filename', nargs='?', arg_only=True, type=qmk.path.FileType('r'), completer=FilesCompleter('.json'), help='The configurator export JSON to compile.')
|
@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.')
|
||||||
@cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.')
|
@cli.argument('-b', '--bootloaders', action='store_true', help='List the available bootloaders.')
|
||||||
@cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.')
|
@cli.argument('-bl', '--bootloader', default='flash', help='The flash command, corresponding to qmk\'s make options of bootloaders.')
|
||||||
|
@cli.argument('-m', '--mcu', help='The MCU name. Required for HalfKay, HID, USBAspLoader and ISP flashing.')
|
||||||
@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.')
|
@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.')
|
||||||
@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.')
|
@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.')
|
||||||
@cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't actually build, just show the make command to be run.")
|
@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():
|
||||||
def flash(cli):
|
def flash(cli):
|
||||||
"""Compile and or flash QMK Firmware or keyboard/layout
|
"""Compile and or flash QMK Firmware or keyboard/layout
|
||||||
|
|
||||||
|
If a binary firmware is supplied, try to flash that.
|
||||||
|
|
||||||
If a Configurator JSON export is supplied this command will create a new keymap. Keymap and Keyboard arguments
|
If a Configurator JSON export is supplied this command will create a new keymap. Keymap and Keyboard arguments
|
||||||
will be ignored.
|
will be ignored.
|
||||||
|
|
||||||
|
@ -60,56 +65,69 @@ def flash(cli):
|
||||||
|
|
||||||
If bootloader is omitted the make system will use the configured bootloader for that keyboard.
|
If bootloader is omitted the make system will use the configured bootloader for that keyboard.
|
||||||
"""
|
"""
|
||||||
if cli.args.clean and not cli.args.filename and not cli.args.dry_run:
|
if cli.args.filename and cli.args.filename.suffix in ['.bin', '.hex']:
|
||||||
if cli.config.flash.keyboard and cli.config.flash.keymap:
|
# Try to flash binary firmware
|
||||||
command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, 'clean')
|
cli.echo('Flashing binary firmware...\nPlease reset your keyboard into bootloader mode now!\nPress Ctrl-C to exit.\n')
|
||||||
cli.run(command, capture_output=False, stdin=DEVNULL)
|
try:
|
||||||
|
err, msg = flasher(cli.args.mcu, cli.args.filename)
|
||||||
|
if err:
|
||||||
|
cli.log.error(msg)
|
||||||
|
return False
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
cli.log.info('Ctrl-C was pressed, exiting...')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if cli.args.clean and not cli.args.filename and not cli.args.dry_run:
|
||||||
|
if cli.config.flash.keyboard and cli.config.flash.keymap:
|
||||||
|
command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, 'clean')
|
||||||
|
cli.run(command, capture_output=False, stdin=DEVNULL)
|
||||||
|
|
||||||
|
# Build the environment vars
|
||||||
|
envs = {}
|
||||||
|
for env in cli.args.env:
|
||||||
|
if '=' in env:
|
||||||
|
key, value = env.split('=', 1)
|
||||||
|
envs[key] = value
|
||||||
|
else:
|
||||||
|
cli.log.warning('Invalid environment variable: %s', env)
|
||||||
|
|
||||||
|
# Determine the compile command
|
||||||
|
command = ''
|
||||||
|
|
||||||
|
if cli.args.bootloaders:
|
||||||
|
# Provide usage and list bootloaders
|
||||||
|
cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
|
||||||
|
print_bootloader_help()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if cli.args.filename:
|
||||||
|
# Handle compiling a configurator JSON
|
||||||
|
user_keymap = parse_configurator_json(cli.args.filename)
|
||||||
|
keymap_path = qmk.path.keymap(user_keymap['keyboard'])
|
||||||
|
command = compile_configurator_json(user_keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs)
|
||||||
|
|
||||||
|
cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
|
||||||
|
|
||||||
# Build the environment vars
|
|
||||||
envs = {}
|
|
||||||
for env in cli.args.env:
|
|
||||||
if '=' in env:
|
|
||||||
key, value = env.split('=', 1)
|
|
||||||
envs[key] = value
|
|
||||||
else:
|
else:
|
||||||
cli.log.warning('Invalid environment variable: %s', env)
|
if cli.config.flash.keyboard and cli.config.flash.keymap:
|
||||||
|
# Generate the make command for a specific keyboard/keymap.
|
||||||
|
command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs)
|
||||||
|
|
||||||
# Determine the compile command
|
elif not cli.config.flash.keyboard:
|
||||||
command = ''
|
cli.log.error('Could not determine keyboard!')
|
||||||
|
elif not cli.config.flash.keymap:
|
||||||
|
cli.log.error('Could not determine keymap!')
|
||||||
|
|
||||||
if cli.args.bootloaders:
|
# Compile the firmware, if we're able to
|
||||||
# Provide usage and list bootloaders
|
if command:
|
||||||
cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
|
cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
|
||||||
print_bootloader_help()
|
if not cli.args.dry_run:
|
||||||
return False
|
cli.echo('\n')
|
||||||
|
compile = cli.run(command, capture_output=False, stdin=DEVNULL)
|
||||||
|
return compile.returncode
|
||||||
|
|
||||||
if cli.args.filename:
|
else:
|
||||||
# Handle compiling a configurator JSON
|
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
|
||||||
user_keymap = parse_configurator_json(cli.args.filename)
|
cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
|
||||||
keymap_path = qmk.path.keymap(user_keymap['keyboard'])
|
return False
|
||||||
command = compile_configurator_json(user_keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs)
|
|
||||||
|
|
||||||
cli.log.info('Wrote keymap to {fg_cyan}%s/%s/keymap.c', keymap_path, user_keymap['keymap'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
if cli.config.flash.keyboard and cli.config.flash.keymap:
|
|
||||||
# Generate the make command for a specific keyboard/keymap.
|
|
||||||
command = create_make_command(cli.config.flash.keyboard, cli.config.flash.keymap, cli.args.bootloader, parallel=cli.config.flash.parallel, **envs)
|
|
||||||
|
|
||||||
elif not cli.config.flash.keyboard:
|
|
||||||
cli.log.error('Could not determine keyboard!')
|
|
||||||
elif not cli.config.flash.keymap:
|
|
||||||
cli.log.error('Could not determine keymap!')
|
|
||||||
|
|
||||||
# Compile the firmware, if we're able to
|
|
||||||
if command:
|
|
||||||
cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
|
|
||||||
if not cli.args.dry_run:
|
|
||||||
cli.echo('\n')
|
|
||||||
compile = cli.run(command, capture_output=False, stdin=DEVNULL)
|
|
||||||
return compile.returncode
|
|
||||||
|
|
||||||
else:
|
|
||||||
cli.log.error('You must supply a configurator export, both `--keyboard` and `--keymap`, or be in a directory for a keyboard or keymap.')
|
|
||||||
cli.echo('usage: qmk flash [-h] [-b] [-n] [-kb KEYBOARD] [-km KEYMAP] [-bl BOOTLOADER] [filename]')
|
|
||||||
return False
|
|
||||||
|
|
|
@ -64,6 +64,54 @@ LEGACY_KEYCODES = { # Comment here is to force multiline formatting
|
||||||
'RESET': 'QK_BOOT'
|
'RESET': 'QK_BOOT'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Map VID:PID values to bootloaders
|
||||||
|
BOOTLOADER_VIDS_PIDS = {
|
||||||
|
'atmel-dfu': {
|
||||||
|
("03eb", "2fef"), # ATmega16U2
|
||||||
|
("03eb", "2ff0"), # ATmega32U2
|
||||||
|
("03eb", "2ff3"), # ATmega16U4
|
||||||
|
("03eb", "2ff4"), # ATmega32U4
|
||||||
|
("03eb", "2ff9"), # AT90USB64
|
||||||
|
("03eb", "2ffa"), # AT90USB162
|
||||||
|
("03eb", "2ffb") # AT90USB128
|
||||||
|
},
|
||||||
|
'kiibohd': {("1c11", "b007")},
|
||||||
|
'stm32-dfu': {
|
||||||
|
("1eaf", "0003"), # STM32duino
|
||||||
|
("0483", "df11") # STM32 DFU
|
||||||
|
},
|
||||||
|
'apm32-dfu': {("314b", "0106")},
|
||||||
|
'gd32v-dfu': {("28e9", "0189")},
|
||||||
|
'bootloadhid': {("16c0", "05df")},
|
||||||
|
'usbasploader': {("16c0", "05dc")},
|
||||||
|
'usbtinyisp': {("1782", "0c9f")},
|
||||||
|
'md-boot': {("03eb", "6124")},
|
||||||
|
'caterina': {
|
||||||
|
# pid.codes shared PID
|
||||||
|
("1209", "9203"), # Keyboardio Atreus 2 Bootloader
|
||||||
|
# Spark Fun Electronics
|
||||||
|
("1b4f", "9203"), # Pro Micro 3V3/8MHz
|
||||||
|
("1b4f", "9205"), # Pro Micro 5V/16MHz
|
||||||
|
("1b4f", "9207"), # LilyPad 3V3/8MHz (and some Pro Micro clones)
|
||||||
|
# Pololu Electronics
|
||||||
|
("1ffb", "0101"), # A-Star 32U4
|
||||||
|
# Arduino SA
|
||||||
|
("2341", "0036"), # Leonardo
|
||||||
|
("2341", "0037"), # Micro
|
||||||
|
# Adafruit Industries LLC
|
||||||
|
("239a", "000c"), # Feather 32U4
|
||||||
|
("239a", "000d"), # ItsyBitsy 32U4 3V3/8MHz
|
||||||
|
("239a", "000e"), # ItsyBitsy 32U4 5V/16MHz
|
||||||
|
# dog hunter AG
|
||||||
|
("2a03", "0036"), # Leonardo
|
||||||
|
("2a03", "0037") # Micro
|
||||||
|
},
|
||||||
|
'hid-bootloader': {
|
||||||
|
("03eb", "2067"), # QMK HID
|
||||||
|
("16c0", "0478") # PJRC halfkay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Common format strings
|
# Common format strings
|
||||||
DATE_FORMAT = '%Y-%m-%d'
|
DATE_FORMAT = '%Y-%m-%d'
|
||||||
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
|
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
|
||||||
|
|
|
@ -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)
|
|
@ -7,6 +7,7 @@ hjson
|
||||||
jsonschema>=4
|
jsonschema>=4
|
||||||
milc>=1.4.2
|
milc>=1.4.2
|
||||||
pygments
|
pygments
|
||||||
|
pyserial
|
||||||
pyusb
|
pyusb
|
||||||
qmk-dotty-dict
|
qmk-dotty-dict
|
||||||
pillow
|
pillow
|
||||||
|
|
|
@ -28,6 +28,9 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05df", TAG+="uacc
|
||||||
# USBAspLoader
|
# USBAspLoader
|
||||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05dc", TAG+="uaccess"
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="05dc", TAG+="uaccess"
|
||||||
|
|
||||||
|
# USBtinyISP
|
||||||
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1782", ATTRS{idProduct}=="0c9f", TAG+="uaccess"
|
||||||
|
|
||||||
# ModemManager should ignore the following devices
|
# ModemManager should ignore the following devices
|
||||||
# Atmel SAM-BA (Massdrop)
|
# Atmel SAM-BA (Massdrop)
|
||||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="6124", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1"
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="6124", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1"
|
||||||
|
@ -72,3 +75,9 @@ KERNEL=="hidraw*", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl"
|
||||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2067", TAG+="uaccess"
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2067", TAG+="uaccess"
|
||||||
## PJRC's HalfKay
|
## PJRC's HalfKay
|
||||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="0478", TAG+="uaccess"
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="0478", TAG+="uaccess"
|
||||||
|
|
||||||
|
# APM32 DFU
|
||||||
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="314b", ATTRS{idProduct}=="0106", TAG+="uaccess"
|
||||||
|
|
||||||
|
# GD32V DFU
|
||||||
|
SUBSYSTEMS=="usb", ATTRS{idVendor}=="28e9", ATTRS{idProduct}=="0189", TAG+="uaccess"
|
||||||
|
|
Loading…
Reference in New Issue