From 47b9b110097a864d6ab76516b2213afd59948527 Mon Sep 17 00:00:00 2001 From: Zach White Date: Wed, 30 Dec 2020 10:27:37 -0800 Subject: Configure keyboard matrix from info.json (#10817) * Make parameters from info.json available to the build system * move all clueboard settings to info.json * code formatting * make flake8 happy * make flake8 happy * make qmk lint happy * Add support for specifying led indicators in json * move led indicators to the clueboard info.json * Apply suggestions from code review Co-authored-by: Erovia * add missing docstring Co-authored-by: Erovia --- lib/python/qmk/cli/c2json.py | 3 +- lib/python/qmk/cli/chibios/confmigrate.py | 8 +- lib/python/qmk/cli/generate/__init__.py | 4 + lib/python/qmk/cli/generate/api.py | 14 +- lib/python/qmk/cli/generate/config_h.py | 277 +++++++++++++++++++++++ lib/python/qmk/cli/generate/info_json.py | 49 +++++ lib/python/qmk/cli/generate/layouts.py | 93 ++++++++ lib/python/qmk/cli/generate/rules_mk.py | 59 +++++ lib/python/qmk/cli/info.py | 7 +- lib/python/qmk/cli/kle2json.py | 51 ++--- lib/python/qmk/constants.py | 11 + lib/python/qmk/info.py | 355 +++++++++++++++++++++++++----- lib/python/qmk/info_json_encoder.py | 96 ++++++++ 13 files changed, 920 insertions(+), 107 deletions(-) create mode 100755 lib/python/qmk/cli/generate/config_h.py create mode 100755 lib/python/qmk/cli/generate/info_json.py create mode 100755 lib/python/qmk/cli/generate/layouts.py create mode 100755 lib/python/qmk/cli/generate/rules_mk.py create mode 100755 lib/python/qmk/info_json_encoder.py (limited to 'lib') diff --git a/lib/python/qmk/cli/c2json.py b/lib/python/qmk/cli/c2json.py index 2b3bb774f7..8f9d8dc383 100644 --- a/lib/python/qmk/cli/c2json.py +++ b/lib/python/qmk/cli/c2json.py @@ -6,6 +6,7 @@ from milc import cli import qmk.keymap import qmk.path +from qmk.info_json_encoder import InfoJSONEncoder @cli.argument('--no-cpp', arg_only=True, action='store_false', help='Do not use \'cpp\' on keymap.c') @@ -47,7 +48,7 @@ def c2json(cli): cli.args.output.parent.mkdir(parents=True, exist_ok=True) if cli.args.output.exists(): cli.args.output.replace(cli.args.output.name + '.bak') - cli.args.output.write_text(json.dumps(keymap_json)) + cli.args.output.write_text(json.dumps(keymap_json, cls=InfoJSONEncoder)) if not cli.args.quiet: cli.log.info('Wrote keymap to %s.', cli.args.output) diff --git a/lib/python/qmk/cli/chibios/confmigrate.py b/lib/python/qmk/cli/chibios/confmigrate.py index eae294a0c6..b9cfda9614 100644 --- a/lib/python/qmk/cli/chibios/confmigrate.py +++ b/lib/python/qmk/cli/chibios/confmigrate.py @@ -13,7 +13,7 @@ def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) -fileHeader = """\ +file_header = """\ /* Copyright 2020 QMK * * This program is free software: you can redistribute it and/or modify @@ -77,7 +77,7 @@ def check_diffs(input_defs, reference_defs): def migrate_chconf_h(to_override, outfile): - print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) + print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) for override in to_override: print("#define %s %s" % (override[0], override[1]), file=outfile) @@ -87,7 +87,7 @@ def migrate_chconf_h(to_override, outfile): def migrate_halconf_h(to_override, outfile): - print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) + print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) for override in to_override: print("#define %s %s" % (override[0], override[1]), file=outfile) @@ -97,7 +97,7 @@ def migrate_halconf_h(to_override, outfile): def migrate_mcuconf_h(to_override, outfile): - print(fileHeader.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) + print(file_header.format(cli.args.input.relative_to(QMK_FIRMWARE), cli.args.reference.relative_to(QMK_FIRMWARE)), file=outfile) print("#include_next \n", file=outfile) diff --git a/lib/python/qmk/cli/generate/__init__.py b/lib/python/qmk/cli/generate/__init__.py index f9585bfb5c..bd75b044c5 100644 --- a/lib/python/qmk/cli/generate/__init__.py +++ b/lib/python/qmk/cli/generate/__init__.py @@ -1,3 +1,7 @@ from . import api +from . import config_h from . import docs +from . import info_json +from . import layouts from . import rgb_breathe_table +from . import rules_mk diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 66db37cb52..6d111f244c 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -8,6 +8,7 @@ from milc import cli from qmk.datetime import current_datetime from qmk.info import info_json +from qmk.info_json_encoder import InfoJSONEncoder from qmk.keyboard import list_keyboards @@ -44,15 +45,16 @@ def generate_api(cli): if 'usb' in kb_all['keyboards'][keyboard_name]: usb = kb_all['keyboards'][keyboard_name]['usb'] - if usb['vid'] not in usb_list['devices']: + if 'vid' in usb and usb['vid'] not in usb_list['devices']: usb_list['devices'][usb['vid']] = {} - if usb['pid'] not in usb_list['devices'][usb['vid']]: + if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]: usb_list['devices'][usb['vid']][usb['pid']] = {} - usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb + if 'vid' in usb and 'pid' in usb: + usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb # Write the global JSON files - keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])})) - keyboard_all.write_text(json.dumps(kb_all)) - usb_file.write_text(json.dumps(usb_list)) + keyboard_list.write_text(json.dumps({'last_updated': current_datetime(), 'keyboards': sorted(kb_all['keyboards'])}, cls=InfoJSONEncoder)) + keyboard_all.write_text(json.dumps(kb_all, cls=InfoJSONEncoder)) + usb_file.write_text(json.dumps(usb_list, cls=InfoJSONEncoder)) diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py new file mode 100755 index 0000000000..4d734017a1 --- /dev/null +++ b/lib/python/qmk/cli/generate/config_h.py @@ -0,0 +1,277 @@ +"""Used by the make system to generate info_config.h from info.json. +""" +from milc import cli + +from qmk.constants import LED_INDICATORS +from qmk.decorators import automagic_keyboard, automagic_keymap +from qmk.info import info_json, rgblight_animations, rgblight_properties, rgblight_toggles +from qmk.path import is_keyboard, normpath + +usb_properties = { + 'vid': 'VENDOR_ID', + 'pid': 'PRODUCT_ID', + 'device_ver': 'DEVICE_VER', +} + + +def debounce(debounce): + """Return the config.h lines that set debounce + """ + return """ +#ifndef DEBOUNCE +# define DEBOUNCE %s +#endif // DEBOUNCE +""" % debounce + + +def diode_direction(diode_direction): + """Return the config.h lines that set diode direction + """ + return """ +#ifndef DIODE_DIRECTION +# define DIODE_DIRECTION %s +#endif // DIODE_DIRECTION +""" % diode_direction + + +def keyboard_name(keyboard_name): + """Return the config.h lines that set the keyboard's name. + """ + return """ +#ifndef DESCRIPTION +# define DESCRIPTION %s +#endif // DESCRIPTION + +#ifndef PRODUCT +# define PRODUCT %s +#endif // PRODUCT +""" % (keyboard_name, keyboard_name) + + +def manufacturer(manufacturer): + """Return the config.h lines that set the manufacturer. + """ + return """ +#ifndef MANUFACTURER +# define MANUFACTURER %s +#endif // MANUFACTURER +""" % (manufacturer) + + +def direct_pins(direct_pins): + """Return the config.h lines that set the direct pins. + """ + rows = [] + + for row in direct_pins: + cols = ','.join([col or 'NO_PIN' for col in row]) + rows.append('{' + cols + '}') + + col_count = len(direct_pins[0]) + row_count = len(direct_pins) + + return """ +#ifndef MATRIX_COLS +# define MATRIX_COLS %s +#endif // MATRIX_COLS + +#ifndef MATRIX_ROWS +# define MATRIX_ROWS %s +#endif // MATRIX_ROWS + +#ifndef DIRECT_PINS +# define DIRECT_PINS {%s} +#endif // DIRECT_PINS +""" % (col_count, row_count, ','.join(rows)) + + +def col_pins(col_pins): + """Return the config.h lines that set the column pins. + """ + cols = ','.join(col_pins) + col_num = len(col_pins) + + return """ +#ifndef MATRIX_COLS +# define MATRIX_COLS %s +#endif // MATRIX_COLS + +#ifndef MATRIX_COL_PINS +# define MATRIX_COL_PINS {%s} +#endif // MATRIX_COL_PINS +""" % (col_num, cols) + + +def row_pins(row_pins): + """Return the config.h lines that set the row pins. + """ + rows = ','.join(row_pins) + row_num = len(row_pins) + + return """ +#ifndef MATRIX_ROWS +# define MATRIX_ROWS %s +#endif // MATRIX_ROWS + +#ifndef MATRIX_ROW_PINS +# define MATRIX_ROW_PINS {%s} +#endif // MATRIX_ROW_PINS +""" % (row_num, rows) + + +def indicators(config): + """Return the config.h lines that setup LED indicators. + """ + defines = [] + + for led, define in LED_INDICATORS.items(): + if led in config: + defines.append('') + defines.append('#ifndef %s' % (define,)) + defines.append('# define %s %s' % (define, config[led])) + defines.append('#endif // %s' % (define,)) + + return '\n'.join(defines) + + +def layout_aliases(layout_aliases): + """Return the config.h lines that setup layout aliases. + """ + defines = [] + + for alias, layout in layout_aliases.items(): + defines.append('') + defines.append('#ifndef %s' % (alias,)) + defines.append('# define %s %s' % (alias, layout)) + defines.append('#endif // %s' % (alias,)) + + return '\n'.join(defines) + + +def matrix_pins(matrix_pins): + """Add the matrix config to the config.h. + """ + pins = [] + + if 'direct' in matrix_pins: + pins.append(direct_pins(matrix_pins['direct'])) + + if 'cols' in matrix_pins: + pins.append(col_pins(matrix_pins['cols'])) + + if 'rows' in matrix_pins: + pins.append(row_pins(matrix_pins['rows'])) + + return '\n'.join(pins) + + +def rgblight(config): + """Return the config.h lines that setup rgblight. + """ + rgblight_config = [] + + for json_key, config_key in rgblight_properties.items(): + if json_key in config: + rgblight_config.append('') + rgblight_config.append('#ifndef %s' % (config_key,)) + rgblight_config.append('# define %s %s' % (config_key, config[json_key])) + rgblight_config.append('#endif // %s' % (config_key,)) + + for json_key, config_key in rgblight_toggles.items(): + if config.get(json_key): + rgblight_config.append('') + rgblight_config.append('#ifndef %s' % (config_key,)) + rgblight_config.append('# define %s' % (config_key,)) + rgblight_config.append('#endif // %s' % (config_key,)) + + for json_key, config_key in rgblight_animations.items(): + if 'animations' in config and config['animations'].get(json_key): + rgblight_config.append('') + rgblight_config.append('#ifndef %s' % (config_key,)) + rgblight_config.append('# define %s' % (config_key,)) + rgblight_config.append('#endif // %s' % (config_key,)) + + return '\n'.join(rgblight_config) + + +def usb_properties(usb_props): + """Return the config.h lines that setup USB params. + """ + usb_lines = [] + + for info_name, config_name in usb_props.items(): + if info_name in usb_props: + usb_lines.append('') + usb_lines.append('#ifndef ' + config_name) + usb_lines.append('# define %s %s' % (config_name, usb_props[info_name])) + usb_lines.append('#endif // ' + config_name) + + return '\n'.join(usb_lines) + + +@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', 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 paramater: --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.json file + kb_info_json = info_json(cli.config.generate_config_h.keyboard) + + # Build the info_config.h file. + config_h_lines = ['/* This file was generated by `qmk generate-config-h`. Do not edit or copy.' ' */', '', '#pragma once'] + + if 'debounce' in kb_info_json: + config_h_lines.append(debounce(kb_info_json['debounce'])) + + if 'diode_direction' in kb_info_json: + config_h_lines.append(diode_direction(kb_info_json['diode_direction'])) + + if 'indicators' in kb_info_json: + config_h_lines.append(indicators(kb_info_json['indicators'])) + + if 'keyboard_name' in kb_info_json: + config_h_lines.append(keyboard_name(kb_info_json['keyboard_name'])) + + if 'layout_aliases' in kb_info_json: + config_h_lines.append(layout_aliases(kb_info_json['layout_aliases'])) + + if 'manufacturer' in kb_info_json: + config_h_lines.append(manufacturer(kb_info_json['manufacturer'])) + + if 'rgblight' in kb_info_json: + config_h_lines.append(rgblight(kb_info_json['rgblight'])) + + if 'matrix_pins' in kb_info_json: + config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) + + if 'usb' in kb_info_json: + config_h_lines.append(usb_properties(kb_info_json['usb'])) + + # Show the results + config_h = '\n'.join(config_h_lines) + + if cli.args.output: + cli.args.output.parent.mkdir(parents=True, exist_ok=True) + if cli.args.output.exists(): + cli.args.output.replace(cli.args.output.name + '.bak') + cli.args.output.write_text(config_h) + + if not cli.args.quiet: + cli.log.info('Wrote info_config.h to %s.', cli.args.output) + + else: + print(config_h) diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py new file mode 100755 index 0000000000..7e6654e45d --- /dev/null +++ b/lib/python/qmk/cli/generate/info_json.py @@ -0,0 +1,49 @@ +"""Keyboard information script. + +Compile an info.json for a particular keyboard and pretty-print it. +""" +import json + +from milc import cli + +from qmk.info_json_encoder import InfoJSONEncoder +from qmk.decorators import automagic_keyboard, automagic_keymap +from qmk.info import info_json +from qmk.path import is_keyboard + + +@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.') +@cli.argument('-km', '--keymap', help='Show the layers for a JSON keymap too.') +@cli.subcommand('Generate an info.json file for a keyboard.', hidden=False if cli.config.user.developer else True) +@automagic_keyboard +@automagic_keymap +def generate_info_json(cli): + """Generate an info.json file for a keyboard + """ + # Determine our keyboard(s) + if not cli.config.generate_info_json.keyboard: + cli.log.error('Missing paramater: --keyboard') + cli.subcommands['info'].print_help() + return False + + if not is_keyboard(cli.config.generate_info_json.keyboard): + cli.log.error('Invalid keyboard: "%s"', cli.config.generate_info_json.keyboard) + return False + + # Build the info.json file + kb_info_json = info_json(cli.config.generate_info_json.keyboard) + pared_down_json = {} + + for key in ('manufacturer', 'maintainer', 'usb', 'keyboard_name', 'width', 'height', 'debounce', 'diode_direction', 'features', 'community_layouts', 'layout_aliases', 'matrix_pins', 'rgblight', 'url'): + if key in kb_info_json: + pared_down_json[key] = kb_info_json[key] + + pared_down_json['layouts'] = {} + if 'layouts' in pared_down_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'])) + pared_down_json['layouts'][layout_name]['layout'] = layout['layout'] + + # Display the results + print(json.dumps(pared_down_json, indent=2, cls=InfoJSONEncoder)) diff --git a/lib/python/qmk/cli/generate/layouts.py b/lib/python/qmk/cli/generate/layouts.py new file mode 100755 index 0000000000..809f0ef7ec --- /dev/null +++ b/lib/python/qmk/cli/generate/layouts.py @@ -0,0 +1,93 @@ +"""Used by the make system to generate layouts.h from info.json. +""" +from milc import cli + +from qmk.constants import COL_LETTERS, ROW_LETTERS +from qmk.decorators import automagic_keyboard, automagic_keymap +from qmk.info import info_json +from qmk.path import is_keyboard, normpath + +usb_properties = { + 'vid': 'VENDOR_ID', + 'pid': 'PRODUCT_ID', + 'device_ver': 'DEVICE_VER', +} + + +@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', help='Keyboard to generate config.h for.') +@cli.subcommand('Used by the make system to generate layouts.h from info.json', hidden=True) +@automagic_keyboard +@automagic_keymap +def generate_layouts(cli): + """Generates the layouts.h file. + """ + # Determine our keyboard(s) + if not cli.config.generate_layouts.keyboard: + cli.log.error('Missing paramater: --keyboard') + cli.subcommands['info'].print_help() + return False + + if not is_keyboard(cli.config.generate_layouts.keyboard): + cli.log.error('Invalid keyboard: "%s"', cli.config.generate_layouts.keyboard) + return False + + # Build the info.json file + kb_info_json = info_json(cli.config.generate_layouts.keyboard) + + # Build the layouts.h file. + layouts_h_lines = ['/* This file was generated by `qmk generate-layouts`. Do not edit or copy.' ' */', '', '#pragma once'] + + if 'direct' in kb_info_json['matrix_pins']: + col_num = len(kb_info_json['matrix_pins']['direct'][0]) + row_num = len(kb_info_json['matrix_pins']['direct']) + elif 'cols' in kb_info_json['matrix_pins'] and 'rows' in kb_info_json['matrix_pins']: + col_num = len(kb_info_json['matrix_pins']['cols']) + row_num = len(kb_info_json['matrix_pins']['rows']) + else: + cli.log.error('%s: Invalid matrix config.', cli.config.generate_layouts.keyboard) + return False + + for layout_name in kb_info_json['layouts']: + if kb_info_json['layouts'][layout_name]['c_macro']: + continue + + layout_keys = [] + layout_matrix = [['KC_NO' for i in range(col_num)] for i in range(row_num)] + + for i, key in enumerate(kb_info_json['layouts'][layout_name]['layout']): + row = key['matrix'][0] + col = key['matrix'][1] + identifier = 'k%s%s' % (ROW_LETTERS[row], COL_LETTERS[col]) + + try: + layout_matrix[row][col] = identifier + layout_keys.append(identifier) + except IndexError: + key_name = key.get('label', identifier) + cli.log.error('Matrix data out of bounds for layout %s at index %s (%s): %s, %s', layout_name, i, key_name, row, col) + return False + + layouts_h_lines.append('') + layouts_h_lines.append('#define %s(%s) {\\' % (layout_name, ', '.join(layout_keys))) + + rows = ', \\\n'.join(['\t {' + ', '.join(row) + '}' for row in layout_matrix]) + rows += ' \\' + layouts_h_lines.append(rows) + layouts_h_lines.append('}') + + # Show the results + layouts_h = '\n'.join(layouts_h_lines) + '\n' + + if cli.args.output: + cli.args.output.parent.mkdir(parents=True, exist_ok=True) + if cli.args.output.exists(): + cli.args.output.replace(cli.args.output.name + '.bak') + cli.args.output.write_text(layouts_h) + + if not cli.args.quiet: + cli.log.info('Wrote info_config.h to %s.', cli.args.output) + + else: + print(layouts_h) diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py new file mode 100755 index 0000000000..4268ae047b --- /dev/null +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -0,0 +1,59 @@ +"""Used by the make system to generate a rules.mk +""" +from milc import cli + +from qmk.decorators import automagic_keyboard, automagic_keymap +from qmk.info import info_json +from qmk.path import is_keyboard, normpath + + +@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', 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_rules_mk(cli): + """Generates a rules.mk file from info.json. + """ + # Determine our keyboard(s) + if not cli.config.generate_rules_mk.keyboard: + cli.log.error('Missing paramater: --keyboard') + cli.subcommands['info'].print_help() + return False + + if not is_keyboard(cli.config.generate_rules_mk.keyboard): + cli.log.error('Invalid keyboard: "%s"', cli.config.generate_rules_mk.keyboard) + return False + + # Build the info.json file + 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.', ''] + + # Find features that should be enabled + if 'features' in kb_info_json: + for feature, enabled in kb_info_json['features'].items(): + feature = feature.upper() + enabled = 'yes' if enabled else 'no' + rules_mk_lines.append(f'{feature}_ENABLE := {enabled}') + + # Add community layouts + if 'community_layouts' in kb_info_json: + rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}') + + # Show the results + rules_mk = '\n'.join(rules_mk_lines) + '\n' + + if cli.args.output: + cli.args.output.parent.mkdir(parents=True, exist_ok=True) + if cli.args.output.exists(): + cli.args.output.replace(cli.args.output.name + '.bak') + cli.args.output.write_text(rules_mk) + + if cli.args.quiet: + print(cli.args.output) + else: + cli.log.info('Wrote info_config.h to %s.', cli.args.output) + + else: + print(rules_mk) diff --git a/lib/python/qmk/cli/info.py b/lib/python/qmk/cli/info.py index 9ab299a21e..87d7253d4b 100755 --- a/lib/python/qmk/cli/info.py +++ b/lib/python/qmk/cli/info.py @@ -7,6 +7,8 @@ import platform from milc import cli +from qmk.info_json_encoder import InfoJSONEncoder +from qmk.constants import COL_LETTERS, ROW_LETTERS from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.keyboard import render_layouts, render_layout from qmk.keymap import locate_keymap @@ -15,9 +17,6 @@ from qmk.path import is_keyboard platform_id = platform.platform().lower() -ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop' -COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz' - def show_keymap(kb_info_json, title_caps=True): """Render the keymap in ascii art. @@ -149,7 +148,7 @@ def info(cli): # Output in the requested format if cli.args.format == 'json': - print(json.dumps(kb_info_json)) + print(json.dumps(kb_info_json, cls=InfoJSONEncoder)) elif cli.args.format == 'text': print_text_output(kb_info_json) elif cli.args.format == 'friendly': diff --git a/lib/python/qmk/cli/kle2json.py b/lib/python/qmk/cli/kle2json.py index 3d1bb8c43c..66d504bfc2 100755 --- a/lib/python/qmk/cli/kle2json.py +++ b/lib/python/qmk/cli/kle2json.py @@ -3,25 +3,12 @@ import json import os from pathlib import Path -from decimal import Decimal -from collections import OrderedDict from milc import cli from kle2xy import KLE2xy from qmk.converter import kle2qmk - - -class CustomJSONEncoder(json.JSONEncoder): - def default(self, obj): - try: - if isinstance(obj, Decimal): - if obj % 2 in (Decimal(0), Decimal(1)): - return int(obj) - return float(obj) - except TypeError: - pass - return json.JSONEncoder.default(self, obj) +from qmk.info_json_encoder import InfoJSONEncoder @cli.argument('filename', help='The KLE raw txt to convert') @@ -52,24 +39,22 @@ def kle2json(cli): cli.log.error('Could not parse KLE raw data: %s', raw_code) cli.log.exception(e) return False - keyboard = OrderedDict( - keyboard_name=kle.name, - url='', - maintainer='qmk', - width=kle.columns, - height=kle.rows, - layouts={'LAYOUT': { - 'layout': 'LAYOUT_JSON_HERE' - }}, - ) - # Initialize keyboard with json encoded from ordered dict - keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=CustomJSONEncoder) - # Initialize layout with kle2qmk from converter module - layout = json.dumps(kle2qmk(kle), separators=(', ', ':'), cls=CustomJSONEncoder) - # Replace layout in keyboard json - keyboard = keyboard.replace('"LAYOUT_JSON_HERE"', layout) + keyboard = { + 'keyboard_name': kle.name, + 'url': '', + 'maintainer': 'qmk', + 'width': kle.columns, + 'height': kle.rows, + 'layouts': { + 'LAYOUT': { + 'layout': kle2qmk(kle) + } + }, + } + # Write our info.json - file = open(out_path / "info.json", "w") - file.write(keyboard) - file.close() + keyboard = json.dumps(keyboard, indent=4, separators=(', ', ': '), sort_keys=False, cls=InfoJSONEncoder) + info_json_file = out_path / 'info.json' + + info_json_file.write_text(keyboard) cli.log.info('Wrote out {fg_cyan}%s/info.json', out_path) diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 94ab68e5e4..675832c506 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -17,3 +17,14 @@ VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' DATE_FORMAT = '%Y-%m-%d' DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z' TIME_FORMAT = '%H:%M:%S' + +# Used when generating matrix locations +COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz' +ROW_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop' + +# Mapping between info.json and config.h keys +LED_INDICATORS = { + 'caps_lock': 'LED_CAPS_LOCK_PIN', + 'num_lock': 'LED_NUM_LOCK_PIN', + 'scrol_lock': 'LED_SCROLL_LOCK_PIN' +} diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index f476dc666d..d7b128aa66 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -6,13 +6,45 @@ from pathlib import Path from milc import cli -from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS +from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS from qmk.c_parse import find_layouts from qmk.keyboard import config_h, rules_mk from qmk.keymap import list_keymaps from qmk.makefile import parse_rules_mk_file from qmk.math import compute +rgblight_properties = { + 'led_count': 'RGBLED_NUM', + 'pin': 'RGB_DI_PIN', + 'split_count': 'RGBLED_SPLIT', + 'max_brightness': 'RGBLIGHT_LIMIT_VAL', + 'hue_steps': 'RGBLIGHT_HUE_STEP', + 'saturation_steps': 'RGBLIGHT_SAT_STEP', + 'brightness_steps': 'RGBLIGHT_VAL_STEP' +} + +rgblight_toggles = { + 'sleep': 'RGBLIGHT_SLEEP', + 'split': 'RGBLIGHT_SPLIT', +} + +rgblight_animations = { + 'all': 'RGBLIGHT_ANIMATIONS', + 'alternating': 'RGBLIGHT_EFFECT_ALTERNATING', + 'breathing': 'RGBLIGHT_EFFECT_BREATHING', + 'christmas': 'RGBLIGHT_EFFECT_CHRISTMAS', + 'knight': 'RGBLIGHT_EFFECT_KNIGHT', + 'rainbow_mood': 'RGBLIGHT_EFFECT_RAINBOW_MOOD', + 'rainbow_swirl': 'RGBLIGHT_EFFECT_RAINBOW_SWIRL', + 'rgb_test': 'RGBLIGHT_EFFECT_RGB_TEST', + 'snake': 'RGBLIGHT_EFFECT_SNAKE', + 'static_gradient': 'RGBLIGHT_EFFECT_STATIC_GRADIENT', + 'twinkle': 'RGBLIGHT_EFFECT_TWINKLE' +} + +true_values = ['1', 'on', 'yes'] +false_values = ['0', 'off', 'no'] + def info_json(keyboard): """Generate the info.json data for a specific keyboard. @@ -38,8 +70,9 @@ def info_json(keyboard): info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} # Populate layout data - for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): + for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items(): if not layout_name.startswith('LAYOUT_kc'): + layout_json['c_macro'] = True info_data['layouts'][layout_name] = layout_json # Merge in the data from info.json, config.h, and rules.mk @@ -47,34 +80,179 @@ def info_json(keyboard): info_data = _extract_config_h(info_data) info_data = _extract_rules_mk(info_data) + # 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.') + + # Make sure we supply layout macros for the community layouts we claim to support + # FIXME(skullydazed): This should be populated into info.json and read from there instead + if 'LAYOUTS' in rules and info_data.get('layouts'): + # Match these up against the supplied layouts + supported_layouts = rules['LAYOUTS'].strip().split() + for layout_name in sorted(info_data['layouts']): + layout_name = layout_name[7:] + + if layout_name in supported_layouts: + supported_layouts.remove(layout_name) + + if supported_layouts: + for supported_layout in supported_layouts: + _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout)) + return info_data -def _extract_config_h(info_data): - """Pull some keyboard information from existing rules.mk files +def _extract_debounce(info_data, config_c): + """Handle debounce. + """ + if 'debounce' in info_data and 'DEBOUNCE' in 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') + + return info_data + + +def _extract_diode_direction(info_data, config_c): + """Handle the diode direction. + """ + if 'diode_direction' in info_data and 'DIODE_DIRECTION' in config_c: + _log_warning(info_data, 'Diode direction is specified in both info.json and config.h, the config.h value wins.') + + if 'DIODE_DIRECTION' in config_c: + info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') + + return info_data + + +def _extract_indicators(info_data, config_c): + """Find the LED indicator information. + """ + for json_key, config_key in LED_INDICATORS.items(): + if json_key in info_data.get('indicators', []) and config_key in config_c: + _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.') + + if config_key in config_c: + info_data['indicators'][json_key] = config_c.get(config_key) + + return info_data + + +def _extract_community_layouts(info_data, rules): + """Find the community layouts in rules.mk. + """ + community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else [] + + if 'community_layouts' in info_data: + for layout in community_layouts: + if layout not in info_data['community_layouts']: + community_layouts.append(layout) + + else: + info_data['community_layouts'] = community_layouts + + return info_data + + +def _extract_features(info_data, rules): + """Find all the features enabled in rules.mk. + """ + for key, value in rules.items(): + if key.endswith('_ENABLE'): + key = '_'.join(key.split('_')[:-1]).lower() + value = True if value in true_values else False if value in false_values else value + + if 'config_h_features' not in info_data: + info_data['config_h_features'] = {} + + if 'features' not in info_data: + info_data['features'] = {} + + if key in info_data['features']: + _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,)) + + info_data['features'][key] = value + info_data['config_h_features'][key] = value + + return info_data + + +def _extract_rgblight(info_data, config_c): + """Handle the rgblight configuration + """ + rgblight = info_data.get('rgblight', {}) + animations = rgblight.get('animations', {}) + + for json_key, config_key in rgblight_properties.items(): + if config_key in config_c: + if json_key in rgblight: + _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,)) + + rgblight[json_key] = config_c[config_key] + + for json_key, config_key in rgblight_toggles.items(): + if config_key in config_c: + if json_key in rgblight: + _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key) + + rgblight[json_key] = config_c[config_key] + + for json_key, config_key in rgblight_animations.items(): + if config_key in config_c: + if json_key in animations: + _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,)) + + animations[json_key] = config_c[config_key] + + if animations: + rgblight['animations'] = animations + + if rgblight: + info_data['rgblight'] = rgblight + + return info_data + + +def _extract_matrix_info(info_data, config_c): + """Populate the matrix information. """ - config_c = config_h(info_data['keyboard_folder']) row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] - info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') - info_data['matrix_size'] = { - 'rows': compute(config_c.get('MATRIX_ROWS', '0')), - 'cols': compute(config_c.get('MATRIX_COLS', '0')), - } - info_data['matrix_pins'] = {} + if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c: + if 'matrix_size' in info_data: + _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.') - if row_pins: - info_data['matrix_pins']['rows'] = row_pins.split(',') - if col_pins: - info_data['matrix_pins']['cols'] = col_pins.split(',') + info_data['matrix_size'] = { + 'rows': compute(config_c.get('MATRIX_ROWS', '0')), + 'cols': compute(config_c.get('MATRIX_COLS', '0')), + } + + if row_pins and col_pins: + if 'matrix_pins' in info_data: + _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.') + + info_data['matrix_pins'] = {} + + if row_pins: + info_data['matrix_pins']['rows'] = row_pins.split(',') + + if col_pins: + info_data['matrix_pins']['cols'] = col_pins.split(',') if direct_pins: + if 'matrix_pins' in info_data: + _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_pin_array = [] + for row in direct_pins.split('},{'): if row.startswith('{'): row = row[1:] + if row.endswith('}'): row = row[:-1] @@ -86,15 +264,43 @@ def _extract_config_h(info_data): direct_pin_array[-1].append(pin) - info_data['matrix_pins']['direct'] = direct_pin_array + info_data['matrix_pins']['direct'] = direct_pin_array - info_data['usb'] = { - 'vid': config_c.get('VENDOR_ID'), - 'pid': config_c.get('PRODUCT_ID'), - 'device_ver': config_c.get('DEVICE_VER'), - 'manufacturer': config_c.get('MANUFACTURER'), - 'product': config_c.get('PRODUCT'), - } + return info_data + + +def _extract_usb_info(info_data, config_c): + """Populate the USB information. + """ + usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'} + + if 'usb' not in info_data: + info_data['usb'] = {} + + for info_name, config_name in usb_properties.items(): + if config_name in config_c: + if info_name in info_data['usb']: + _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name)) + + info_data['usb'][info_name] = config_c[config_name] + + elif info_name not in info_data['usb']: + _log_error(info_data, '%s not specified in config.h, and %s not specified in info.json. One is required.' % (config_name, info_name)) + + return info_data + + +def _extract_config_h(info_data): + """Pull some keyboard information from existing config.h files + """ + config_c = config_h(info_data['keyboard_folder']) + + _extract_debounce(info_data, config_c) + _extract_diode_direction(info_data, config_c) + _extract_indicators(info_data, config_c) + _extract_matrix_info(info_data, config_c) + _extract_usb_info(info_data, config_c) + _extract_rgblight(info_data, config_c) return info_data @@ -106,16 +312,52 @@ def _extract_rules_mk(info_data): mcu = rules.get('MCU') if mcu in CHIBIOS_PROCESSORS: - return arm_processor_rules(info_data, rules) + arm_processor_rules(info_data, rules) elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: - return avr_processor_rules(info_data, rules) + avr_processor_rules(info_data, rules) + + else: + cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu)) + unknown_processor_rules(info_data, rules) + + _extract_community_layouts(info_data, rules) + _extract_features(info_data, rules) + + return info_data - msg = "Unknown MCU: " + str(mcu) - _log_warning(info_data, msg) +def _merge_layouts(info_data, new_info_data): + """Merge new_info_data into info_data in an intelligent way. + """ + for layout_name, layout_json in new_info_data['layouts'].items(): + if layout_name in info_data['layouts']: + # Pull in layouts we have a macro for + if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']): + msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s' + _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout']))) + else: + for i, key in enumerate(info_data['layouts'][layout_name]['layout']): + key.update(layout_json['layout'][i]) + else: + # Pull in layouts that have matrix data + missing_matrix = False + for key in layout_json['layout']: + if 'matrix' not in key: + missing_matrix = True + + if not missing_matrix: + if layout_name in info_data['layouts']: + # Update an existing layout with new data + for i, key in enumerate(info_data['layouts'][layout_name]['layout']): + key.update(layout_json['layout'][i]) - return unknown_processor_rules(info_data, rules) + else: + # Copy in the new layout wholesale + layout_json['c_macro'] = False + info_data['layouts'][layout_name] = layout_json + + return info_data def _search_keyboard_h(path): @@ -131,34 +373,21 @@ def _search_keyboard_h(path): return layouts -def _find_all_layouts(info_data, keyboard, rules): +def _find_all_layouts(info_data, keyboard): """Looks for layout macros associated with this keyboard. """ layouts = _search_keyboard_h(Path(keyboard)) if not layouts: - # If we didn't find any layouts above we widen our search. This is error - # prone which is why we want to encourage people to follow the standard above. - _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.') + # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above. + info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard)) + for file in glob('keyboards/%s/*.h' % keyboard): if file.endswith('.h'): these_layouts = find_layouts(file) if these_layouts: layouts.update(these_layouts) - if 'LAYOUTS' in rules: - # Match these up against the supplied layouts - supported_layouts = rules['LAYOUTS'].strip().split() - for layout_name in sorted(layouts): - if not layout_name.startswith('LAYOUT_'): - continue - layout_name = layout_name[7:] - if layout_name in supported_layouts: - supported_layouts.remove(layout_name) - - if supported_layouts: - _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts))) - return layouts @@ -231,32 +460,40 @@ def merge_info_jsons(keyboard, info_data): for info_file in find_info_json(keyboard): # Load and validate the JSON data try: - with info_file.open('r') as info_fd: - new_info_data = json.load(info_fd) + 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)) - continue + new_info_data = {} if not isinstance(new_info_data, dict): _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) continue # Copy whitelisted keys into `info_data` - for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): + for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): if key in new_info_data: info_data[key] = new_info_data[key] - # Merge the layouts in + # 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'): + if key in new_info_data: + if key not in info_data: + info_data[key] = {} + + info_data[key].update(new_info_data[key]) + + # Merge the layouts + if 'community_layouts' in new_info_data: + if 'community_layouts' in info_data: + for layout in new_info_data['community_layouts']: + if layout not in info_data['community_layouts']: + info_data['community_layouts'].append(layout) + else: + info_data['community_layouts'] = new_info_data['community_layouts'] + if 'layouts' in new_info_data: - for layout_name, json_layout in new_info_data['layouts'].items(): - # Only pull in layouts we have a macro for - if layout_name in info_data['layouts']: - if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']): - msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s' - _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout']))) - else: - for i, key in enumerate(info_data['layouts'][layout_name]['layout']): - key.update(json_layout['layout'][i]) + _merge_layouts(info_data, new_info_data) return info_data diff --git a/lib/python/qmk/info_json_encoder.py b/lib/python/qmk/info_json_encoder.py new file mode 100755 index 0000000000..60dae7247f --- /dev/null +++ b/lib/python/qmk/info_json_encoder.py @@ -0,0 +1,96 @@ +"""Class that pretty-prints QMK info.json files. +""" +import json +from decimal import Decimal + + +class InfoJSONEncoder(json.JSONEncoder): + """Custom encoder to make info.json's a little nicer to work with. + """ + container_types = (list, tuple, dict) + indentation_char = " " + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.indentation_level = 0 + + if not self.indent: + self.indent = 4 + + def encode(self, obj): + """Encode JSON objects for QMK. + """ + if isinstance(obj, Decimal): + if obj == int(obj): # I can't believe Decimal objects don't have .is_integer() + return int(obj) + return float(obj) + + elif isinstance(obj, (list, tuple)): + if self._primitives_only(obj): + return "[" + ", ".join(self.encode(element) for element in obj) + "]" + + else: + self.indentation_level += 1 + output = [self.indent_str + self.encode(element) for element in obj] + self.indentation_level -= 1 + return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]" + + elif isinstance(obj, dict): + if obj: + if self.indentation_level == 4: + # These are part of a layout, put them on a single line. + return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }" + + else: + self.indentation_level += 1 + output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value)}" for key, value in sorted(obj.items(), key=self.sort_root_dict)] + self.indentation_level -= 1 + return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}" + else: + return "{}" + else: + return super().encode(obj) + + def _primitives_only(self, obj): + """Returns true if the object doesn't have any container type objects (list, tuple, dict). + """ + if isinstance(obj, dict): + obj = obj.values() + + return not any(isinstance(element, self.container_types) for element in obj) + + def sort_root_dict(self, key): + """Forces layout to the back of the sort order. + """ + key = key[0] + + if self.indentation_level == 1: + if key == 'manufacturer': + return '10keyboard_name' + + elif key == 'keyboard_name': + return '11keyboard_name' + + elif key == 'maintainer': + return '12maintainer' + + elif key in ('height', 'width'): + return '40' + str(key) + + elif key == 'community_layouts': + return '97community_layouts' + + elif key == 'layout_aliases': + return '98layout_aliases' + + elif key == 'layouts': + return '99layouts' + + else: + return '50' + str(key) + + return key + + @property + def indent_str(self): + return self.indentation_char * (self.indentation_level * self.indent) -- cgit v1.2.3