diff options
Diffstat (limited to 'lib/python/qmk')
-rw-r--r-- | lib/python/qmk/c_parse.py | 3 | ||||
-rw-r--r-- | lib/python/qmk/cli/__init__.py | 4 | ||||
-rw-r--r-- | lib/python/qmk/cli/doctor/linux.py | 57 | ||||
-rw-r--r-- | lib/python/qmk/cli/doctor/macos.py | 2 | ||||
-rw-r--r-- | lib/python/qmk/cli/flash.py | 122 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/api.py | 37 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/config_h.py | 36 | ||||
-rw-r--r-- | lib/python/qmk/cli/generate/dfu_header.py | 4 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/info_json.py | 8 | ||||
-rw-r--r-- | lib/python/qmk/cli/import/__init__.py | 0 | ||||
-rw-r--r-- | lib/python/qmk/cli/import/kbfirmware.py | 25 | ||||
-rw-r--r-- | lib/python/qmk/cli/import/keyboard.py | 23 | ||||
-rw-r--r-- | lib/python/qmk/cli/import/keymap.py | 23 | ||||
-rw-r--r-- | lib/python/qmk/constants.py | 56 | ||||
-rw-r--r-- | lib/python/qmk/flashers.py | 203 | ||||
-rw-r--r-- | lib/python/qmk/git.py | 4 | ||||
-rw-r--r-- | lib/python/qmk/importers.py | 193 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 86 | ||||
-rw-r--r-- | lib/python/qmk/json_schema.py | 6 |
19 files changed, 748 insertions, 144 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 4b49b8d4e9..c14eb490fa 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -258,6 +258,9 @@ def _parse_led_config(file, matrix_cols, matrix_rows): position_raw.append(_coerce_led_token(_type, value)) if section == 3 and bracket_count == 2: flags.append(_coerce_led_token(_type, value)) + elif _type in [Token.Comment.Preproc]: + # TODO: Promote to error + return None # Slightly better intrim format matrix = list(_get_chunks(matrix_raw, matrix_cols)) diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 1e15c28758..98e212c47b 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -15,6 +15,7 @@ from milc.questions import yesno 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' @@ -59,6 +60,9 @@ subcommands = [ 'qmk.cli.generate.rules_mk', 'qmk.cli.generate.version_h', 'qmk.cli.hello', + 'qmk.cli.import.kbfirmware', + 'qmk.cli.import.keyboard', + 'qmk.cli.import.keymap', 'qmk.cli.info', 'qmk.cli.json2c', 'qmk.cli.lint', 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 from milc import cli -from qmk.constants import QMK_FIRMWARE +from qmk.constants import QMK_FIRMWARE, BOOTLOADER_VIDS_PIDS from .check import CheckStatus @@ -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 @@ -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 = { diff --git a/lib/python/qmk/cli/doctor/macos.py b/lib/python/qmk/cli/doctor/macos.py index 00fb272858..5d088c9492 100644 --- a/lib/python/qmk/cli/doctor/macos.py +++ b/lib/python/qmk/cli/doctor/macos.py @@ -8,6 +8,6 @@ from .check import CheckStatus def os_test_macos(): """Run the Mac specific tests. """ - cli.log.info("Detected {fg_cyan}macOS %s{fg_reset}.", platform.mac_ver()[0]) + cli.log.info("Detected {fg_cyan}macOS %s (%s){fg_reset}.", platform.mac_ver()[0], 'Apple Silicon' if platform.processor() == 'arm' else 'Intel') return CheckStatus.OK diff --git a/lib/python/qmk/cli/flash.py b/lib/python/qmk/cli/flash.py index 216896b974..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. A bootloader must be specified. """ from subprocess import DEVNULL +import sys from argcomplete.completers import FilesCompleter from milc import cli @@ -12,6 +13,7 @@ import qmk.path 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(): @@ -33,12 +35,15 @@ def print_bootloader_help(): cli.echo('\tdfu-split-right') cli.echo('\tdfu-util-split-left') cli.echo('\tdfu-util-split-right') + cli.echo('\tuf2-split-left') + cli.echo('\tuf2-split-right') 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.") @@ -51,6 +56,8 @@ 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. @@ -58,56 +65,69 @@ def flash(cli): 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 diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 0596b3f22b..8d8ca3cd41 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -12,21 +12,30 @@ from qmk.json_encoders import InfoJSONEncoder from qmk.json_schema import json_load from qmk.keyboard import find_readme, list_keyboards -TEMPLATE_PATH = Path('data/templates/api/') +DATA_PATH = Path('data') +TEMPLATE_PATH = DATA_PATH / 'templates/api/' BUILD_API_PATH = Path('.build/api_data/') +def _filtered_keyboard_list(): + """Perform basic filtering of list_keyboards + """ + keyboard_list = list_keyboards() + if cli.args.filter: + kb_list = [] + for keyboard_name in keyboard_list: + if any(i in keyboard_name for i in cli.args.filter): + kb_list.append(keyboard_name) + keyboard_list = kb_list + return keyboard_list + + @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't write the data to disk.") @cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on partial name matches the supplied value. May be passed multiple times.") -@cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True) +@cli.subcommand('Generate QMK API data', hidden=False if cli.config.user.developer else True) def generate_api(cli): """Generates the QMK API data. """ - if BUILD_API_PATH.exists(): - shutil.rmtree(BUILD_API_PATH) - - shutil.copytree(TEMPLATE_PATH, BUILD_API_PATH) - v1_dir = BUILD_API_PATH / 'v1' keyboard_all_file = v1_dir / 'keyboards.json' # A massive JSON containing everything keyboard_list_file = v1_dir / 'keyboard_list.json' # A simple list of keyboard targets @@ -34,14 +43,14 @@ def generate_api(cli): keyboard_metadata_file = v1_dir / 'keyboard_metadata.json' # All the data configurator/via needs for initialization usb_file = v1_dir / 'usb.json' # A mapping of USB VID/PID -> keyboard target + if BUILD_API_PATH.exists(): + shutil.rmtree(BUILD_API_PATH) + + shutil.copytree(TEMPLATE_PATH, BUILD_API_PATH) + shutil.copytree(DATA_PATH, v1_dir) + # Filter down when required - keyboard_list = list_keyboards() - if cli.args.filter: - kb_list = [] - for keyboard_name in keyboard_list: - if any(i in keyboard_name for i in cli.args.filter): - kb_list.append(keyboard_name) - keyboard_list = kb_list + keyboard_list = _filtered_keyboard_list() kb_all = {} usb_list = {} diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 893892c479..a26dcdf7d7 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -134,6 +134,36 @@ def generate_config_items(kb_info_json, config_h_lines): config_h_lines.append(f'#endif // {config_key}') +def generate_encoder_config(encoder_json, config_h_lines, postfix=''): + """Generate the config.h lines for encoders.""" + a_pads = [] + b_pads = [] + resolutions = [] + for encoder in encoder_json.get("rotary", []): + a_pads.append(encoder["pin_a"]) + b_pads.append(encoder["pin_b"]) + resolutions.append(encoder.get("resolution", None)) + + config_h_lines.append(f'#ifndef ENCODERS_PAD_A{postfix}') + config_h_lines.append(f'# define ENCODERS_PAD_A{postfix} {{ { ", ".join(a_pads) } }}') + config_h_lines.append(f'#endif // ENCODERS_PAD_A{postfix}') + + config_h_lines.append(f'#ifndef ENCODERS_PAD_B{postfix}') + config_h_lines.append(f'# define ENCODERS_PAD_B{postfix} {{ { ", ".join(b_pads) } }}') + config_h_lines.append(f'#endif // ENCODERS_PAD_B{postfix}') + + if None in resolutions: + cli.log.debug("Unable to generate ENCODER_RESOLUTION configuration") + elif len(set(resolutions)) == 1: + config_h_lines.append(f'#ifndef ENCODER_RESOLUTION{postfix}') + config_h_lines.append(f'# define ENCODER_RESOLUTION{postfix} { resolutions[0] }') + config_h_lines.append(f'#endif // ENCODER_RESOLUTION{postfix}') + else: + config_h_lines.append(f'#ifndef ENCODER_RESOLUTIONS{postfix}') + config_h_lines.append(f'# define ENCODER_RESOLUTIONS{postfix} {{ { ", ".join(map(str,resolutions)) } }}') + config_h_lines.append(f'#endif // ENCODER_RESOLUTIONS{postfix}') + + def generate_split_config(kb_info_json, config_h_lines): """Generate the config.h lines for split boards.""" if 'primary' in kb_info_json['split']: @@ -173,6 +203,9 @@ def generate_split_config(kb_info_json, config_h_lines): if 'right' in kb_info_json['split'].get('matrix_pins', {}): config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT')) + if 'right' in kb_info_json['split'].get('encoder', {}): + generate_encoder_config(kb_info_json['split']['encoder']['right'], config_h_lines, '_RIGHT') + @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @@ -198,6 +231,9 @@ def generate_config_h(cli): if 'matrix_pins' in kb_info_json: config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) + if 'encoder' in kb_info_json: + generate_encoder_config(kb_info_json['encoder'], config_h_lines) + if 'split' in kb_info_json: generate_split_config(kb_info_json, config_h_lines) diff --git a/lib/python/qmk/cli/generate/dfu_header.py b/lib/python/qmk/cli/generate/dfu_header.py index e873117387..aa0252ca86 100644 --- a/lib/python/qmk/cli/generate/dfu_header.py +++ b/lib/python/qmk/cli/generate/dfu_header.py @@ -33,8 +33,8 @@ def generate_dfu_header(cli): kb_info_json = dotty(info_json(cli.config.generate_dfu_header.keyboard)) keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once'] - keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}') - keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader') + keyboard_h_lines.append(f'#define MANUFACTURER "{kb_info_json["manufacturer"]}"') + keyboard_h_lines.append(f'#define PRODUCT "{kb_info_json["keyboard_name"]} Bootloader"') # Optional if 'qmk_lufa_bootloader.esc_output' in kb_info_json: diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 284d1a8510..0dc80f10cc 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -5,7 +5,7 @@ Compile an info.json for a particular keyboard and pretty-print it. import json from argcomplete.completers import FilesCompleter -from jsonschema import Draft7Validator, RefResolver, validators +from jsonschema import Draft202012Validator, RefResolver, validators from milc import cli from pathlib import Path @@ -18,7 +18,7 @@ from qmk.path import is_keyboard, normpath def pruning_validator(validator_class): - """Extends Draft7Validator to remove properties that aren't specified in the schema. + """Extends Draft202012Validator to remove properties that aren't specified in the schema. """ validate_properties = validator_class.VALIDATORS["properties"] @@ -37,10 +37,10 @@ def strip_info_json(kb_info_json): """Remove the API-only properties from the info.json. """ schema_store = compile_schema_store() - pruning_draft_7_validator = pruning_validator(Draft7Validator) + pruning_draft_validator = pruning_validator(Draft202012Validator) schema = schema_store['qmk.keyboard.v1'] resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store) - validator = pruning_draft_7_validator(schema, resolver=resolver).validate + validator = pruning_draft_validator(schema, resolver=resolver).validate return validator(kb_info_json) diff --git a/lib/python/qmk/cli/import/__init__.py b/lib/python/qmk/cli/import/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/python/qmk/cli/import/__init__.py diff --git a/lib/python/qmk/cli/import/kbfirmware.py b/lib/python/qmk/cli/import/kbfirmware.py new file mode 100644 index 0000000000..9c03737378 --- /dev/null +++ b/lib/python/qmk/cli/import/kbfirmware.py @@ -0,0 +1,25 @@ +from milc import cli + +from qmk.importers import import_kbfirmware as _import_kbfirmware +from qmk.path import FileType +from qmk.json_schema import json_load + + +@cli.argument('filename', type=FileType('r'), nargs='+', arg_only=True, help='file') +@cli.subcommand('Import kbfirmware json export') +def import_kbfirmware(cli): + filename = cli.args.filename[0] + + data = json_load(filename) + + cli.log.info(f'{{style_bright}}Importing {filename.name}.{{style_normal}}') + cli.echo('') + + cli.log.warn("Support here is basic - Consider using 'qmk new-keyboard' instead") + + kb_name = _import_kbfirmware(data) + + cli.log.info(f'{{fg_green}}Imported a new keyboard named {{fg_cyan}}{kb_name}{{fg_green}}.{{fg_reset}}') + cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}keyboards/{kb_name}{{fg_reset}},') + cli.log.info('or open the directory in your preferred text editor.') + cli.log.info(f"And build with {{fg_yellow}}qmk compile -kb {kb_name} -km default{{fg_reset}}.") diff --git a/lib/python/qmk/cli/import/keyboard.py b/lib/python/qmk/cli/import/keyboard.py new file mode 100644 index 0000000000..3a5ed37dee --- /dev/null +++ b/lib/python/qmk/cli/import/keyboard.py @@ -0,0 +1,23 @@ +from milc import cli + +from qmk.importers import import_keyboard as _import_keyboard +from qmk.path import FileType +from qmk.json_schema import json_load + + +@cli.argument('filename', type=FileType('r'), nargs='+', arg_only=True, help='file') +@cli.subcommand('Import data-driven keyboard') +def import_keyboard(cli): + filename = cli.args.filename[0] + + data = json_load(filename) + + cli.log.info(f'{{style_bright}}Importing {filename.name}.{{style_normal}}') + cli.echo('') + + kb_name = _import_keyboard(data) + + cli.log.info(f'{{fg_green}}Imported a new keyboard named {{fg_cyan}}{kb_name}{{fg_green}}.{{fg_reset}}') + cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}keyboards/{kb_name}{{fg_reset}},') + cli.log.info('or open the directory in your preferred text editor.') + cli.log.info(f"And build with {{fg_yellow}}qmk compile -kb {kb_name} -km default{{fg_reset}}.") diff --git a/lib/python/qmk/cli/import/keymap.py b/lib/python/qmk/cli/import/keymap.py new file mode 100644 index 0000000000..a499c93480 --- /dev/null +++ b/lib/python/qmk/cli/import/keymap.py @@ -0,0 +1,23 @@ +from milc import cli + +from qmk.importers import import_keymap as _import_keymap +from qmk.path import FileType +from qmk.json_schema import json_load + + +@cli.argument('filename', type=FileType('r'), nargs='+', arg_only=True, help='file') +@cli.subcommand('Import data-driven keymap') +def import_keymap(cli): + filename = cli.args.filename[0] + + data = json_load(filename) + + cli.log.info(f'{{style_bright}}Importing {filename.name}.{{style_normal}}') + cli.echo('') + + kb_name, km_name = _import_keymap(data) + + cli.log.info(f'{{fg_green}}Imported a new keymap named {{fg_cyan}}{km_name}{{fg_green}}.{{fg_reset}}') + cli.log.info(f'To start working on things, `cd` into {{fg_cyan}}keyboards/{kb_name}/keymaps/{km_name}{{fg_reset}},') + cli.log.info('or open the directory in your preferred text editor.') + cli.log.info(f"And build with {{fg_yellow}}qmk compile -kb {kb_name} -km {km_name}{{fg_reset}}.") diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index a54d9058bc..622199e46e 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -14,12 +14,13 @@ QMK_FIRMWARE_UPSTREAM = 'qmk/qmk_firmware' MAX_KEYBOARD_SUBFOLDERS = 5 # Supported processor types -CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66FX1M0', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F405', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L432', 'STM32L433', 'STM32L442', 'STM32L443', 'GD32VF103', 'WB32F3G71', 'WB32FQ95' +CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK64FX512', 'MK66FX1M0', 'RP2040', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F405', 'STM32F407', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L412', 'STM32L422', 'STM32L432', 'STM32L433', 'STM32L442', 'STM32L443', 'GD32VF103', 'WB32F3G71', 'WB32FQ95' LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' # Bootloaders of the supported processors MCU2BOOTLOADER = { + "RP2040": "rp2040", "MKL26Z64": "halfkay", "MK20DX128": "halfkay", "MK20DX256": "halfkay", @@ -58,6 +59,59 @@ MCU2BOOTLOADER = { "atmega328": "usbasploader", } +# Map of legacy keycodes that can be automatically updated +LEGACY_KEYCODES = { # Comment here is to force multiline formatting + '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", "2302"), # 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' 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 @@ +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) diff --git a/lib/python/qmk/git.py b/lib/python/qmk/git.py index 5d09d816df..7fa0306f5c 100644 --- a/lib/python/qmk/git.py +++ b/lib/python/qmk/git.py @@ -130,9 +130,9 @@ def git_check_deviation(active_branch): def git_get_ignored_files(check_dir='.'): - """Return a list of files that would be captured by the current .gitingore + """Return a list of files that would be captured by the current .gitignore """ - invalid = cli.run(['git', 'ls-files', '-c', '-o', '-i', '--exclude-standard', check_dir]) + invalid = cli.run(['git', 'ls-files', '-c', '-o', '-i', '--exclude-from=.gitignore', check_dir]) if invalid.returncode != 0: return [] return invalid.stdout.strip().splitlines() diff --git a/lib/python/qmk/importers.py b/lib/python/qmk/importers.py new file mode 100644 index 0000000000..307c66ee3c --- /dev/null +++ b/lib/python/qmk/importers.py @@ -0,0 +1,193 @@ +from dotty_dict import dotty +from datetime import date +from pathlib import Path +import json + +from qmk.git import git_get_username +from qmk.json_schema import validate +from qmk.path import keyboard, keymap +from qmk.constants import MCU2BOOTLOADER, LEGACY_KEYCODES +from qmk.json_encoders import InfoJSONEncoder, KeymapJSONEncoder +from qmk.json_schema import deep_update, json_load + +TEMPLATE = Path('data/templates/keyboard/') + + +def replace_placeholders(src, dest, tokens): + """Replaces the given placeholders in each template file. + """ + content = src.read_text() + for key, value in tokens.items(): + content = content.replace(f'%{key}%', value) + + dest.write_text(content) + + +def _gen_dummy_keymap(name, info_data): + # Pick the first layout macro and just dump in KC_NOs or something? + (layout_name, layout_data), *_ = info_data["layouts"].items() + layout_length = len(layout_data["layout"]) + + keymap_data = { + "keyboard": name, + "layout": layout_name, + "layers": [["KC_NO" for _ in range(0, layout_length)]], + } + + return keymap_data + + +def _extract_kbfirmware_layout(kbf_data): + layout = [] + for key in kbf_data['keyboard.keys']: + item = { + 'matrix': [key['row'], key['col']], + 'x': key['state']['x'], + 'y': key['state']['y'], + } + if key['state']['w'] != 1: + item['w'] = key['state']['w'] + if key['state']['h'] != 1: + item['h'] = key['state']['h'] + layout.append(item) + + return layout + + +def _extract_kbfirmware_keymap(kbf_data): + keymap_data = { + 'keyboard': kbf_data['keyboard.settings.name'].lower(), + 'layout': 'LAYOUT', + 'layers': [], + } + + for i in range(15): + layer = [] + for key in kbf_data['keyboard.keys']: + keycode = key['keycodes'][i]['id'] + keycode = LEGACY_KEYCODES.get(keycode, keycode) + if '()' in keycode: + fields = key['keycodes'][i]['fields'] + keycode = f'{keycode.split(")")[0]}{",".join(map(str, fields))})' + layer.append(keycode) + if set(layer) == {'KC_TRNS'}: + break + keymap_data['layers'].append(layer) + + return keymap_data + + +def import_keymap(keymap_data): + # Validate to ensure we don't have to deal with bad data - handles stdin/file + validate(keymap_data, 'qmk.keymap.v1') + + kb_name = keymap_data['keyboard'] + km_name = keymap_data['keymap'] + + km_folder = keymap(kb_name) / km_name + keyboard_keymap = km_folder / 'keymap.json' + + # This is the deepest folder in the expected tree + keyboard_keymap.parent.mkdir(parents=True, exist_ok=True) + + # Dump out all those lovely files + keyboard_keymap.write_text(json.dumps(keymap_data, cls=KeymapJSONEncoder)) + + return (kb_name, km_name) + + +def import_keyboard(info_data, keymap_data=None): + # Validate to ensure we don't have to deal with bad data - handles stdin/file + validate(info_data, 'qmk.api.keyboard.v1') + + # And validate some more as everything is optional + if not all(key in info_data for key in ['keyboard_name', 'layouts']): + raise ValueError('invalid info.json') + + kb_name = info_data['keyboard_name'] + + # bail + kb_folder = keyboard(kb_name) + if kb_folder.exists(): + raise ValueError(f'Keyboard {{fg_cyan}}{kb_name}{{fg_reset}} already exists! Please choose a different name.') + + if not keymap_data: + # TODO: if supports community then grab that instead + keymap_data = _gen_dummy_keymap(kb_name, info_data) + + keyboard_info = kb_folder / 'info.json' + keyboard_keymap = kb_folder / 'keymaps' / 'default' / 'keymap.json' + + # begin with making the deepest folder in the tree + keyboard_keymap.parent.mkdir(parents=True, exist_ok=True) + + user_name = git_get_username() + if not user_name: + user_name = 'TODO' + + tokens = { # Comment here is to force multiline formatting + 'YEAR': str(date.today().year), + 'KEYBOARD': kb_name, + 'USER_NAME': user_name, + 'REAL_NAME': user_name, + } + + # Dump out all those lovely files + for file in list(TEMPLATE.iterdir()): + replace_placeholders(file, kb_folder / file.name, tokens) + + temp = json_load(keyboard_info) + deep_update(temp, info_data) + + keyboard_info.write_text(json.dumps(temp, cls=InfoJSONEncoder)) + keyboard_keymap.write_text(json.dumps(keymap_data, cls=KeymapJSONEncoder)) + + return kb_name + + +def import_kbfirmware(kbfirmware_data): + kbf_data = dotty(kbfirmware_data) + + diode_direction = ["COL2ROW", "ROW2COL"][kbf_data['keyboard.settings.diodeDirection']] + mcu = ["atmega32u2", "atmega32u4", "at90usb1286"][kbf_data['keyboard.controller']] + bootloader = MCU2BOOTLOADER.get(mcu, "custom") + + layout = _extract_kbfirmware_layout(kbf_data) + keymap_data = _extract_kbfirmware_keymap(kbf_data) + + # convert to d/d info.json + info_data = dotty({ + "keyboard_name": kbf_data['keyboard.settings.name'].lower(), + "processor": mcu, + "bootloader": bootloader, + "diode_direction": diode_direction, + "matrix_pins": { + "cols": kbf_data['keyboard.pins.col'], + "rows": kbf_data['keyboard.pins.row'], + }, + "layouts": { + "LAYOUT": { + "layout": layout, + } + } + }) + + if kbf_data['keyboard.pins.num'] or kbf_data['keyboard.pins.caps'] or kbf_data['keyboard.pins.scroll']: + if kbf_data['keyboard.pins.num']: + info_data['indicators.num_lock'] = kbf_data['keyboard.pins.num'] + if kbf_data['keyboard.pins.caps']: + info_data['indicators.caps_lock'] = kbf_data['keyboard.pins.caps'] + if kbf_data['keyboard.pins.scroll']: + info_data['indicators.scroll_lock'] = kbf_data['keyboard.pins.scroll'] + + if kbf_data['keyboard.pins.rgb']: + info_data['rgblight.animations.all'] = True + info_data['rgblight.led_count'] = kbf_data['keyboard.settings.rgbNum'] + info_data['rgblight.pin'] = kbf_data['keyboard.pins.rgb'] + + if kbf_data['keyboard.pins.led']: + info_data['backlight.levels'] = kbf_data['keyboard.settings.backlightLevels'] + info_data['backlight.pin'] = kbf_data['keyboard.pins.led'] + + # delegate as if it were a regular keyboard import + return import_keyboard(info_data.to_dict(), keymap_data) diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index ccec46ce21..c95b55916c 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -110,14 +110,7 @@ def info_json(keyboard): def _extract_features(info_data, rules): """Find all the features enabled in rules.mk. """ - # Special handling for bootmagic which also supports a "lite" mode. - if rules.get('BOOTMAGIC_ENABLE') == 'lite': - rules['BOOTMAGIC_LITE_ENABLE'] = 'on' - del rules['BOOTMAGIC_ENABLE'] - if rules.get('BOOTMAGIC_ENABLE') == 'full': - rules['BOOTMAGIC_ENABLE'] = 'on' - - # Process the rest of the rules as booleans + # Process booleans rules for key, value in rules.items(): if key.endswith('_ENABLE'): key = '_'.join(key.split('_')[:-1]).lower() @@ -218,6 +211,66 @@ def _extract_audio(info_data, config_c): info_data['audio'] = {'pins': audio_pins} +def _extract_encoders_values(config_c, postfix=''): + """Common encoder extraction logic + """ + a_pad = config_c.get(f'ENCODERS_PAD_A{postfix}', '').replace(' ', '')[1:-1] + b_pad = config_c.get(f'ENCODERS_PAD_B{postfix}', '').replace(' ', '')[1:-1] + resolutions = config_c.get(f'ENCODER_RESOLUTIONS{postfix}', '').replace(' ', '')[1:-1] + + default_resolution = config_c.get('ENCODER_RESOLUTION', None) + + if a_pad and b_pad: + a_pad = list(filter(None, a_pad.split(','))) + b_pad = list(filter(None, b_pad.split(','))) + resolutions = list(filter(None, resolutions.split(','))) + if default_resolution: + resolutions += [default_resolution] * (len(a_pad) - len(resolutions)) + + encoders = [] + for index in range(len(a_pad)): + encoder = {'pin_a': a_pad[index], 'pin_b': b_pad[index]} + if index < len(resolutions): + encoder['resolution'] = int(resolutions[index]) + encoders.append(encoder) + + return encoders + + +def _extract_encoders(info_data, config_c): + """Populate data about encoder pins + """ + encoders = _extract_encoders_values(config_c) + if encoders: + if 'encoder' not in info_data: + info_data['encoder'] = {} + + if 'rotary' in info_data['encoder']: + _log_warning(info_data, 'Encoder config is specified in both config.h and info.json (encoder.rotary) (Value: %s), the config.h value wins.' % info_data['encoder']['rotary']) + + info_data['encoder']['rotary'] = encoders + + +def _extract_split_encoders(info_data, config_c): + """Populate data about split encoder pins + """ + encoders = _extract_encoders_values(config_c, '_RIGHT') + if encoders: + if 'split' not in info_data: + info_data['split'] = {} + + if 'encoder' not in info_data['split']: + info_data['split']['encoder'] = {} + + if 'right' not in info_data['split']['encoder']: + info_data['split']['encoder']['right'] = {} + + if 'rotary' in info_data['split']['encoder']['right']: + _log_warning(info_data, 'Encoder config is specified in both config.h and info.json (encoder.rotary) (Value: %s), the config.h value wins.' % info_data['split']['encoder']['right']['rotary']) + + info_data['split']['encoder']['right']['rotary'] = encoders + + def _extract_secure_unlock(info_data, config_c): """Populate data about the secure unlock sequence """ @@ -314,11 +367,9 @@ def _extract_split_right_pins(info_data, config_c): # Figure out the right half matrix pins row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip() col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip() - unused_pin_text = config_c.get('UNUSED_PINS_RIGHT') - unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1] - if row_pins or col_pins or direct_pins or unused_pins: + if row_pins or col_pins or direct_pins: if info_data.get('split', {}).get('matrix_pins', {}).get('right') in info_data: _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.') @@ -340,17 +391,12 @@ def _extract_split_right_pins(info_data, config_c): if direct_pins: info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins) - if unused_pins: - info_data['split']['matrix_pins']['right']['unused'] = _extract_pins(unused_pins) - def _extract_matrix_info(info_data, config_c): """Populate the matrix information. """ row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() - unused_pin_text = config_c.get('UNUSED_PINS') - unused_pins = unused_pin_text.replace('{', '').replace('}', '').strip() if isinstance(unused_pin_text, str) else None direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] info_snippet = {} @@ -376,12 +422,6 @@ def _extract_matrix_info(info_data, config_c): info_snippet['direct'] = _extract_direct_matrix(direct_pins) - if unused_pins: - if 'matrix_pins' not in info_data: - info_data['matrix_pins'] = {} - - info_snippet['unused'] = _extract_pins(unused_pins) - if config_c.get('CUSTOM_MATRIX', 'no') != 'no': if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']: _log_warning(info_data, 'Custom Matrix is specified in both info.json and config.h, the config.h values win.') @@ -493,6 +533,8 @@ def _extract_config_h(info_data, config_c): _extract_split_main(info_data, config_c) _extract_split_transport(info_data, config_c) _extract_split_right_pins(info_data, config_c) + _extract_encoders(info_data, config_c) + _extract_split_encoders(info_data, config_c) _extract_device_version(info_data) return info_data diff --git a/lib/python/qmk/json_schema.py b/lib/python/qmk/json_schema.py index 682346113e..01175146b5 100644 --- a/lib/python/qmk/json_schema.py +++ b/lib/python/qmk/json_schema.py @@ -68,11 +68,7 @@ def create_validator(schema): schema_store = compile_schema_store() resolver = jsonschema.RefResolver.from_schema(schema_store[schema], store=schema_store) - # TODO: Remove this after the jsonschema>=4 requirement had time to reach users - try: - return jsonschema.Draft202012Validator(schema_store[schema], resolver=resolver).validate - except AttributeError: - return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate + return jsonschema.Draft202012Validator(schema_store[schema], resolver=resolver).validate def validate(data, schema): |