Skip to content

Commit

Permalink
CLI: Teaching the CLI to flash binaries (#16584)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
5 people authored Aug 20, 2022
1 parent 3bf36e8 commit 5e2ffe7
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 93 deletions.
17 changes: 17 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 can also flash binary firmware files (hex or bin) such as the ones produced by [Configurator](https://config.qmk.fm).

**Usage for Configurator Exports**:

```
Expand All @@ -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>]
```

**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**

```
Expand Down
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import_names = {
# A mapping of package name to importable name
'pep8-naming': 'pep8ext_naming',
'pyserial': 'serial',
'pyusb': 'usb.core',
'qmk-dotty-dict': 'dotty_dict',
'pillow': 'PIL'
Expand Down
57 changes: 15 additions & 42 deletions lib/python/qmk/cli/doctor/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from milc import cli

from qmk.constants import QMK_FIRMWARE
from qmk.constants import QMK_FIRMWARE, BOOTLOADER_VIDS_PIDS
from .check import CheckStatus


Expand All @@ -26,6 +26,18 @@ def _udev_rule(vid, pid=None, *args):
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):
""" Helper function that return udev rules
Expand All @@ -47,47 +59,8 @@ def check_udev_rules():
Path("/run/udev/rules.d/"),
Path("/etc/udev/rules.d/"),
]
desired_rules = {
'atmel-dfu': {
_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
}
}

desired_rules = _generate_desired_rules(BOOTLOADER_VIDS_PIDS)

# These rules are no longer recommended, only use them to check for their presence.
deprecated_rules = {
Expand Down
120 changes: 69 additions & 51 deletions lib/python/qmk/cli/flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
A bootloader must be specified.
"""
from subprocess import DEVNULL
import sys

from argcomplete.completers import FilesCompleter
from milc import cli
Expand All @@ -12,6 +13,7 @@
from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.commands import compile_configurator_json, create_make_command, parse_configurator_json
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.flashers import flasher


def print_bootloader_help():
Expand All @@ -38,9 +40,10 @@ def print_bootloader_help():
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('-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('-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.")
Expand All @@ -53,63 +56,78 @@ def print_bootloader_help():
def flash(cli):
"""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
will be ignored.
If no file is supplied, keymap and keyboard are expected.
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.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'])
if cli.args.filename and cli.args.filename.suffix in ['.bin', '.hex']:
# Try to flash binary firmware
cli.echo('Flashing binary firmware...\nPlease reset your keyboard into bootloader mode now!\nPress Ctrl-C to exit.\n')
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.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
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'])

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
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
48 changes: 48 additions & 0 deletions lib/python/qmk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,54 @@
'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
DATE_FORMAT = '%Y-%m-%d'
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'
Expand Down
Loading

0 comments on commit 5e2ffe7

Please sign in to comment.