summary refs log tree commit diff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/c_parse.py3
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rw-r--r--lib/python/qmk/cli/doctor/linux.py57
-rw-r--r--lib/python/qmk/cli/doctor/macos.py2
-rw-r--r--lib/python/qmk/cli/flash.py122
-rwxr-xr-xlib/python/qmk/cli/generate/api.py37
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py36
-rw-r--r--lib/python/qmk/cli/generate/dfu_header.py4
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py8
-rw-r--r--lib/python/qmk/cli/import/__init__.py0
-rw-r--r--lib/python/qmk/cli/import/kbfirmware.py25
-rw-r--r--lib/python/qmk/cli/import/keyboard.py23
-rw-r--r--lib/python/qmk/cli/import/keymap.py23
-rw-r--r--lib/python/qmk/constants.py56
-rw-r--r--lib/python/qmk/flashers.py203
-rw-r--r--lib/python/qmk/git.py4
-rw-r--r--lib/python/qmk/importers.py193
-rw-r--r--lib/python/qmk/info.py86
-rw-r--r--lib/python/qmk/json_schema.py6
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):