summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
authorZach White <skullydazed@gmail.com>2021-08-16 15:33:30 -0700
committerGitHub <noreply@github.com>2021-08-16 23:33:30 +0100
commit8d9bfdc25437bb401985ba93b47edae2126e7fac (patch)
tree2439e7adde0bafd6af9a403c92c1a89384c3f6ea /lib
parentfac717c11cfa27780f2f9098383673784174141a (diff)
Add a lot more data to info.json (#13366)
* add some split data to info.json

* add tags

* add half of config_options.md to info.json

* add support for designating master split

* sort out split transport and primary

* fix bad data in UNUSED_PINS

* fixup custom transport

* wip

* allow for setting split right half keyboard matrix

* add SPLIT_USB_DETECT

* minor cleanup

* fix an erroneous message

* rework split.usb_detect

* adding missing rgblight vars to info.json

* add mouse_key to info.json

* add all remaining options from docs/config_options.md

* fix audio voices

* qmk info: Change text output to use dotted notation

* tweak layout output

* resolve alias names

* break out some functions to make flake8 happy

* add a field for bootloader instructions

* qmk generate-info-json: add a write-to-file argument

Adds an argument that instructs qmk generate-info-json to write the output to a file instead of just to the terminal.

* -arg_only, +action

Because it was never my intention that one would have to specify a value for the argument that enables writing the file.

* Bring qmk generate-info-json inline with other generate commands

* pytest fixup

* fix esca/getawayvan

* fix data driven errors for bpiphany converters

* features.force_nkro -> usb.force_nkro

* split.primary->split.main

* fix esca/getawayvan_f042

* fix the bpiphany converters for real

* fix bpiphany/tiger_lily

* Apply suggestions from code review

Co-authored-by: Nick Brassel <nick@tzarc.org>

* fix generate-api errors

* fix matrix pin extraction for split boards

* fix ploopyco/trackball_nano/rev1_001

Co-authored-by: James Young <18669334+noroadsleft@users.noreply.github.com>
Co-authored-by: Nick Brassel <nick@tzarc.org>
Diffstat (limited to 'lib')
-rwxr-xr-xlib/python/qmk/cli/generate/config_h.py140
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py40
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py11
-rwxr-xr-xlib/python/qmk/cli/info.py53
-rw-r--r--lib/python/qmk/info.py184
-rw-r--r--lib/python/qmk/json_schema.py15
6 files changed, 360 insertions, 83 deletions
diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py
index 54cd5b96a8..c0c148f1c0 100755
--- a/lib/python/qmk/cli/generate/config_h.py
+++ b/lib/python/qmk/cli/generate/config_h.py
@@ -12,7 +12,7 @@ from qmk.keyboard import keyboard_completer, keyboard_folder
 from qmk.path import is_keyboard, normpath
 
 
-def direct_pins(direct_pins):
+def direct_pins(direct_pins, postfix):
     """Return the config.h lines that set the direct pins.
     """
     rows = []
@@ -24,81 +24,60 @@ def direct_pins(direct_pins):
     col_count = len(direct_pins[0])
     row_count = len(direct_pins)
 
-    return """
-#ifndef MATRIX_COLS
-#   define MATRIX_COLS %s
-#endif // MATRIX_COLS
+    return f"""
+#ifndef MATRIX_COLS{postfix}
+#   define MATRIX_COLS{postfix} {col_count}
+#endif // MATRIX_COLS{postfix}
 
-#ifndef MATRIX_ROWS
-#   define MATRIX_ROWS %s
-#endif // MATRIX_ROWS
+#ifndef MATRIX_ROWS{postfix}
+#   define MATRIX_ROWS{postfix} {row_count}
+#endif // MATRIX_ROWS{postfix}
 
-#ifndef DIRECT_PINS
-#   define DIRECT_PINS {%s}
-#endif // DIRECT_PINS
-""" % (col_count, row_count, ','.join(rows))
+#ifndef DIRECT_PINS{postfix}
+#   define DIRECT_PINS{postfix} {{ {", ".join(rows)} }}
+#endif // DIRECT_PINS{postfix}
+"""
 
 
-def pin_array(define, pins):
+def pin_array(define, pins, postfix):
     """Return the config.h lines that set a pin array.
     """
     pin_num = len(pins)
     pin_array = ', '.join(map(str, [pin or 'NO_PIN' for pin in pins]))
 
     return f"""
-#ifndef {define}S
-#   define {define}S {pin_num}
-#endif // {define}S
+#ifndef {define}S{postfix}
+#   define {define}S{postfix} {pin_num}
+#endif // {define}S{postfix}
 
-#ifndef {define}_PINS
-#   define {define}_PINS {{ {pin_array} }}
-#endif // {define}_PINS
+#ifndef {define}_PINS{postfix}
+#   define {define}_PINS{postfix} {{ {pin_array} }}
+#endif // {define}_PINS{postfix}
 """
 
 
-def matrix_pins(matrix_pins):
+def matrix_pins(matrix_pins, postfix=''):
     """Add the matrix config to the config.h.
     """
     pins = []
 
     if 'direct' in matrix_pins:
-        pins.append(direct_pins(matrix_pins['direct']))
+        pins.append(direct_pins(matrix_pins['direct'], postfix))
 
     if 'cols' in matrix_pins:
-        pins.append(pin_array('MATRIX_COL', matrix_pins['cols']))
+        pins.append(pin_array('MATRIX_COL', matrix_pins['cols'], postfix))
 
     if 'rows' in matrix_pins:
-        pins.append(pin_array('MATRIX_ROW', matrix_pins['rows']))
+        pins.append(pin_array('MATRIX_ROW', matrix_pins['rows'], postfix))
 
     return '\n'.join(pins)
 
 
-@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")
-@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
-@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
-@automagic_keyboard
-@automagic_keymap
-def generate_config_h(cli):
-    """Generates the info_config.h file.
+def generate_config_items(kb_info_json, config_h_lines):
+    """Iterate through the info_config map to generate basic config values.
     """
-    # Determine our keyboard(s)
-    if not cli.config.generate_config_h.keyboard:
-        cli.log.error('Missing parameter: --keyboard')
-        cli.subcommands['info'].print_help()
-        return False
-
-    if not is_keyboard(cli.config.generate_config_h.keyboard):
-        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
-        return False
-
-    # Build the info_config.h file.
-    kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
     info_config_map = json_load(Path('data/mappings/info_config.json'))
 
-    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
-
-    # Iterate through the info_config map to generate basic things
     for config_key, info_dict in info_config_map.items():
         info_key = info_dict['info_key']
         key_type = info_dict.get('value_type', 'str')
@@ -135,9 +114,78 @@ def generate_config_h(cli):
             config_h_lines.append(f'#   define {config_key} {config_value}')
             config_h_lines.append(f'#endif // {config_key}')
 
+
+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']:
+        if kb_info_json['split']['primary'] in ('left', 'right'):
+            config_h_lines.append('')
+            config_h_lines.append('#ifndef MASTER_LEFT')
+            config_h_lines.append('#   ifndef MASTER_RIGHT')
+            if kb_info_json['split']['primary'] == 'left':
+                config_h_lines.append('#       define MASTER_LEFT')
+            elif kb_info_json['split']['primary'] == 'right':
+                config_h_lines.append('#       define MASTER_RIGHT')
+            config_h_lines.append('#   endif // MASTER_RIGHT')
+            config_h_lines.append('#endif // MASTER_LEFT')
+        elif kb_info_json['split']['primary'] == 'pin':
+            config_h_lines.append('')
+            config_h_lines.append('#ifndef SPLIT_HAND_PIN')
+            config_h_lines.append('#   define SPLIT_HAND_PIN')
+            config_h_lines.append('#endif // SPLIT_HAND_PIN')
+        elif kb_info_json['split']['primary'] == 'matrix_grid':
+            config_h_lines.append('')
+            config_h_lines.append('#ifndef SPLIT_HAND_MATRIX_GRID')
+            config_h_lines.append('#   define SPLIT_HAND_MATRIX_GRID {%s}' % (','.join(kb_info_json["split"]["matrix_grid"],)))
+            config_h_lines.append('#endif // SPLIT_HAND_MATRIX_GRID')
+        elif kb_info_json['split']['primary'] == 'eeprom':
+            config_h_lines.append('')
+            config_h_lines.append('#ifndef EE_HANDS')
+            config_h_lines.append('#   define EE_HANDS')
+            config_h_lines.append('#endif // EE_HANDS')
+
+    if 'protocol' in kb_info_json['split'].get('transport', {}):
+        if kb_info_json['split']['transport']['protocol'] == 'i2c':
+            config_h_lines.append('')
+            config_h_lines.append('#ifndef USE_I2C')
+            config_h_lines.append('#   define USE_I2C')
+            config_h_lines.append('#endif // USE_I2C')
+
+    if 'right' in kb_info_json['split'].get('matrix_pins', {}):
+        config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_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")
+@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to generate config.h for.')
+@cli.subcommand('Used by the make system to generate info_config.h from info.json', hidden=True)
+@automagic_keyboard
+@automagic_keymap
+def generate_config_h(cli):
+    """Generates the info_config.h file.
+    """
+    # Determine our keyboard(s)
+    if not cli.config.generate_config_h.keyboard:
+        cli.log.error('Missing parameter: --keyboard')
+        cli.subcommands['info'].print_help()
+        return False
+
+    if not is_keyboard(cli.config.generate_config_h.keyboard):
+        cli.log.error('Invalid keyboard: "%s"', cli.config.generate_config_h.keyboard)
+        return False
+
+    # Build the info_config.h file.
+    kb_info_json = dotty(info_json(cli.config.generate_config_h.keyboard))
+    config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once']
+
+    generate_config_items(kb_info_json, config_h_lines)
+
     if 'matrix_pins' in kb_info_json:
         config_h_lines.append(matrix_pins(kb_info_json['matrix_pins']))
 
+    if 'split' in kb_info_json:
+        generate_split_config(kb_info_json, config_h_lines)
+
     # Show the results
     config_h = '\n'.join(config_h_lines)
 
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
index 8931b68b6f..284d1a8510 100755
--- a/lib/python/qmk/cli/generate/info_json.py
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -4,15 +4,17 @@ Compile an info.json for a particular keyboard and pretty-print it.
 """
 import json
 
-from jsonschema import Draft7Validator, validators
+from argcomplete.completers import FilesCompleter
+from jsonschema import Draft7Validator, RefResolver, validators
 from milc import cli
+from pathlib import Path
 
 from qmk.decorators import automagic_keyboard, automagic_keymap
 from qmk.info import info_json
 from qmk.json_encoders import InfoJSONEncoder
-from qmk.json_schema import load_jsonschema
+from qmk.json_schema import compile_schema_store
 from qmk.keyboard import keyboard_completer, keyboard_folder
-from qmk.path import is_keyboard
+from qmk.path import is_keyboard, normpath
 
 
 def pruning_validator(validator_class):
@@ -34,15 +36,19 @@ def pruning_validator(validator_class):
 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)
