diff options
Diffstat (limited to 'lib/python')
| -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 | ||||
| -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 | 
18 files changed, 746 insertions, 142 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 02c6d1cbf4..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  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/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): | 
