qmk_firmware

QMK firmware for my keyboards (Corne, Sweep Ferris) and trackball (Ploopy Adept)
Log | Files | Refs | Submodules | LICENSE

info.py (43532B)


      1 """Functions that help us generate and use info.json files.
      2 """
      3 import re
      4 import os
      5 from pathlib import Path
      6 import jsonschema
      7 from dotty_dict import dotty
      8 from enum import IntFlag
      9 
     10 from milc import cli
     11 
     12 from qmk.constants import COL_LETTERS, ROW_LETTERS, CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, JOYSTICK_AXES
     13 from qmk.c_parse import find_layouts, parse_config_h_file, find_led_config
     14 from qmk.json_schema import deep_update, json_load, validate
     15 from qmk.keyboard import config_h, rules_mk
     16 from qmk.commands import parse_configurator_json
     17 from qmk.makefile import parse_rules_mk_file
     18 from qmk.math_ops import compute
     19 from qmk.util import maybe_exit, truthy
     20 
     21 true_values = ['1', 'on', 'yes']
     22 false_values = ['0', 'off', 'no']
     23 
     24 
     25 class LedFlags(IntFlag):
     26     ALL = 0xFF
     27     NONE = 0x00
     28     MODIFIER = 0x01
     29     UNDERGLOW = 0x02
     30     KEYLIGHT = 0x04
     31     INDICATOR = 0x08
     32 
     33 
     34 def _keyboard_in_layout_name(keyboard, layout):
     35     """Validate that a layout macro does not contain name of keyboard
     36     """
     37     # TODO: reduce this list down
     38     safe_layout_tokens = {
     39         'ansi',
     40         'iso',
     41         'jp',
     42         'jis',
     43         'ortho',
     44         'wkl',
     45         'tkl',
     46         'preonic',
     47         'planck',
     48     }
     49 
     50     # Ignore tokens like 'split_3x7_4' or just '2x4'
     51     layout = re.sub(r"_split_\d+x\d+_\d+", '', layout)
     52     layout = re.sub(r"_\d+x\d+", '', layout)
     53 
     54     name_fragments = set(keyboard.split('/')) - safe_layout_tokens
     55 
     56     return any(fragment in layout for fragment in name_fragments)
     57 
     58 
     59 def _valid_community_layout(layout):
     60     """Validate that a declared community list exists
     61     """
     62     return (Path('layouts/default') / layout).exists()
     63 
     64 
     65 def _get_key_left_position(key):
     66     # Special case for ISO enter
     67     return key['x'] - 0.25 if key.get('h', 1) == 2 and key.get('w', 1) == 1.25 else key['x']
     68 
     69 
     70 def _find_invalid_encoder_index(info_data):
     71     """Perform additional validation of encoders
     72     """
     73     enc_left = info_data.get('encoder', {}).get('rotary', [])
     74     enc_right = []
     75 
     76     if info_data.get('split', {}).get('enabled', False):
     77         enc_right = info_data.get('split', {}).get('encoder', {}).get('right', {}).get('rotary', enc_left)
     78 
     79     enc_count = len(enc_left) + len(enc_right)
     80 
     81     ret = []
     82     layouts = info_data.get('layouts', {})
     83     for layout_name, layout_data in layouts.items():
     84         found = set()
     85         for key in layout_data['layout']:
     86             if 'encoder' in key:
     87                 if enc_count == 0:
     88                     ret.append((layout_name, key['encoder'], 'non-configured'))
     89                 elif key['encoder'] >= enc_count:
     90                     ret.append((layout_name, key['encoder'], 'out of bounds'))
     91                 elif key['encoder'] in found:
     92                     ret.append((layout_name, key['encoder'], 'duplicate'))
     93                 found.add(key['encoder'])
     94 
     95     return ret
     96 
     97 
     98 def _validate_build_target(keyboard, info_data):
     99     """Non schema checks
    100     """
    101     keyboard_json_path = Path('keyboards') / keyboard / 'keyboard.json'
    102     config_files = find_info_json(keyboard)
    103 
    104     # keyboard.json can only exist at the deepest part of the tree
    105     keyboard_json_count = 0
    106     for info_file in config_files:
    107         if info_file.name == 'keyboard.json':
    108             keyboard_json_count += 1
    109             if info_file != keyboard_json_path:
    110                 _log_error(info_data, f'Invalid keyboard.json location detected: {info_file}.')
    111 
    112     # No keyboard.json next to info.json
    113     for conf_file in config_files:
    114         if conf_file.name == 'keyboard.json':
    115             info_file = conf_file.parent / 'info.json'
    116             if info_file.exists():
    117                 _log_error(info_data, f'Invalid info.json location detected: {info_file}.')
    118 
    119     # Moving forward keyboard.json should be used as a build target
    120     if keyboard_json_count == 0:
    121         _log_warning(info_data, 'Build marker "keyboard.json" not found.')
    122 
    123 
    124 def _validate_layouts(keyboard, info_data):  # noqa C901
    125     """Non schema checks
    126     """
    127     col_num = info_data.get('matrix_size', {}).get('cols', 0)
    128     row_num = info_data.get('matrix_size', {}).get('rows', 0)
    129     layouts = info_data.get('layouts', {})
    130     layout_aliases = info_data.get('layout_aliases', {})
    131     community_layouts = info_data.get('community_layouts', [])
    132     community_layouts_names = list(map(lambda layout: f'LAYOUT_{layout}', community_layouts))
    133 
    134     # Make sure we have at least one layout
    135     if len(layouts) == 0 or all(not layout.get('json_layout', False) for layout in layouts.values()):
    136         _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in info.json.')
    137 
    138     # Make sure all layouts are DD
    139     for layout_name, layout_data in layouts.items():
    140         if layout_data.get('c_macro', False):
    141             _log_error(info_data, f'{layout_name}: Layout macro should not be defined within ".h" files.')
    142 
    143     # Make sure all matrix values are in bounds
    144     for layout_name, layout_data in layouts.items():
    145         for index, key_data in enumerate(layout_data['layout']):
    146             row, col = key_data['matrix']
    147             key_name = key_data.get('label', f'k{ROW_LETTERS[row]}{COL_LETTERS[col]}')
    148             if row >= row_num:
    149                 _log_error(info_data, f'{layout_name}: Matrix row for key {index} ({key_name}) is {row} but must be less than {row_num}')
    150             if col >= col_num:
    151                 _log_error(info_data, f'{layout_name}: Matrix column for key {index} ({key_name}) is {col} but must be less than {col_num}')
    152 
    153     # Reject duplicate matrix locations
    154     for layout_name, layout_data in layouts.items():
    155         seen = set()
    156         for index, key_data in enumerate(layout_data['layout']):
    157             key = f"{key_data['matrix']}"
    158             if key in seen:
    159                 _log_error(info_data, f'{layout_name}: Matrix location for key {index} is not unique {key_data}')
    160             seen.add(key)
    161 
    162     # Warn if physical positions are offset (at least one key should be at x=0, and at least one key at y=0)
    163     for layout_name, layout_data in layouts.items():
    164         offset_x = min([_get_key_left_position(k) for k in layout_data['layout']])
    165         if offset_x > 0:
    166             _log_warning(info_data, f'Layout "{layout_name}" is offset on X axis by {offset_x}')
    167 
    168         offset_y = min([k['y'] for k in layout_data['layout']])
    169         if offset_y > 0:
    170             _log_warning(info_data, f'Layout "{layout_name}" is offset on Y axis by {offset_y}')
    171 
    172     # Providing only LAYOUT_all "because I define my layouts in a 3rd party tool"
    173     if len(layouts) == 1 and 'LAYOUT_all' in layouts:
    174         _log_warning(info_data, '"LAYOUT_all" should be "LAYOUT" unless additional layouts are provided.')
    175 
    176     # Extended layout name checks - ignoring community_layouts and "safe" values
    177     potential_layouts = set(layouts.keys()) - set(community_layouts_names)
    178     for layout in potential_layouts:
    179         if _keyboard_in_layout_name(keyboard, layout):
    180             _log_warning(info_data, f'Layout "{layout}" should not contain name of keyboard.')
    181 
    182     # Filter out any non-existing community layouts
    183     for layout in community_layouts:
    184         if not _valid_community_layout(layout):
    185             # Ignore layout from future checks
    186             info_data['community_layouts'].remove(layout)
    187             _log_error(info_data, 'Claims to support a community layout that does not exist: %s' % (layout))
    188 
    189     # Make sure we supply layout macros for the community layouts we claim to support
    190     for layout_name in community_layouts_names:
    191         if layout_name not in layouts and layout_name not in layout_aliases:
    192             _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
    193 
    194 
    195 def _validate_keycodes(keyboard, info_data):
    196     """Non schema checks
    197     """
    198     # keycodes with length > 7 must have short forms for visualisation purposes
    199     for decl in info_data.get('keycodes', []):
    200         if len(decl["key"]) > 7:
    201             if not decl.get("aliases", []):
    202                 _log_error(info_data, f'Keycode {decl["key"]} has no short form alias')
    203 
    204 
    205 def _validate_encoders(keyboard, info_data):
    206     """Non schema checks
    207     """
    208     # encoder IDs in layouts must be in range and not duplicated
    209     found = _find_invalid_encoder_index(info_data)
    210     for layout_name, encoder_index, reason in found:
    211         _log_error(info_data, f'Layout "{layout_name}" contains {reason} encoder index {encoder_index}.')
    212 
    213 
    214 def _validate(keyboard, info_data):
    215     """Perform various validation on the provided info.json data
    216     """
    217     # First validate against the jsonschema
    218     try:
    219         validate(info_data, 'qmk.api.keyboard.v1')
    220 
    221         # Additional validation
    222         _validate_build_target(keyboard, info_data)
    223         _validate_layouts(keyboard, info_data)
    224         _validate_keycodes(keyboard, info_data)
    225         _validate_encoders(keyboard, info_data)
    226 
    227     except jsonschema.ValidationError as e:
    228         json_path = '.'.join([str(p) for p in e.absolute_path])
    229         cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
    230         maybe_exit(1)
    231 
    232 
    233 def info_json(keyboard, force_layout=None):
    234     """Generate the info.json data for a specific keyboard.
    235     """
    236     info_data = {
    237         'keyboard_name': str(keyboard),
    238         'keyboard_folder': str(keyboard),
    239         'keymaps': {},
    240         'layouts': {},
    241         'parse_errors': [],
    242         'parse_warnings': [],
    243         'maintainer': 'qmk',
    244     }
    245 
    246     # Populate layout data
    247     layouts, aliases = _search_keyboard_h(keyboard)
    248 
    249     if aliases:
    250         info_data['layout_aliases'] = aliases
    251 
    252     for layout_name, layout_json in layouts.items():
    253         if not layout_name.startswith('LAYOUT_kc'):
    254             layout_json['c_macro'] = True
    255             layout_json['json_layout'] = False
    256             info_data['layouts'][layout_name] = layout_json
    257 
    258     # Merge in the data from info.json, config.h, and rules.mk
    259     info_data = merge_info_jsons(keyboard, info_data)
    260     info_data = _process_defaults(info_data)
    261     info_data = _extract_rules_mk(info_data, rules_mk(str(keyboard)))
    262     info_data = _extract_config_h(info_data, config_h(str(keyboard)))
    263 
    264     # Ensure that we have various calculated values
    265     info_data = _matrix_size(info_data)
    266     info_data = _joystick_axis_count(info_data)
    267     info_data = _matrix_masked(info_data)
    268 
    269     # Merge in data from <keyboard.c>
    270     info_data = _extract_led_config(info_data, str(keyboard))
    271 
    272     # Force a community layout if requested
    273     community_layouts = info_data.get("community_layouts", [])
    274     if force_layout in community_layouts:
    275         info_data["community_layouts"] = [force_layout]
    276 
    277     # Validate
    278     # Skip processing if necessary
    279     if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False):
    280         _validate(keyboard, info_data)
    281 
    282     # Check that the reported matrix size is consistent with the actual matrix size
    283     _check_matrix(info_data)
    284 
    285     return info_data
    286 
    287 
    288 def _extract_features(info_data, rules):
    289     """Find all the features enabled in rules.mk.
    290     """
    291     # Process booleans rules
    292     for key, value in rules.items():
    293         if key.endswith('_ENABLE'):
    294             key = '_'.join(key.split('_')[:-1]).lower()
    295             value = True if value.lower() in true_values else False if value.lower() in false_values else value
    296 
    297             if key in ['lto']:
    298                 continue
    299 
    300             if 'config_h_features' not in info_data:
    301                 info_data['config_h_features'] = {}
    302 
    303             if 'features' not in info_data:
    304                 info_data['features'] = {}
    305 
    306             if key in info_data['features']:
    307                 _log_warning(info_data, 'Feature %s is specified in both info.json (%s) and rules.mk (%s). The rules.mk value wins.' % (key, info_data['features'], value))
    308 
    309             info_data['features'][key] = value
    310             info_data['config_h_features'][key] = value
    311 
    312     return info_data
    313 
    314 
    315 def _extract_matrix_rules(info_data, rules):
    316     """Find all the features enabled in rules.mk.
    317     """
    318     if rules.get('CUSTOM_MATRIX', 'no') != 'no':
    319         if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']:
    320             _log_warning(info_data, 'Custom Matrix is specified in both info.json and rules.mk, the rules.mk values win.')
    321 
    322         if 'matrix_pins' not in info_data:
    323             info_data['matrix_pins'] = {}
    324 
    325         if rules['CUSTOM_MATRIX'] == 'lite':
    326             info_data['matrix_pins']['custom_lite'] = True
    327         else:
    328             info_data['matrix_pins']['custom'] = True
    329 
    330     return info_data
    331 
    332 
    333 def _pin_name(pin):
    334     """Returns the proper representation for a pin.
    335     """
    336     pin = pin.strip()
    337 
    338     if not pin:
    339         return None
    340 
    341     elif pin.isdigit():
    342         return int(pin)
    343 
    344     elif pin == 'NO_PIN':
    345         return None
    346 
    347     return pin
    348 
    349 
    350 def _extract_pins(pins):
    351     """Returns a list of pins from a comma separated string of pins.
    352     """
    353     return [_pin_name(pin) for pin in pins.split(',')]
    354 
    355 
    356 def _extract_2d_array(raw):
    357     """Return a 2d array of strings
    358     """
    359     out_array = []
    360 
    361     while raw[-1] != '}':
    362         raw = raw[:-1]
    363 
    364     for row in raw.split('},{'):
    365         if row.startswith('{'):
    366             row = row[1:]
    367 
    368         if row.endswith('}'):
    369             row = row[:-1]
    370 
    371         out_array.append([])
    372 
    373         for val in row.split(','):
    374             out_array[-1].append(val)
    375 
    376     return out_array
    377 
    378 
    379 def _extract_2d_int_array(raw):
    380     """Return a 2d array of ints
    381     """
    382     ret = _extract_2d_array(raw)
    383 
    384     return [list(map(int, x)) for x in ret]
    385 
    386 
    387 def _extract_direct_matrix(direct_pins):
    388     """extract direct_matrix
    389     """
    390     direct_pin_array = _extract_2d_array(direct_pins)
    391 
    392     for i in range(len(direct_pin_array)):
    393         for j in range(len(direct_pin_array[i])):
    394             if direct_pin_array[i][j] == 'NO_PIN':
    395                 direct_pin_array[i][j] = None
    396 
    397     return direct_pin_array
    398 
    399 
    400 def _extract_audio(info_data, config_c):
    401     """Populate data about the audio configuration
    402     """
    403     audio_pins = []
    404 
    405     for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6':
    406         if config_c.get(f'{pin}_AUDIO'):
    407             audio_pins.append(pin)
    408 
    409     if audio_pins:
    410         info_data['audio'] = {'pins': audio_pins}
    411 
    412 
    413 def _extract_encoders_values(config_c, postfix=''):
    414     """Common encoder extraction logic
    415     """
    416     a_pad = config_c.get(f'ENCODER_A_PINS{postfix}', '').replace(' ', '')[1:-1]
    417     b_pad = config_c.get(f'ENCODER_B_PINS{postfix}', '').replace(' ', '')[1:-1]
    418     resolutions = config_c.get(f'ENCODER_RESOLUTIONS{postfix}', '').replace(' ', '')[1:-1]
    419 
    420     default_resolution = config_c.get('ENCODER_RESOLUTION', None)
    421 
    422     if a_pad and b_pad:
    423         a_pad = list(filter(None, a_pad.split(',')))
    424         b_pad = list(filter(None, b_pad.split(',')))
    425         resolutions = list(filter(None, resolutions.split(',')))
    426         if default_resolution:
    427             resolutions += [default_resolution] * (len(a_pad) - len(resolutions))
    428 
    429         encoders = []
    430         for index in range(len(a_pad)):
    431             encoder = {'pin_a': a_pad[index], 'pin_b': b_pad[index]}
    432             if index < len(resolutions):
    433                 encoder['resolution'] = int(resolutions[index])
    434             encoders.append(encoder)
    435 
    436         return encoders
    437 
    438 
    439 def _extract_encoders(info_data, config_c):
    440     """Populate data about encoder pins
    441     """
    442     encoders = _extract_encoders_values(config_c)
    443     if encoders:
    444         if 'encoder' not in info_data:
    445             info_data['encoder'] = {}
    446 
    447         if 'rotary' in info_data['encoder']:
    448             _log_warning(info_data, 'Encoder config is specified in both config.h (%s) and info.json (%s). The config.h value wins.' % (encoders, info_data['encoder']['rotary']))
    449 
    450         info_data['encoder']['rotary'] = encoders
    451 
    452     # TODO: some logic still assumes ENCODER_ENABLED would partially create encoder dict
    453     if info_data.get('features', {}).get('encoder', False):
    454         if 'encoder' not in info_data:
    455             info_data['encoder'] = {}
    456         info_data['encoder']['enabled'] = True
    457 
    458 
    459 def _extract_split_encoders(info_data, config_c):
    460     """Populate data about split encoder pins
    461     """
    462     encoders = _extract_encoders_values(config_c, '_RIGHT')
    463     if encoders:
    464         if 'split' not in info_data:
    465             info_data['split'] = {}
    466 
    467         if 'encoder' not in info_data['split']:
    468             info_data['split']['encoder'] = {}
    469 
    470         if 'right' not in info_data['split']['encoder']:
    471             info_data['split']['encoder']['right'] = {}
    472 
    473         if 'rotary' in info_data['split']['encoder']['right']:
    474             _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'])
    475 
    476         info_data['split']['encoder']['right']['rotary'] = encoders
    477 
    478 
    479 def _extract_secure_unlock(info_data, config_c):
    480     """Populate data about the secure unlock sequence
    481     """
    482     unlock = config_c.get('SECURE_UNLOCK_SEQUENCE', '').replace(' ', '')[1:-1]
    483     if unlock:
    484         unlock_array = _extract_2d_int_array(unlock)
    485         if 'secure' not in info_data:
    486             info_data['secure'] = {}
    487 
    488         if 'unlock_sequence' in info_data['secure']:
    489             _log_warning(info_data, 'Secure unlock sequence is specified in both config.h (SECURE_UNLOCK_SEQUENCE) and info.json (secure.unlock_sequence) (Value: %s), the config.h value wins.' % info_data['secure']['unlock_sequence'])
    490 
    491         info_data['secure']['unlock_sequence'] = unlock_array
    492 
    493 
    494 def _extract_split_handedness(info_data, config_c):
    495     # Migrate
    496     split = info_data.get('split', {})
    497     if 'matrix_grid' in split:
    498         split['handedness'] = split.get('handedness', {})
    499         split['handedness']['matrix_grid'] = split.pop('matrix_grid')
    500 
    501 
    502 def _extract_split_serial(info_data, config_c):
    503     # Migrate
    504     split = info_data.get('split', {})
    505     if 'soft_serial_pin' in split:
    506         split['serial'] = split.get('serial', {})
    507         split['serial']['pin'] = split.pop('soft_serial_pin')
    508     if 'soft_serial_speed' in split:
    509         split['serial'] = split.get('serial', {})
    510         split['serial']['speed'] = split.pop('soft_serial_speed')
    511 
    512 
    513 def _extract_split_transport(info_data, config_c):
    514     # Figure out the transport method
    515     if config_c.get('USE_I2C') is True:
    516         if 'split' not in info_data:
    517             info_data['split'] = {}
    518 
    519         if 'transport' not in info_data['split']:
    520             info_data['split']['transport'] = {}
    521 
    522         if 'protocol' in info_data['split']['transport']:
    523             _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'])
    524 
    525         info_data['split']['transport']['protocol'] = 'i2c'
    526 
    527     # Ignore transport defaults if "SPLIT_KEYBOARD" is unset
    528     elif 'enabled' in info_data.get('split', {}):
    529         if 'split' not in info_data:
    530             info_data['split'] = {}
    531 
    532         if 'transport' not in info_data['split']:
    533             info_data['split']['transport'] = {}
    534 
    535         if 'protocol' not in info_data['split']['transport']:
    536             info_data['split']['transport']['protocol'] = 'serial'
    537 
    538     # Migrate
    539     transport = info_data.get('split', {}).get('transport', {})
    540     if 'sync_matrix_state' in transport:
    541         transport['sync'] = transport.get('sync', {})
    542         transport['sync']['matrix_state'] = transport.pop('sync_matrix_state')
    543     if 'sync_modifiers' in transport:
    544         transport['sync'] = transport.get('sync', {})
    545         transport['sync']['modifiers'] = transport.pop('sync_modifiers')
    546 
    547 
    548 def _extract_split_right_pins(info_data, config_c):
    549     # Figure out the right half matrix pins
    550     row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
    551     col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
    552     direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1]
    553 
    554     if row_pins or col_pins or direct_pins:
    555         if info_data.get('split', {}).get('matrix_pins', {}).get('right', None):
    556             _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
    557 
    558         if 'split' not in info_data:
    559             info_data['split'] = {}
    560 
    561         if 'matrix_pins' not in info_data['split']:
    562             info_data['split']['matrix_pins'] = {}
    563 
    564         if 'right' not in info_data['split']['matrix_pins']:
    565             info_data['split']['matrix_pins']['right'] = {}
    566 
    567         if col_pins:
    568             info_data['split']['matrix_pins']['right']['cols'] = _extract_pins(col_pins)
    569 
    570         if row_pins:
    571             info_data['split']['matrix_pins']['right']['rows'] = _extract_pins(row_pins)
    572 
    573         if direct_pins:
    574             info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins)
    575 
    576 
    577 def _extract_matrix_info(info_data, config_c):
    578     """Populate the matrix information.
    579     """
    580     row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
    581     col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
    582     direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
    583 
    584     if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
    585         if 'matrix_size' in info_data:
    586             _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
    587 
    588         info_data['matrix_size'] = {
    589             'cols': compute(config_c.get('MATRIX_COLS', '0')),
    590             'rows': compute(config_c.get('MATRIX_ROWS', '0')),
    591         }
    592 
    593     if row_pins and col_pins:
    594         if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
    595             _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
    596 
    597         if 'matrix_pins' not in info_data:
    598             info_data['matrix_pins'] = {}
    599 
    600         info_data['matrix_pins']['cols'] = _extract_pins(col_pins)
    601         info_data['matrix_pins']['rows'] = _extract_pins(row_pins)
    602 
    603     if direct_pins:
    604         if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:
    605             _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
    606 
    607         if 'matrix_pins' not in info_data:
    608             info_data['matrix_pins'] = {}
    609 
    610         info_data['matrix_pins']['direct'] = _extract_direct_matrix(direct_pins)
    611 
    612     return info_data
    613 
    614 
    615 def _config_to_json(key_type, config_value):
    616     """Convert config value using spec
    617     """
    618     if key_type.startswith('array'):
    619         if key_type.count('.') > 1:
    620             raise Exception(f"Conversion of {key_type} not possible")
    621 
    622         if '.' in key_type:
    623             key_type, array_type = key_type.split('.', 1)
    624         else:
    625             array_type = None
    626 
    627         config_value = config_value.replace('{', '').replace('}', '').strip()
    628 
    629         if array_type == 'int':
    630             return list(map(int, config_value.split(',')))
    631         else:
    632             return list(map(str.strip, config_value.split(',')))
    633 
    634     elif key_type in ['bool', 'flag']:
    635         if isinstance(config_value, bool):
    636             return config_value
    637         return config_value in true_values
    638 
    639     elif key_type == 'hex':
    640         return '0x' + config_value[2:].upper()
    641 
    642     elif key_type == 'list':
    643         return config_value.split()
    644 
    645     elif key_type == 'int':
    646         return int(config_value)
    647 
    648     elif key_type == 'str':
    649         return config_value.strip('"').replace('\\"', '"').replace('\\\\', '\\')
    650 
    651     elif key_type == 'bcd_version':
    652         major = int(config_value[2:4])
    653         minor = int(config_value[4])
    654         revision = int(config_value[5])
    655 
    656         return f'{major}.{minor}.{revision}'
    657 
    658     return config_value
    659 
    660 
    661 def _extract_config_h(info_data, config_c):
    662     """Pull some keyboard information from existing config.h files
    663     """
    664     # Pull in data from the json map
    665     dotty_info = dotty(info_data)
    666     info_config_map = json_load(Path('data/mappings/info_config.hjson'))
    667 
    668     for config_key, info_dict in info_config_map.items():
    669         info_key = info_dict['info_key']
    670         key_type = info_dict.get('value_type', 'raw')
    671 
    672         try:
    673             replace_with = info_dict.get('replace_with')
    674             if config_key in config_c and info_dict.get('invalid', False):
    675                 if replace_with:
    676                     _log_error(info_data, '%s in config.h is no longer a valid option and should be replaced with %s' % (config_key, replace_with))
    677                 else:
    678                     _log_error(info_data, '%s in config.h is no longer a valid option and should be removed' % config_key)
    679             elif config_key in config_c and info_dict.get('deprecated', False):
    680                 if replace_with:
    681                     _log_warning(info_data, '%s in config.h is deprecated in favor of %s and will be removed at a later date' % (config_key, replace_with))
    682                 else:
    683                     _log_warning(info_data, '%s in config.h is deprecated and will be removed at a later date' % config_key)
    684 
    685             if config_key in config_c and info_dict.get('to_json', True):
    686                 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
    687                     _log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
    688 
    689                 dotty_info[info_key] = _config_to_json(key_type, config_c[config_key])
    690 
    691         except Exception as e:
    692             _log_warning(info_data, f'{config_key}->{info_key}: {e}')
    693 
    694     info_data.update(dotty_info)
    695 
    696     # Pull data that easily can't be mapped in json
    697     _extract_matrix_info(info_data, config_c)
    698     _extract_audio(info_data, config_c)
    699     _extract_secure_unlock(info_data, config_c)
    700     _extract_split_handedness(info_data, config_c)
    701     _extract_split_serial(info_data, config_c)
    702     _extract_split_transport(info_data, config_c)
    703     _extract_split_right_pins(info_data, config_c)
    704     _extract_encoders(info_data, config_c)
    705     _extract_split_encoders(info_data, config_c)
    706 
    707     return info_data
    708 
    709 
    710 def _process_defaults(info_data):
    711     """Process any additional defaults based on currently discovered information
    712     """
    713     defaults_map = json_load(Path('data/mappings/defaults.hjson'))
    714     for default_type in defaults_map.keys():
    715         thing_map = defaults_map[default_type]
    716         if default_type in info_data:
    717             merged_count = 0
    718             thing_items = thing_map.get(info_data[default_type], {}).items()
    719             for key, value in thing_items:
    720                 if key not in info_data:
    721                     info_data[key] = value
    722                     merged_count += 1
    723 
    724             if merged_count == 0 and len(thing_items) > 0:
    725                 _log_warning(info_data, 'All defaults for \'%s\' were skipped, potential redundant config or misconfiguration detected' % (default_type))
    726 
    727     return info_data
    728 
    729 
    730 def _extract_rules_mk(info_data, rules):
    731     """Pull some keyboard information from existing rules.mk files
    732     """
    733     info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
    734 
    735     if info_data['processor'] in CHIBIOS_PROCESSORS:
    736         arm_processor_rules(info_data, rules)
    737 
    738     elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
    739         avr_processor_rules(info_data, rules)
    740 
    741     else:
    742         cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
    743         unknown_processor_rules(info_data, rules)
    744 
    745     # Pull in data from the json map
    746     dotty_info = dotty(info_data)
    747     info_rules_map = json_load(Path('data/mappings/info_rules.hjson'))
    748 
    749     for rules_key, info_dict in info_rules_map.items():
    750         info_key = info_dict['info_key']
    751         key_type = info_dict.get('value_type', 'raw')
    752 
    753         try:
    754             replace_with = info_dict.get('replace_with')
    755             if rules_key in rules and info_dict.get('invalid', False):
    756                 if replace_with:
    757                     _log_error(info_data, '%s in rules.mk is no longer a valid option and should be replaced with %s' % (rules_key, replace_with))
    758                 else:
    759                     _log_error(info_data, '%s in rules.mk is no longer a valid option and should be removed' % rules_key)
    760             elif rules_key in rules and info_dict.get('deprecated', False):
    761                 if replace_with:
    762                     _log_warning(info_data, '%s in rules.mk is deprecated in favor of %s and will be removed at a later date' % (rules_key, replace_with))
    763                 else:
    764                     _log_warning(info_data, '%s in rules.mk is deprecated and will be removed at a later date' % rules_key)
    765 
    766             if rules_key in rules and info_dict.get('to_json', True):
    767                 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
    768                     _log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
    769 
    770                 dotty_info[info_key] = _config_to_json(key_type, rules[rules_key])
    771 
    772         except Exception as e:
    773             _log_warning(info_data, f'{rules_key}->{info_key}: {e}')
    774 
    775     info_data.update(dotty_info)
    776 
    777     # Merge in config values that can't be easily mapped
    778     _extract_features(info_data, rules)
    779     _extract_matrix_rules(info_data, rules)
    780 
    781     return info_data
    782 
    783 
    784 def find_keyboard_c(keyboard):
    785     """Find all <keyboard>.c files
    786     """
    787     keyboard = Path(keyboard)
    788     current_path = Path('keyboards/')
    789 
    790     files = []
    791     for directory in keyboard.parts:
    792         current_path = current_path / directory
    793         keyboard_c_path = current_path / f'{directory}.c'
    794         if keyboard_c_path.exists():
    795             files.append(keyboard_c_path)
    796 
    797     return files
    798 
    799 
    800 def _extract_led_config(info_data, keyboard):
    801     """Scan all <keyboard>.c files for led config
    802     """
    803     for feature in ['rgb_matrix', 'led_matrix']:
    804         if info_data.get('features', {}).get(feature, False) or feature in info_data:
    805             # Only attempt search if dd led config is missing
    806             if 'layout' not in info_data.get(feature, {}):
    807                 cols = info_data.get('matrix_size', {}).get('cols')
    808                 rows = info_data.get('matrix_size', {}).get('rows')
    809                 if cols and rows:
    810                     # Process
    811                     for file in find_keyboard_c(keyboard):
    812                         try:
    813                             ret = find_led_config(file, cols, rows)
    814                             if ret:
    815                                 info_data[feature] = info_data.get(feature, {})
    816                                 info_data[feature]['layout'] = ret
    817                         except Exception as e:
    818                             _log_warning(info_data, f'led_config: {file.name}: {e}')
    819                 else:
    820                     _log_warning(info_data, 'led_config: matrix size required to parse g_led_config')
    821 
    822             if info_data[feature].get('layout', None) and not info_data[feature].get('led_count', None):
    823                 info_data[feature]['led_count'] = len(info_data[feature]['layout'])
    824 
    825             if info_data[feature].get('layout', None) and not info_data[feature].get('flag_steps', None):
    826                 flags = {LedFlags.ALL, LedFlags.NONE}
    827                 default_flags = {LedFlags.MODIFIER | LedFlags.KEYLIGHT, LedFlags.UNDERGLOW}
    828 
    829                 # if only a single flag is used, assume only all+none flags
    830                 kb_flags = set(x.get('flags', LedFlags.NONE) for x in info_data[feature]['layout'])
    831                 if len(kb_flags) > 1:
    832                     # check if any part of LED flag is with the defaults
    833                     unique_flags = set()
    834                     for candidate in default_flags:
    835                         if any(candidate & flag for flag in kb_flags):
    836                             unique_flags.add(candidate)
    837 
    838                     # if we still have a single flag, assume only all+none
    839                     if len(unique_flags) > 1:
    840                         flags.update(unique_flags)
    841 
    842                 info_data[feature]['flag_steps'] = sorted([int(flag) for flag in flags], reverse=True)
    843 
    844     return info_data
    845 
    846 
    847 def _matrix_size(info_data):
    848     """Add info_data['matrix_size'] if it doesn't exist.
    849     """
    850     if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
    851         info_data['matrix_size'] = {}
    852 
    853         if 'direct' in info_data['matrix_pins']:
    854             info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
    855             info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
    856         elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
    857             info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
    858             info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
    859 
    860         # Assumption of split common
    861         if 'split' in info_data:
    862             if info_data['split'].get('enabled', False):
    863                 info_data['matrix_size']['rows'] *= 2
    864 
    865     return info_data
    866 
    867 
    868 def _joystick_axis_count(info_data):
    869     """Add info_data['joystick.axis_count'] if required
    870     """
    871     if 'axes' in info_data.get('joystick', {}):
    872         axes_keys = info_data['joystick']['axes'].keys()
    873         info_data['joystick']['axis_count'] = max(JOYSTICK_AXES.index(a) for a in axes_keys) + 1 if axes_keys else 0
    874 
    875     return info_data
    876 
    877 
    878 def _matrix_masked(info_data):
    879     """"Add info_data['matrix_pins.masked'] if required"""
    880     mask_required = False
    881 
    882     if 'matrix_grid' in info_data.get('dip_switch', {}):
    883         mask_required = True
    884     if 'matrix_grid' in info_data.get('split', {}).get('handedness', {}):
    885         mask_required = True
    886 
    887     if mask_required:
    888         if 'masked' not in info_data.get('matrix_pins', {}):
    889             if 'matrix_pins' not in info_data:
    890                 info_data['matrix_pins'] = {}
    891 
    892             info_data['matrix_pins']['masked'] = True
    893 
    894     return info_data
    895 
    896 
    897 def _check_matrix(info_data):
    898     """Check the matrix to ensure that row/column count is consistent.
    899     """
    900     if 'matrix_pins' in info_data and 'matrix_size' in info_data:
    901         actual_col_count = info_data['matrix_size'].get('cols', 0)
    902         actual_row_count = info_data['matrix_size'].get('rows', 0)
    903         col_count = row_count = 0
    904 
    905         if 'direct' in info_data['matrix_pins']:
    906             col_count = len(info_data['matrix_pins']['direct'][0])
    907             row_count = len(info_data['matrix_pins']['direct'])
    908         elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
    909             col_count = len(info_data['matrix_pins']['cols'])
    910             row_count = len(info_data['matrix_pins']['rows'])
    911         elif 'cols' not in info_data['matrix_pins'] and 'rows' not in info_data['matrix_pins']:
    912             # This case caters for custom matrix implementations where normal rows/cols are specified
    913             return
    914 
    915         if col_count != actual_col_count and col_count != (actual_col_count / 2):
    916             # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
    917             _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
    918 
    919         if row_count != actual_row_count and row_count != (actual_row_count / 2):
    920             # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
    921             _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
    922 
    923 
    924 def _search_keyboard_h(keyboard):
    925     keyboard = Path(keyboard)
    926     current_path = Path('keyboards/')
    927     aliases = {}
    928     layouts = {}
    929 
    930     for directory in keyboard.parts:
    931         current_path = current_path / directory
    932         keyboard_h = '%s.h' % (directory,)
    933         keyboard_h_path = current_path / keyboard_h
    934         if keyboard_h_path.exists():
    935             new_layouts, new_aliases = find_layouts(keyboard_h_path)
    936             layouts.update(new_layouts)
    937 
    938             for alias, alias_text in new_aliases.items():
    939                 if alias_text in layouts:
    940                     aliases[alias] = alias_text
    941 
    942     return layouts, aliases
    943 
    944 
    945 def _log_error(info_data, message):
    946     """Send an error message to both JSON and the log.
    947     """
    948     info_data['parse_errors'].append(message)
    949     cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
    950 
    951 
    952 def _log_warning(info_data, message):
    953     """Send a warning message to both JSON and the log.
    954     """
    955     info_data['parse_warnings'].append(message)
    956     cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
    957 
    958 
    959 def arm_processor_rules(info_data, rules):
    960     """Setup the default info for an ARM board.
    961     """
    962     info_data['processor_type'] = 'arm'
    963     info_data['protocol'] = 'ChibiOS'
    964     info_data['platform_key'] = 'chibios'
    965 
    966     if 'STM32' in info_data['processor']:
    967         info_data['platform'] = 'STM32'
    968     elif 'MCU_SERIES' in rules:
    969         info_data['platform'] = rules['MCU_SERIES']
    970 
    971     return info_data
    972 
    973 
    974 def avr_processor_rules(info_data, rules):
    975     """Setup the default info for an AVR board.
    976     """
    977     info_data['processor_type'] = 'avr'
    978     info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
    979     info_data['platform_key'] = 'avr'
    980     info_data['protocol'] = 'V-USB' if info_data['processor'] in VUSB_PROCESSORS else 'LUFA'
    981 
    982     # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
    983     # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
    984 
    985     return info_data
    986 
    987 
    988 def unknown_processor_rules(info_data, rules):
    989     """Setup the default keyboard info for unknown boards.
    990     """
    991     info_data['bootloader'] = 'unknown'
    992     info_data['platform'] = 'unknown'
    993     info_data['processor'] = 'unknown'
    994     info_data['processor_type'] = 'unknown'
    995     info_data['protocol'] = 'unknown'
    996 
    997     return info_data
    998 
    999 
   1000 def merge_info_jsons(keyboard, info_data):
   1001     """Return a merged copy of all the info.json files for a keyboard.
   1002     """
   1003     config_files = find_info_json(keyboard)
   1004 
   1005     for info_file in config_files:
   1006         # Load and validate the JSON data
   1007         new_info_data = json_load(info_file)
   1008 
   1009         if not isinstance(new_info_data, dict):
   1010             _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
   1011             continue
   1012 
   1013         if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False):
   1014             try:
   1015                 validate(new_info_data, 'qmk.keyboard.v1')
   1016             except jsonschema.ValidationError as e:
   1017                 json_path = '.'.join([str(p) for p in e.absolute_path])
   1018                 cli.log.error('Not including data from file: %s', info_file)
   1019                 cli.log.error('\t%s: %s', json_path, e.message)
   1020                 continue
   1021 
   1022         # Merge layout data in
   1023         if 'layout_aliases' in new_info_data:
   1024             info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']}
   1025             del new_info_data['layout_aliases']
   1026 
   1027         for layout_name, layout in new_info_data.get('layouts', {}).items():
   1028             if layout_name in info_data.get('layout_aliases', {}):
   1029                 _log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}")
   1030                 layout_name = info_data['layout_aliases'][layout_name]
   1031 
   1032             if layout_name in info_data['layouts']:
   1033                 if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']):
   1034                     msg = 'Number of keys for %s does not match! info.json specifies %d keys, C macro specifies %d'
   1035                     _log_error(info_data, msg % (layout_name, len(layout['layout']), len(info_data['layouts'][layout_name]['layout'])))
   1036                 else:
   1037                     info_data['layouts'][layout_name]['json_layout'] = True
   1038                     for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
   1039                         existing_key.update(new_key)
   1040             else:
   1041                 if not all('matrix' in key_data.keys() for key_data in layout['layout']):
   1042                     _log_error(info_data, f'Layout "{layout_name}" has no "matrix" definition in either "info.json" or "<keyboard>.h"!')
   1043                 else:
   1044                     layout['c_macro'] = False
   1045                     layout['json_layout'] = True
   1046                     info_data['layouts'][layout_name] = layout
   1047 
   1048         # Update info_data with the new data
   1049         if 'layouts' in new_info_data:
   1050             del new_info_data['layouts']
   1051 
   1052         deep_update(info_data, new_info_data)
   1053 
   1054     return info_data
   1055 
   1056 
   1057 def find_info_json(keyboard):
   1058     """Finds all the info.json files associated with a keyboard.
   1059     """
   1060     # Find the most specific first
   1061     base_path = Path('keyboards')
   1062     keyboard_path = base_path / keyboard
   1063     keyboard_parent = keyboard_path.parent
   1064     info_jsons = [keyboard_path / 'info.json', keyboard_path / 'keyboard.json']
   1065 
   1066     # Add in parent folders for least specific
   1067     for _ in range(5):
   1068         if keyboard_parent == base_path:
   1069             break
   1070         info_jsons.append(keyboard_parent / 'info.json')
   1071         info_jsons.append(keyboard_parent / 'keyboard.json')
   1072         keyboard_parent = keyboard_parent.parent
   1073 
   1074     # Return a list of the info.json files that actually exist
   1075     return [info_json for info_json in info_jsons if info_json.exists()]
   1076 
   1077 
   1078 def keymap_json_config(keyboard, keymap, force_layout=None):
   1079     """Extract keymap level config
   1080     """
   1081     # TODO: resolve keymap.py and info.py circular dependencies
   1082     from qmk.keymap import locate_keymap
   1083 
   1084     keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent
   1085 
   1086     km_info_json = parse_configurator_json(keymap_folder / 'keymap.json')
   1087     return km_info_json.get('config', {})
   1088 
   1089 
   1090 def keymap_json(keyboard, keymap, force_layout=None):
   1091     """Generate the info.json data for a specific keymap.
   1092     """
   1093     # TODO: resolve keymap.py and info.py circular dependencies
   1094     from qmk.keymap import locate_keymap
   1095 
   1096     keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent
   1097 
   1098     # Files to scan
   1099     keymap_config = keymap_folder / 'config.h'
   1100     keymap_rules = keymap_folder / 'rules.mk'
   1101     keymap_file = keymap_folder / 'keymap.json'
   1102 
   1103     # Build the info.json file
   1104     kb_info_json = info_json(keyboard, force_layout=force_layout)
   1105 
   1106     # Merge in the data from keymap.json
   1107     km_info_json = keymap_json_config(keyboard, keymap, force_layout=force_layout) if keymap_file.exists() else {}
   1108     deep_update(kb_info_json, km_info_json)
   1109 
   1110     # Merge in the data from config.h, and rules.mk
   1111     _extract_rules_mk(kb_info_json, parse_rules_mk_file(keymap_rules))
   1112     _extract_config_h(kb_info_json, parse_config_h_file(keymap_config))
   1113 
   1114     return kb_info_json
   1115 
   1116 
   1117 def get_modules(keyboard, keymap_filename):
   1118     """Get the modules for a keyboard/keymap.
   1119     """
   1120     modules = []
   1121 
   1122     kb_info_json = info_json(keyboard)
   1123     modules.extend(kb_info_json.get('modules', []))
   1124 
   1125     if keymap_filename:
   1126         keymap_json = parse_configurator_json(keymap_filename)
   1127 
   1128         if keymap_json:
   1129             modules.extend(keymap_json.get('modules', []))
   1130 
   1131     return list(dict.fromkeys(modules))  # remove dupes