-    schema = load_jsonschema('keyboard')
-    validator = pruning_draft_7_validator(schema).validate
+    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
 
     return validator(kb_info_json)
 
 
 @cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Keyboard to show info for.')
 @cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.')
+@cli.argument('-o', '--output', arg_only=True, completer=FilesCompleter, help='Write the output the specified file, overwriting if necessary.')
+@cli.argument('-ow', '--overwrite', arg_only=True, action='store_true', help='Overwrite the existing info.json. (Overrides the location of --output)')
 @cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True)
 @automagic_keyboard
 @automagic_keymap
@@ -59,9 +65,29 @@ def generate_info_json(cli):
         cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard)
         return False
 
+    if cli.args.overwrite:
+        output_path = (Path('keyboards') / cli.config.generate_info_json.keyboard / 'info.json').resolve()
+
+        if cli.args.output:
+            cli.log.warning('Overwriting user supplied --output with %s', output_path)
+
+        cli.args.output = output_path
+
     # Build the info.json file
     kb_info_json = info_json(cli.config.generate_info_json.keyboard)
     strip_info_json(kb_info_json)
+    info_json_text = json.dumps(kb_info_json, indent=4, cls=InfoJSONEncoder)
+
+    if cli.args.output:
+        # Write to a file
+        output_path = normpath(cli.args.output)
+
+        if output_path.exists():
+            cli.log.warning('Overwriting output file %s', output_path)
+
+        output_path.write_text(info_json_text + '\n')
+        cli.log.info('Wrote info.json to %s.', output_path)
 
