From 17ec1650fd4fd27b3bf409e3493faf11c8d421e8 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Sat, 18 Jun 2022 06:30:46 +0100 Subject: Additional schema fixes (#17414) --- lib/python/qmk/cli/generate/info_json.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py index 284d1a8510..0dc80f10cc 100755 --- a/lib/python/qmk/cli/generate/info_json.py +++ b/lib/python/qmk/cli/generate/info_json.py @@ -5,7 +5,7 @@ Compile an info.json for a particular keyboard and pretty-print it. import json from argcomplete.completers import FilesCompleter -from jsonschema import Draft7Validator, RefResolver, validators +from jsonschema import Draft202012Validator, RefResolver, validators from milc import cli from pathlib import Path @@ -18,7 +18,7 @@ from qmk.path import is_keyboard, normpath def pruning_validator(validator_class): - """Extends Draft7Validator to remove properties that aren't specified in the schema. + """Extends Draft202012Validator to remove properties that aren't specified in the schema. """ validate_properties = validator_class.VALIDATORS["properties"] @@ -37,10 +37,10 @@ def strip_info_json(kb_info_json): """Remove the API-only properties from the info.json. """ schema_store = compile_schema_store() - pruning_draft_7_validator = pruning_validator(Draft7Validator) + pruning_draft_validator = pruning_validator(Draft202012Validator) schema = schema_store['qmk.keyboard.v1'] resolver = RefResolver.from_schema(schema_store['qmk.keyboard.v1'], store=schema_store) - validator = pruning_draft_7_validator(schema, resolver=resolver).validate + validator = pruning_draft_validator(schema, resolver=resolver).validate return validator(kb_info_json) -- cgit v1.2.3 From 1a400d8644a1f0763c68626863b897cb83c6c939 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Tue, 21 Jun 2022 04:15:06 +0100 Subject: Allow encoder config from info.json (#17295) --- data/mappings/info_rules.json | 1 + data/schemas/keyboard.jsonschema | 35 ++++++++++++++++++++ docs/reference_info_json.md | 36 ++++++++++++++++++++ lib/python/qmk/cli/generate/config_h.py | 29 +++++++++++++++++ lib/python/qmk/info.py | 58 +++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+) (limited to 'lib/python/qmk/cli/generate') diff --git a/data/mappings/info_rules.json b/data/mappings/info_rules.json index d4eec37ba0..5cdae962a5 100644 --- a/data/mappings/info_rules.json +++ b/data/mappings/info_rules.json @@ -13,6 +13,7 @@ "BOOTLOADER": {"info_key": "bootloader", "warn_duplicate": false}, "BLUETOOTH": {"info_key": "bluetooth.driver"}, "CAPS_WORD_ENABLE": {"info_key": "caps_word.enabled", "value_type": "bool"}, + "ENCODER_ENABLE": {"info_key": "encoder.enabled", "value_type": "bool"}, "FIRMWARE_FORMAT": {"info_key": "build.firmware_format"}, "KEYBOARD_SHARED_EP": {"info_key": "usb.shared_endpoint.keyboard", "value_type": "bool"}, "MOUSE_SHARED_EP": {"info_key": "usb.shared_endpoint.mouse", "value_type": "bool"}, diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 52a66cc132..8d9fc4754d 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -2,6 +2,26 @@ "$schema": "https://json-schema.org/draft/2020-12/schema#", "$id": "qmk.keyboard.v1", "title": "Keyboard Information", + "definitions": { + "encoder_config": { + "type": "object", + "properties": { + "rotary": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["pin_a", "pin_b"], + "properties": { + "pin_a": {"$ref": "qmk.definitions.v1#/mcu_pin"}, + "pin_b": {"$ref": "qmk.definitions.v1#/mcu_pin"}, + "resolution": {"$ref": "qmk.definitions.v1#/unsigned_int"} + } + } + } + } + } + }, "type": "object", "properties": { "keyboard_name": {"$ref": "qmk.definitions.v1#/text_identifier"}, @@ -113,6 +133,12 @@ "type": "array", "items": {"$ref": "qmk.definitions.v1#/filename"} }, + "encoder": { + "$ref": "#/definitions/encoder_config", + "properties": { + "enabled": {"type": "boolean"} + } + }, "features": {"$ref": "qmk.definitions.v1#/boolean_array"}, "indicators": { "type": "object", @@ -363,6 +389,15 @@ } } }, + "encoder": { + "type": "object", + "additionalProperties": false, + "properties": { + "right": { + "$ref": "#/definitions/encoder_config" + } + } + }, "main": { "type": "string", "enum": ["eeprom", "left", "matrix_grid", "pin", "right"] diff --git a/docs/reference_info_json.md b/docs/reference_info_json.md index 90b28689d0..d2e9346ee3 100644 --- a/docs/reference_info_json.md +++ b/docs/reference_info_json.md @@ -187,3 +187,39 @@ Example: ``` The device version is a BCD (binary coded decimal) value, in the format `MMmr`, so the below value would look like `0x0100` in the generated code. This also means the maximum valid values for each part are `99.9.9`, despite it being a hexadecimal value under the hood. + +### Encoders + +This section controls the basic [rotary encoder](feature_encoders.md) support. + +The following items can be set. Not every value is required. + +* `pin_a` + * __Required__. A pad definition +* `pin_b` + * __Required__. B pad definition +* `resolution` + * How many pulses the encoder registers between each detent + +Examples: + +```json +{ + "encoder": { + "rotary": [ + { "pin_a": "B5", "pin_b": "A2" } + ] + } +} +``` + +```json +{ + "encoder": { + "rotary": [ + { "pin_a": "B5", "pin_b": "A2", "resolution": 4 } + { "pin_a": "B6", "pin_b": "A3", "resolution": 2 } + ] + } +} +``` diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 893892c479..9d50368aba 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -134,6 +134,29 @@ def generate_config_items(kb_info_json, config_h_lines): config_h_lines.append(f'#endif // {config_key}') +def generate_encoder_config(encoder_json, config_h_lines, postfix=''): + """Generate the config.h lines for encoders.""" + a_pads = [] + b_pads = [] + resolutions = [] + for encoder in encoder_json.get("rotary", []): + a_pads.append(encoder["pin_a"]) + b_pads.append(encoder["pin_b"]) + resolutions.append(str(encoder.get("resolution", 4))) + + config_h_lines.append(f'#ifndef ENCODERS_PAD_A{postfix}') + config_h_lines.append(f'# define ENCODERS_PAD_A{postfix} {{ { ", ".join(a_pads) } }}') + config_h_lines.append(f'#endif // ENCODERS_PAD_A{postfix}') + + config_h_lines.append(f'#ifndef ENCODERS_PAD_B{postfix}') + config_h_lines.append(f'# define ENCODERS_PAD_B{postfix} {{ { ", ".join(b_pads) } }}') + config_h_lines.append(f'#endif // ENCODERS_PAD_B{postfix}') + + config_h_lines.append(f'#ifndef ENCODER_RESOLUTIONS{postfix}') + config_h_lines.append(f'# define ENCODER_RESOLUTIONS{postfix} {{ { ", ".join(resolutions) } }}') + config_h_lines.append(f'#endif // ENCODER_RESOLUTIONS{postfix}') + + def generate_split_config(kb_info_json, config_h_lines): """Generate the config.h lines for split boards.""" if 'primary' in kb_info_json['split']: @@ -173,6 +196,9 @@ def generate_split_config(kb_info_json, config_h_lines): if 'right' in kb_info_json['split'].get('matrix_pins', {}): config_h_lines.append(matrix_pins(kb_info_json['split']['matrix_pins']['right'], '_RIGHT')) + if 'right' in kb_info_json['split'].get('encoder', {}): + generate_encoder_config(kb_info_json['split']['encoder']['right'], config_h_lines, '_RIGHT') + @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @@ -198,6 +224,9 @@ def generate_config_h(cli): if 'matrix_pins' in kb_info_json: config_h_lines.append(matrix_pins(kb_info_json['matrix_pins'])) + if 'encoder' in kb_info_json: + generate_encoder_config(kb_info_json['encoder'], config_h_lines) + if 'split' in kb_info_json: generate_split_config(kb_info_json, config_h_lines) diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 23761d71b7..d308de9db8 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -218,6 +218,62 @@ def _extract_audio(info_data, config_c): info_data['audio'] = {'pins': audio_pins} +def _extract_encoders_values(config_c, postfix=''): + """Common encoder extraction logic + """ + a_pad = config_c.get(f'ENCODERS_PAD_A{postfix}', '').replace(' ', '')[1:-1] + b_pad = config_c.get(f'ENCODERS_PAD_B{postfix}', '').replace(' ', '')[1:-1] + resolutions = config_c.get(f'ENCODER_RESOLUTIONS{postfix}', '').replace(' ', '')[1:-1] + + default_resolution = config_c.get('ENCODER_RESOLUTION', '4') + + if a_pad and b_pad: + a_pad = list(filter(None, a_pad.split(','))) + b_pad = list(filter(None, b_pad.split(','))) + resolutions = list(filter(None, resolutions.split(','))) + resolutions += [default_resolution] * (len(a_pad) - len(resolutions)) + + encoders = [] + for index in range(len(a_pad)): + encoders.append({'pin_a': a_pad[index], 'pin_b': b_pad[index], "resolution": int(resolutions[index])}) + + return encoders + + +def _extract_encoders(info_data, config_c): + """Populate data about encoder pins + """ + encoders = _extract_encoders_values(config_c) + if encoders: + if 'encoder' not in info_data: + info_data['encoder'] = {} + + if 'rotary' in info_data['encoder']: + _log_warning(info_data, 'Encoder config is specified in both config.h and info.json (encoder.rotary) (Value: %s), the config.h value wins.' % info_data['encoder']['rotary']) + + info_data['encoder']['rotary'] = encoders + + +def _extract_split_encoders(info_data, config_c): + """Populate data about split encoder pins + """ + encoders = _extract_encoders_values(config_c, '_RIGHT') + if encoders: + if 'split' not in info_data: + info_data['split'] = {} + + if 'encoder' not in info_data['split']: + info_data['split']['encoder'] = {} + + if 'right' not in info_data['split']['encoder']: + info_data['split']['encoder']['right'] = {} + + if 'rotary' in info_data['split']['encoder']['right']: + _log_warning(info_data, 'Encoder config is specified in both config.h and info.json (encoder.rotary) (Value: %s), the config.h value wins.' % info_data['split']['encoder']['right']['rotary']) + + info_data['split']['encoder']['right']['rotary'] = encoders + + def _extract_secure_unlock(info_data, config_c): """Populate data about the secure unlock sequence """ @@ -506,6 +562,8 @@ def _extract_config_h(info_data, config_c): _extract_split_main(info_data, config_c) _extract_split_transport(info_data, config_c) _extract_split_right_pins(info_data, config_c) + _extract_encoders(info_data, config_c) + _extract_split_encoders(info_data, config_c) _extract_device_version(info_data) return info_data -- cgit v1.2.3 From 81d317aa8768fe53a6cca040297231278b06af64 Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Fri, 8 Jul 2022 22:48:48 +0100 Subject: Fix rgbkb/sol/rev2 build issues (#17601) --- lib/python/qmk/c_parse.py | 3 +++ lib/python/qmk/cli/generate/config_h.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 4b49b8d4e9..c14eb490fa 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -258,6 +258,9 @@ def _parse_led_config(file, matrix_cols, matrix_rows): position_raw.append(_coerce_led_token(_type, value)) if section == 3 and bracket_count == 2: flags.append(_coerce_led_token(_type, value)) + elif _type in [Token.Comment.Preproc]: + # TODO: Promote to error + return None # Slightly better intrim format matrix = list(_get_chunks(matrix_raw, matrix_cols)) diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 9d50368aba..b53f4ff335 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -152,9 +152,14 @@ def generate_encoder_config(encoder_json, config_h_lines, postfix=''): config_h_lines.append(f'# define ENCODERS_PAD_B{postfix} {{ { ", ".join(b_pads) } }}') config_h_lines.append(f'#endif // ENCODERS_PAD_B{postfix}') - config_h_lines.append(f'#ifndef ENCODER_RESOLUTIONS{postfix}') - config_h_lines.append(f'# define ENCODER_RESOLUTIONS{postfix} {{ { ", ".join(resolutions) } }}') - config_h_lines.append(f'#endif // ENCODER_RESOLUTIONS{postfix}') + if len(set(resolutions)) == 1: + config_h_lines.append(f'#ifndef ENCODER_RESOLUTION{postfix}') + config_h_lines.append(f'# define ENCODER_RESOLUTION{postfix} { resolutions[0] }') + config_h_lines.append(f'#endif // ENCODER_RESOLUTION{postfix}') + else: + config_h_lines.append(f'#ifndef ENCODER_RESOLUTIONS{postfix}') + config_h_lines.append(f'# define ENCODER_RESOLUTIONS{postfix} {{ { ", ".join(resolutions) } }}') + config_h_lines.append(f'#endif // ENCODER_RESOLUTIONS{postfix}') def generate_split_config(kb_info_json, config_h_lines): -- cgit v1.2.3 From 35d78aa8a4587ce5286a362471380a9d4f000f3c Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Mon, 11 Jul 2022 10:51:39 +0100 Subject: More DD encoder fixes (#17615) --- lib/python/qmk/cli/generate/config_h.py | 8 +++++--- lib/python/qmk/info.py | 10 +++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index b53f4ff335..a26dcdf7d7 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -142,7 +142,7 @@ def generate_encoder_config(encoder_json, config_h_lines, postfix=''): for encoder in encoder_json.get("rotary", []): a_pads.append(encoder["pin_a"]) b_pads.append(encoder["pin_b"]) - resolutions.append(str(encoder.get("resolution", 4))) + resolutions.append(encoder.get("resolution", None)) config_h_lines.append(f'#ifndef ENCODERS_PAD_A{postfix}') config_h_lines.append(f'# define ENCODERS_PAD_A{postfix} {{ { ", ".join(a_pads) } }}') @@ -152,13 +152,15 @@ def generate_encoder_config(encoder_json, config_h_lines, postfix=''): config_h_lines.append(f'# define ENCODERS_PAD_B{postfix} {{ { ", ".join(b_pads) } }}') config_h_lines.append(f'#endif // ENCODERS_PAD_B{postfix}') - if len(set(resolutions)) == 1: + if None in resolutions: + cli.log.debug("Unable to generate ENCODER_RESOLUTION configuration") + elif len(set(resolutions)) == 1: config_h_lines.append(f'#ifndef ENCODER_RESOLUTION{postfix}') config_h_lines.append(f'# define ENCODER_RESOLUTION{postfix} { resolutions[0] }') config_h_lines.append(f'#endif // ENCODER_RESOLUTION{postfix}') else: config_h_lines.append(f'#ifndef ENCODER_RESOLUTIONS{postfix}') - config_h_lines.append(f'# define ENCODER_RESOLUTIONS{postfix} {{ { ", ".join(resolutions) } }}') + config_h_lines.append(f'# define ENCODER_RESOLUTIONS{postfix} {{ { ", ".join(map(str,resolutions)) } }}') config_h_lines.append(f'#endif // ENCODER_RESOLUTIONS{postfix}') diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index dac2fd6e9e..72424f390e 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -225,17 +225,21 @@ def _extract_encoders_values(config_c, postfix=''): b_pad = config_c.get(f'ENCODERS_PAD_B{postfix}', '').replace(' ', '')[1:-1] resolutions = config_c.get(f'ENCODER_RESOLUTIONS{postfix}', '').replace(' ', '')[1:-1] - default_resolution = config_c.get('ENCODER_RESOLUTION', '4') + default_resolution = config_c.get('ENCODER_RESOLUTION', None) if a_pad and b_pad: a_pad = list(filter(None, a_pad.split(','))) b_pad = list(filter(None, b_pad.split(','))) resolutions = list(filter(None, resolutions.split(','))) - resolutions += [default_resolution] * (len(a_pad) - len(resolutions)) + if default_resolution: + resolutions += [default_resolution] * (len(a_pad) - len(resolutions)) encoders = [] for index in range(len(a_pad)): - encoders.append({'pin_a': a_pad[index], 'pin_b': b_pad[index], "resolution": int(resolutions[index])}) + encoder = {'pin_a': a_pad[index], 'pin_b': b_pad[index]} + if index < len(resolutions): + encoder['resolution'] = int(resolutions[index]) + encoders.append(encoder) return encoders -- cgit v1.2.3 From 897403c4a71080469b8c11fc87e0e145ffe3837a Mon Sep 17 00:00:00 2001 From: Joel Challis Date: Sat, 6 Aug 2022 07:14:29 +0100 Subject: Publish data as part of API generation (#17020) --- .github/workflows/api.yml | 3 +++ .github/workflows/develop_api.yml | 3 +++ lib/python/qmk/cli/generate/api.py | 37 +++++++++++++++++++++++-------------- 3 files changed, 29 insertions(+), 14 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 9d850080a6..dd3fbdaa92 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -7,6 +7,9 @@ on: paths: - 'keyboards/**' - 'layouts/community/**' + - 'lib/python/**' + - 'data/**' + - '.github/workflows/api.yml' workflow_dispatch: jobs: diff --git a/.github/workflows/develop_api.yml b/.github/workflows/develop_api.yml index ebc1b545b7..194305e730 100644 --- a/.github/workflows/develop_api.yml +++ b/.github/workflows/develop_api.yml @@ -7,6 +7,9 @@ on: paths: - 'keyboards/**' - 'layouts/community/**' + - 'lib/python/**' + - 'data/**' + - '.github/workflows/develop_api.yml' workflow_dispatch: jobs: diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 0596b3f22b..8d8ca3cd41 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -12,21 +12,30 @@ from qmk.json_encoders import InfoJSONEncoder from qmk.json_schema import json_load from qmk.keyboard import find_readme, list_keyboards -TEMPLATE_PATH = Path('data/templates/api/') +DATA_PATH = Path('data') +TEMPLATE_PATH = DATA_PATH / 'templates/api/' BUILD_API_PATH = Path('.build/api_data/') +def _filtered_keyboard_list(): + """Perform basic filtering of list_keyboards + """ + keyboard_list = list_keyboards() + if cli.args.filter: + kb_list = [] + for keyboard_name in keyboard_list: + if any(i in keyboard_name for i in cli.args.filter): + kb_list.append(keyboard_name) + keyboard_list = kb_list + return keyboard_list + + @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't write the data to disk.") @cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on partial name matches the supplied value. May be passed multiple times.") -@cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True) +@cli.subcommand('Generate QMK API data', hidden=False if cli.config.user.developer else True) def generate_api(cli): """Generates the QMK API data. """ - if BUILD_API_PATH.exists(): - shutil.rmtree(BUILD_API_PATH) - - shutil.copytree(TEMPLATE_PATH, BUILD_API_PATH) - v1_dir = BUILD_API_PATH / 'v1' keyboard_all_file = v1_dir / 'keyboards.json' # A massive JSON containing everything keyboard_list_file = v1_dir / 'keyboard_list.json' # A simple list of keyboard targets @@ -34,14 +43,14 @@ def generate_api(cli): keyboard_metadata_file = v1_dir / 'keyboard_metadata.json' # All the data configurator/via needs for initialization usb_file = v1_dir / 'usb.json' # A mapping of USB VID/PID -> keyboard target + if BUILD_API_PATH.exists(): + shutil.rmtree(BUILD_API_PATH) + + shutil.copytree(TEMPLATE_PATH, BUILD_API_PATH) + shutil.copytree(DATA_PATH, v1_dir) + # Filter down when required - keyboard_list = list_keyboards() - if cli.args.filter: - kb_list = [] - for keyboard_name in keyboard_list: - if any(i in keyboard_name for i in cli.args.filter): - kb_list.append(keyboard_name) - keyboard_list = kb_list + keyboard_list = _filtered_keyboard_list() kb_all = {} usb_list = {} -- cgit v1.2.3 From 24720400a854e5954c75baef67161469bc6a3e8e Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 26 Aug 2022 12:19:34 +1000 Subject: Update LUFA submodule (#18168) --- lib/lufa | 2 +- lib/python/qmk/cli/generate/dfu_header.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/python/qmk/cli/generate') diff --git a/lib/lufa b/lib/lufa index 35cc3d92f5..549b97320d 160000 --- a/lib/lufa +++ b/lib/lufa @@ -1 +1 @@ -Subproject commit 35cc3d92f557bc8874ca602d2f22642d77cfe129 +Subproject commit 549b97320d515bfca2f95c145a67bd13be968faa diff --git a/lib/python/qmk/cli/generate/dfu_header.py b/lib/python/qmk/cli/generate/dfu_header.py index e873117387..aa0252ca86 100644 --- a/lib/python/qmk/cli/generate/dfu_header.py +++ b/lib/python/qmk/cli/generate/dfu_header.py @@ -33,8 +33,8 @@ def generate_dfu_header(cli): kb_info_json = dotty(info_json(cli.config.generate_dfu_header.keyboard)) keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once'] - keyboard_h_lines.append(f'#define MANUFACTURER {kb_info_json["manufacturer"]}') - keyboard_h_lines.append(f'#define PRODUCT {kb_info_json["keyboard_name"]} Bootloader') + keyboard_h_lines.append(f'#define MANUFACTURER "{kb_info_json["manufacturer"]}"') + keyboard_h_lines.append(f'#define PRODUCT "{kb_info_json["keyboard_name"]} Bootloader"') # Optional if 'qmk_lufa_bootloader.esc_output' in kb_info_json: -- cgit v1.2.3