~ruther/qmk_firmware

ededff8556daff544633cb143cb6d939afd09014 — Zach White 4 years ago 95cbcef
validate keyboard data with jsonschema
M lib/python/qmk/cli/generate/info_json.py => lib/python/qmk/cli/generate/info_json.py +1 -1
@@ 39,7 39,7 @@ def generate_info_json(cli):
            pared_down_json[key] = kb_info_json[key]

    pared_down_json['layouts'] = {}
    if 'layouts' in pared_down_json:
    if 'layouts' in kb_info_json:
        for layout_name, layout in kb_info_json['layouts'].items():
            pared_down_json['layouts'][layout_name] = {}
            pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))

M lib/python/qmk/cli/generate/rules_mk.py => lib/python/qmk/cli/generate/rules_mk.py +13 -0
@@ 6,6 6,10 @@ from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json
from qmk.path import is_keyboard, normpath

info_to_rules = {
    'bootloader': 'BOOTLOADER',
    'processor': 'MCU'
}

@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")


@@ 30,6 34,10 @@ def generate_rules_mk(cli):
    kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
    rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']

    # Bring in settings
    for info_key, rule_key in info_to_rules.items():
        rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}')

    # Find features that should be enabled
    if 'features' in kb_info_json:
        for feature, enabled in kb_info_json['features'].items():


@@ 37,6 45,11 @@ def generate_rules_mk(cli):
            enabled = 'yes' if enabled else 'no'
            rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')

    # Set the LED driver
    if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
        driver = kb_info_json['led_matrix']['driver']
        rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}')

    # Add community layouts
    if 'community_layouts' in kb_info_json:
        rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')

M lib/python/qmk/info.py => lib/python/qmk/info.py +140 -11
@@ 4,6 4,7 @@ import json
from glob import glob
from pathlib import Path

import jsonschema
from milc import cli

from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS


@@ 13,6 14,17 @@ from qmk.keymap import list_keymaps
from qmk.makefile import parse_rules_mk_file
from qmk.math import compute

led_matrix_properties = {
    'driver_count': 'LED_DRIVER_COUNT',
    'driver_addr1': 'LED_DRIVER_ADDR_1',
    'driver_addr2': 'LED_DRIVER_ADDR_2',
    'driver_addr3': 'LED_DRIVER_ADDR_3',
    'driver_addr4': 'LED_DRIVER_ADDR_4',
    'led_count': 'LED_DRIVER_LED_COUNT',
    'timeout': 'ISSI_TIMEOUT',
    'persistence': 'ISSI_PERSISTENCE'
}

rgblight_properties = {
    'led_count': 'RGBLED_NUM',
    'pin': 'RGB_DI_PIN',


@@ 80,6 92,15 @@ def info_json(keyboard):
    info_data = _extract_config_h(info_data)
    info_data = _extract_rules_mk(info_data)

    # Validate against the jsonschema
    try:
        keyboard_api_validate(info_data)

    except jsonschema.ValidationError as e:
        cli.log.error('Invalid info.json data: %s', e.message)
        print(dir(e))
        exit()

    # Make sure we have at least one layout
    if not info_data.get('layouts'):
        _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')


@@ 102,6 123,50 @@ def info_json(keyboard):
    return info_data


def _json_load(json_file):
    """Load a json file from disk.

    Note: file must be a Path object.
    """
    try:
        return json.load(json_file.open())

    except json.decoder.JSONDecodeError as e:
        cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
        exit(1)


def _jsonschema(schema_name):
    """Read a jsonschema file from disk.
    """
    schema_path = Path(f'data/schemas/{schema_name}.jsonschema')

    if not schema_path.exists():
        schema_path = Path('data/schemas/false.jsonschema')

    return _json_load(schema_path)


def keyboard_validate(data):
    """Validates data against the keyboard jsonschema.
    """
    schema = _jsonschema('keyboard')
    validator = jsonschema.Draft7Validator(schema).validate

    return validator(data)


def keyboard_api_validate(data):
    """Validates data against the api_keyboard jsonschema.
    """
    base = _jsonschema('keyboard')
    relative = _jsonschema('api_keyboard')
    resolver = jsonschema.RefResolver.from_schema(base)
    validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate

    return validator(data)


def _extract_debounce(info_data, config_c):
    """Handle debounce.
    """


@@ 109,7 174,7 @@ def _extract_debounce(info_data, config_c):
        _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')

    if 'DEBOUNCE' in config_c:
        info_data['debounce'] = config_c.get('DEBOUNCE')
        info_data['debounce'] = int(config_c['DEBOUNCE'])

    return info_data



@@ 181,8 246,36 @@ def _extract_features(info_data, rules):
    return info_data


def _extract_led_drivers(info_data, rules):
    """Find all the LED drivers set in rules.mk.
    """
    if 'LED_MATRIX_DRIVER' in rules:
        if 'led_matrix' not in info_data:
            info_data['led_matrix'] = {}

        if info_data['led_matrix'].get('driver'):
            _log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')

        info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']

    return info_data


def _extract_led_matrix(info_data, config_c):
    """Handle the led_matrix configuration.
    """
    led_matrix = info_data.get('led_matrix', {})

    for json_key, config_key in led_matrix_properties.items():
        if config_key in config_c:
            if json_key in led_matrix:
                _log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))

            led_matrix[json_key] = config_c[config_key]