-    # Display the results
-    print(json.dumps(kb_info_json, indent=2, cls=InfoJSONEncoder))
+    else:
+        # Display the results
+        print(info_json_text)
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 41c94e16b5..2712b81cb5 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -76,6 +76,17 @@ def generate_rules_mk(cli):
                 enabled = 'yes' if enabled else 'no'
                 rules_mk_lines.append(f'{feature}_ENABLE ?= {enabled}')
 
+    # Set SPLIT_TRANSPORT, if needed
+    if kb_info_json.get('split', {}).get('transport', {}).get('protocol') == 'custom':
+        rules_mk_lines.append('SPLIT_TRANSPORT ?= custom')
+
+    # Set CUSTOM_MATRIX, if needed
+    if kb_info_json.get('matrix_pins', {}).get('custom'):
+        if kb_info_json.get('matrix_pins', {}).get('custom_lite'):
+            rules_mk_lines.append('CUSTOM_MATRIX ?= lite')
+        else:
+            rules_mk_lines.append('CUSTOM_MATRIX ?= yes')
+
     # Show the results
     rules_mk = '\n'.join(rules_mk_lines) + '\n'
 
diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py
index 337b494a99..3131d4b53f 100755
--- a/lib/python/qmk/cli/info.py
+++ b/lib/python/qmk/cli/info.py
@@ -24,19 +24,15 @@ def show_keymap(kb_info_json, title_caps=True):
     keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
 
     if keymap_path and keymap_path.suffix == '.json':
-        if title_caps:
-            cli.echo('{fg_blue}Keymap "%s"{fg_reset}:', cli.config.info.keymap)
-        else:
-            cli.echo('{fg_blue}keymap_%s{fg_reset}:', cli.config.info.keymap)
-
         keymap_data = json.load(keymap_path.open(encoding='utf-8'))
         layout_name = keymap_data['layout']
+        layout_name = kb_info_json.get('layout_aliases', {}).get(layout_name, layout_name)  # Resolve alias names
 
         for layer_num, layer in enumerate(keymap_data['layers']):
             if title_caps:
-                cli.echo('{fg_cyan}Layer %s{fg_reset}:', layer_num)
+                cli.echo('{fg_cyan}Keymap %s Layer %s{fg_reset}:', cli.config.info.keymap, layer_num)
             else:
-                cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)
+                cli.echo('{fg_cyan}keymap.%s.layer.%s{fg_reset}:', cli.config.info.keymap, layer_num)
 
             print(render_layout(kb_info_json['layouts'][layout_name]['layout'], cli.config.info.ascii, layer))
 
@@ -45,7 +41,7 @@ def show_layouts(kb_info_json, title_caps=True):
     """Render the layouts with info.json labels.
     """
     for layout_name, layout_art in render_layouts(kb_info_json, cli.config.info.ascii).items():
-        title = layout_name.title() if title_caps else layout_name
+        title = f'Layout {layout_name.title()}' if title_caps else f'layouts.{layout_name}'
         cli.echo('{fg_cyan}%s{fg_reset}:', title)
         print(layout_art)  # Avoid passing dirty data to cli.echo()
 
@@ -93,15 +89,6 @@ def print_friendly_output(kb_info_json):
         aliases = [f'{key}={value}' for key, value in kb_info_json['layout_aliases'].items()]
         cli.echo('{fg_blue}Layout aliases:{fg_reset} %s' % (', '.join(aliases),))
 