def _extract_rgblight(info_data, config_c):
    """Handle the rgblight configuration
    """Handle the rgblight configuration.
    """
    rgblight = info_data.get('rgblight', {})
    animations = rgblight.get('animations', {})


@@ 303,6 396,7 @@ def _extract_config_h(info_data):
    _extract_indicators(info_data, config_c)
    _extract_matrix_info(info_data, config_c)
    _extract_usb_info(info_data, config_c)
    _extract_led_matrix(info_data, config_c)
    _extract_rgblight(info_data, config_c)

    return info_data


@@ 326,6 420,7 @@ def _extract_rules_mk(info_data):

    _extract_community_layouts(info_data, rules)
    _extract_features(info_data, rules)
    _extract_led_drivers(info_data, rules)

    return info_data



@@ 412,13 507,28 @@ def arm_processor_rules(info_data, rules):
    """Setup the default info for an ARM board.
    """
    info_data['processor_type'] = 'arm'
    info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
    info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
    info_data['protocol'] = 'ChibiOS'

    if info_data['bootloader'] == 'unknown':
    if 'MCU' in rules:
        if 'processor' in info_data:
            _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')

        info_data['processor'] = rules['MCU']

    elif 'processor' not in info_data:
        info_data['processor'] = 'unknown'

    if 'BOOTLOADER' in rules:
        if 'bootloader' in info_data:
            _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')

        info_data['bootloader'] = rules['BOOTLOADER']

    else:
        if 'STM32' in info_data['processor']:
            info_data['bootloader'] = 'stm32-dfu'
        else:
            info_data['bootloader'] = 'unknown'

    if 'STM32' in info_data['processor']:
        info_data['platform'] = 'STM32'


@@ 436,9 546,25 @@ def avr_processor_rules(info_data, rules):
    info_data['processor_type'] = 'avr'
    info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
    info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
    info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
    info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'

    if 'MCU' in rules:
        if 'processor' in info_data:
            _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')

        info_data['processor'] = rules['MCU']

    elif 'processor' not in info_data:
        info_data['processor'] = 'unknown'

    if 'BOOTLOADER' in rules:
        if 'bootloader' in info_data:
            _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')

        info_data['bootloader'] = rules['BOOTLOADER']
    else:
        info_data['bootloader'] = 'atmel-dfu'

    # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
    # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'



@@ 463,10 589,13 @@ def merge_info_jsons(keyboard, info_data):
    for info_file in find_info_json(keyboard):
        # Load and validate the JSON data
        try:
            new_info_data = json.load(info_file.open('r'))
        except Exception as e:
            _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
            new_info_data = {}
            new_info_data = _json_load(info_file)
            keyboard_validate(new_info_data)

        except jsonschema.ValidationError as e:
            cli.log.error('Invalid info.json data: %s', e.message)
            cli.log.error('Not including file %s', info_file)
            continue

        if not isinstance(new_info_data, dict):
            _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))


@@ 479,7 608,7 @@ def merge_info_jsons(keyboard, info_data):

        # Deep merge certain keys
        # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something.
        for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
        for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'):
            if key in new_info_data:
                if key not in info_data:
                    info_data[key] = {}

M requirements.txt => requirements.txt +1 -0
@@ 3,5 3,6 @@ appdirs
argcomplete
colorama
hjson
jsonschema
milc
pygments