-    if cli.config.info.layouts:
-        show_layouts(kb_info_json, True)
-
-    if cli.config.info.matrix:
-        show_matrix(kb_info_json, True)
-
-    if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
-        show_keymap(kb_info_json, True)
-
 
 def print_text_output(kb_info_json):
     """Print the info.json in a plain text format.
@@ -122,6 +109,24 @@ def print_text_output(kb_info_json):
         show_keymap(kb_info_json, False)
 
 
+def print_dotted_output(kb_info_json, prefix=''):
+    """Print the info.json in a plain text format with dot-joined keys.
+    """
+    for key in sorted(kb_info_json):
+        new_prefix = f'{prefix}.{key}' if prefix else key
+
+        if key in ['parse_errors', 'parse_warnings']:
+            continue
+        elif key == 'layouts' and prefix == '':
+            cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
+        elif isinstance(kb_info_json[key], dict):
+            print_dotted_output(kb_info_json[key], new_prefix)
+        elif isinstance(kb_info_json[key], list):
+            cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, sorted(kb_info_json[key]))))
+        else:
+            cli.echo('{fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key])
+
+
 def print_parsed_rules_mk(keyboard_name):
     rules = rules_mk(keyboard_name)
     for k in sorted(rules.keys()):
@@ -162,10 +167,22 @@ def info(cli):
     # Output in the requested format
     if cli.args.format == 'json':
         print(json.dumps(kb_info_json, cls=InfoJSONEncoder))
+        return True
     elif cli.args.format == 'text':
-        print_text_output(kb_info_json)
+        print_dotted_output(kb_info_json)
+        title_caps = False
     elif cli.args.format == 'friendly':
         print_friendly_output(kb_info_json)
+        title_caps = True
     else:
         cli.log.error('Unknown format: %s', cli.args.format)
         return False
+
+    if cli.config.info.layouts:
+        show_layouts(kb_info_json, title_caps)
+
+    if cli.config.info.matrix:
+        show_matrix(kb_info_json, title_caps)
+
+    if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
+        show_keymap(kb_info_json, title_caps)
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 858fbab335..b6f2ecf644 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -61,8 +61,8 @@ def info_json(keyboard):
 
     # Merge in the data from info.json, config.h, and rules.mk
     info_data = merge_info_jsons(keyboard, info_data)
-    info_data = _extract_config_h(info_data)
     info_data = _extract_rules_mk(info_data)
+    info_data = _extract_config_h(info_data)
 
     # Ensure that we have matrix row and column counts
     info_data = _matrix_size(info_data)
@@ -161,10 +161,9 @@ def _extract_pins(pins):
     return [_pin_name(pin) for pin in pins.split(',')]
 
 
-def _extract_direct_matrix(info_data, direct_pins):
+def _extract_direct_matrix(direct_pins):
     """
     """
-    info_data['matrix_pins'] = {}
     direct_pin_array = []
 
     while direct_pins[-1] != '}':
@@ -188,12 +187,157 @@ def _extract_direct_matrix(info_data, direct_pins):
     return direct_pin_array
 
 
+def _extract_audio(info_data, config_c):
+    """Populate data about the audio configuration
+    """
+    audio_pins = []
+
+    for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6':
+        if config_c.get(f'{pin}_AUDIO'):
+            audio_pins.append(pin)
+
+    if audio_pins:
+        info_data['audio'] = {'pins': audio_pins}
+
+
+def _extract_split_main(info_data, config_c):
+    """Populate data about the split configuration
+    """
+    # Figure out how the main half is determined
+    if config_c.get('SPLIT_HAND_PIN') is True:
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'main' in info_data['split']:
+            _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_PIN) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
+
+        info_data['split']['main'] = 'pin'
+
+    if config_c.get('SPLIT_HAND_MATRIX_GRID'):
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'main' in info_data['split']:
+            _log_warning(info_data, 'Split main hand is specified in both config.h (SPLIT_HAND_MATRIX_GRID) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
+
+        info_data['split']['main'] = 'matrix_grid'
+        info_data['split']['matrix_grid'] = _extract_pins(config_c['SPLIT_HAND_MATRIX_GRID'])
+
+    if config_c.get('EE_HANDS') is True:
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'main' in info_data['split']:
+            _log_warning(info_data, 'Split main hand is specified in both config.h (EE_HANDS) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
+
+        info_data['split']['main'] = 'eeprom'
+
+    if config_c.get('MASTER_RIGHT') is True:
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'main' in info_data['split']:
+            _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_RIGHT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
+
+        info_data['split']['main'] = 'right'
+
+    if config_c.get('MASTER_LEFT') is True:
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'main' in info_data['split']:
+            _log_warning(info_data, 'Split main hand is specified in both config.h (MASTER_LEFT) and info.json (split.main) (Value: %s), the config.h value wins.' % info_data['split']['main'])
+
+        info_data['split']['main'] = 'left'
+
+
+def _extract_split_transport(info_data, config_c):
+    # Figure out the transport method
+    if config_c.get('USE_I2C') is True:
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'transport' not in info_data['split']:
+            info_data['split']['transport'] = {}
+
+        if 'protocol' in info_data['split']['transport']:
+            _log_warning(info_data, 'Split transport is specified in both config.h (USE_I2C) and info.json (split.transport.protocol) (Value: %s), the config.h value wins.' % info_data['split']['transport'])
+
+        info_data['split']['transport']['protocol'] = 'i2c'
+
+    elif 'protocol' not in info_data.get('split', {}).get('transport', {}):
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'transport' not in info_data['split']:
+            info_data['split']['transport'] = {}
+
+        info_data['split']['transport']['protocol'] = 'serial'
+
+
+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 and col_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.')
+
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'matrix_pins' not in info_data['split']:
+            info_data['split']['matrix_pins'] = {}
+
+        if 'right' not in info_data['split']['matrix_pins']:
+            info_data['split']['matrix_pins']['right'] = {}
+
+        info_data['split']['matrix_pins']['right'] = {
+            'cols': _extract_pins(col_pins),
+            'rows': _extract_pins(row_pins),
+        }
+
+    if direct_pins:
+        if info_data.get('split', {}).get('matrix_pins', {}).get('right', {}):
+            _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
+
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'matrix_pins' not in info_data['split']:
+            info_data['split']['matrix_pins'] = {}
+
+        if 'right' not in info_data['split']['matrix_pins']:
+            info_data['split']['matrix_pins']['right'] = {}
+
+        info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins)
+
+    if unused_pins:
+        if 'split' not in info_data:
+            info_data['split'] = {}
+
+        if 'matrix_pins' not in info_data['split']:
+            info_data['split']['matrix_pins'] = {}
+
+        if 'right' not in info_data['split']['matrix_pins']:
+            info_data['split']['matrix_pins']['right'] = {}
+
+        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 = {}
 
     if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
         if 'matrix_size' in info_data:
@@ -205,19 +349,35 @@ def _extract_matrix_info(info_data, config_c):
         }
 
     if row_pins and col_pins:
-        if 'matrix_pins' in info_data:
+        if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
             _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
 
-        info_data['matrix_pins'] = {
-            'cols': _extract_pins(col_pins),
-            'rows': _extract_pins(row_pins),
-        }
+        info_snippet['cols'] = _extract_pins(col_pins)
+        info_snippet['rows'] = _extract_pins(row_pins)
 
     if direct_pins:
-        if 'matrix_pins' in info_data:
+        if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:
             _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
 
-        info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
+        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.')
+
+        info_snippet['custom'] = True
+
+        if config_c['CUSTOM_MATRIX'] == 'lite':
+            info_snippet['custom_lite'] = True
+
+    if info_snippet:
+        info_data['matrix_pins'] = info_snippet
 
     return info_data
 
@@ -275,6 +435,10 @@ def _extract_config_h(info_data):
 
     # Pull data that easily can't be mapped in json
     _extract_matrix_info(info_data, config_c)
+    _extract_audio(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)
 
     return info_data
 
diff --git a/lib/python/qmk/json_schema.py b/lib/python/qmk/json_schema.py
index cbc5bff518..ffc7c6bcd1 100644
--- a/lib/python/qmk/json_schema.py
+++ b/lib/python/qmk/json_schema.py
@@ -2,6 +2,7 @@
 """
 import json
 from collections.abc import Mapping
+from functools import lru_cache
 from pathlib import Path
 
 import hjson
@@ -25,6 +26,7 @@ def json_load(json_file):
         exit(1)
 
 
+@lru_cache(maxsize=0)
 def load_jsonschema(schema_name):
     """Read a jsonschema file from disk.
     """
@@ -39,8 +41,9 @@ def load_jsonschema(schema_name):
     return json_load(schema_path)
 
 
-def create_validator(schema):
-    """Creates a validator for the given schema id.
+@lru_cache(maxsize=0)
+def compile_schema_store():
+    """Compile all our schemas into a schema store.
     """
     schema_store = {}
 
@@ -51,6 +54,14 @@ def create_validator(schema):
             continue
         schema_store[schema_data['$id']] = schema_data
 
+    return schema_store
+
+
+@lru_cache(maxsize=0)
+def create_validator(schema):
+    """Creates a validator for the given schema id.
+    """
+    schema_store = compile_schema_store()
     resolver = jsonschema.RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store)
 
     return jsonschema.Draft7Validator(schema_store[schema], resolver=resolver).validate