From 0bf774b079ba1f47f788e432ef8c5f3aa5242a87 Mon Sep 17 00:00:00 2001 From: Zach White Date: Tue, 1 Dec 2020 16:04:22 -0800 Subject: [PATCH] get qmk generate-api into a good state --- data/schemas/keyboard.jsonschema | 33 ++++++++++ lib/python/qmk/c_parse.py | 26 ++++++-- lib/python/qmk/cli/generate/api.py | 2 +- lib/python/qmk/cli/generate/rules_mk.py | 9 ++- lib/python/qmk/info.py | 82 +++++++++++++++++-------- 5 files changed, 117 insertions(+), 35 deletions(-) diff --git a/data/schemas/keyboard.jsonschema b/data/schemas/keyboard.jsonschema index 75e792b646fe..9355ee49bd3c 100644 --- a/data/schemas/keyboard.jsonschema +++ b/data/schemas/keyboard.jsonschema @@ -90,6 +90,9 @@ "type": "object", "additionalProperties": false, "properties": { + "filename": { + "type": "string" + }, "c_macro": { "type": "boolean" }, @@ -119,6 +122,18 @@ "type": "number", "min": 0.25 }, + "r": { + "type": "number", + "min": 0 + }, + "rx": { + "type": "number", + "min": 0 + }, + "ry": { + "type": "number", + "min": 0 + }, "w": { "type": "number", "min": 0.25 @@ -199,6 +214,12 @@ "min": 0, "multipleOf": 1 }, + "max_brightness": { + "type": "number", + "min": 0, + "max": 255, + "multipleOf": 1 + }, "pin": { "type": "string", "pattern": "^[A-K]\\d{1,2}$" @@ -207,6 +228,18 @@ "type": "number", "min": 0, "multipleOf": 1 + }, + "sleep": {"type": "boolean"}, + "split": {"type": "boolean"}, + "split_count": { + "type": "array", + "minLength": 2, + "maxLength": 2, + "items": { + "type": "number", + "min": 0, + "multipleOf": 1 + } } } }, diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index e41e271a4355..67e196f0eaa5 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -1,12 +1,27 @@ """Functions for working with config.h files. """ from pathlib import Path +import re from milc import cli from qmk.comment_remover import comment_remover default_key_entry = {'x': -1, 'y': 0, 'w': 1} +single_comment_regex = re.compile(r' */[/*].*$') +multi_comment_regex = re.compile(r'/\*(.|\n)*\*/', re.MULTILINE) + + +def strip_line_comment(string): + """Removes comments from a single line string. + """ + return single_comment_regex.sub('', string) + + +def strip_multiline_comment(string): + """Removes comments from a single line string. + """ + return multi_comment_regex.sub('', string) def c_source_files(dir_names): @@ -53,7 +68,8 @@ def find_layouts(file): parsed_layout = [_default_key(key) for key in layout.split(',')] for key in parsed_layout: - key['matrix'] = matrix_locations.get(key['label']) + if key['label'] in matrix_locations: + key['matrix'] = matrix_locations[key['label']] parsed_layouts[macro_name] = { 'key_count': len(parsed_layout), @@ -88,12 +104,10 @@ def parse_config_h_file(config_h_file, config_h=None): if config_h_file.exists(): config_h_text = config_h_file.read_text() config_h_text = config_h_text.replace('\\\n', '') + config_h_text = strip_multiline_comment(config_h_text) for linenum, line in enumerate(config_h_text.split('\n')): - line = line.strip() - - if '//' in line: - line = line[:line.index('//')].strip() + line = strip_line_comment(line).strip() if not line: continue @@ -156,6 +170,6 @@ def _parse_matrix_locations(matrix, file, macro_name): row = row.replace('{', '').replace('}', '') for col_num, identifier in enumerate(row.split(',')): if identifier != 'KC_NO': - matrix_locations[identifier] = (row_num, col_num) + matrix_locations[identifier] = [row_num, col_num] return matrix_locations diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 939ec17cd025..02f7cace9299 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -48,7 +48,7 @@ def generate_api(cli): if 'vid' in usb and usb['vid'] not in usb_list['devices']: usb_list['devices'][usb['vid']] = {} - if 'pid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]: + if 'vid' in usb and usb['pid'] not in usb_list['devices'][usb['vid']]: usb_list['devices'][usb['vid']][usb['pid']] = {} if 'vid' in usb and 'pid' in usb: diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py index 72ed3c45fa47..570ef5a0d6f7 100755 --- a/lib/python/qmk/cli/generate/rules_mk.py +++ b/lib/python/qmk/cli/generate/rules_mk.py @@ -41,9 +41,12 @@ def generate_rules_mk(cli): # 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}') + if feature == 'bootmagic_lite' and enabled: + rules_mk_lines.append(f'BOOTMAGIC_ENABLE := lite') + else: + feature = feature.upper() + 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']: diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index a60830440d08..c5ac7b130efe 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -26,13 +26,12 @@ } 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' + 'led_count': ('RGBLED_NUM', int), + 'pin': ('RGB_DI_PIN', str), + 'max_brightness': ('RGBLIGHT_LIMIT_VAL', int), + 'hue_steps': ('RGBLIGHT_HUE_STEP', int), + 'saturation_steps': ('RGBLIGHT_SAT_STEP', int), + 'brightness_steps': ('RGBLIGHT_VAL_STEP', int) } rgblight_toggles = { @@ -54,6 +53,8 @@ 'twinkle': 'RGBLIGHT_EFFECT_TWINKLE' } +usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'} + true_values = ['1', 'on', 'yes'] false_values = ['0', 'off', 'no'] @@ -97,7 +98,8 @@ def info_json(keyboard): keyboard_api_validate(info_data) except jsonschema.ValidationError as e: - cli.log.error('Invalid info.json data: %s', e.message) + json_path = '.'.join([str(p) for p in e.absolute_path]) + cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message) print(dir(e)) exit() @@ -198,6 +200,9 @@ def _extract_indicators(info_data, config_c): 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 'indicators' not in info_data: + info_data['indicators'] = {} + if config_key in config_c: info_data['indicators'][json_key] = config_c.get(config_key) @@ -223,10 +228,23 @@ def _extract_community_layouts(info_data, rules): def _extract_features(info_data, rules): """Find all the features enabled in rules.mk. """ + # Special handling for bootmagic which also supports a "lite" mode. + if rules.get('BOOTMAGIC_ENABLE') == 'lite': + rules['BOOTMAGIC_LITE_ENABLE'] = 'on' + del(rules['BOOTMAGIC_ENABLE']) + if rules.get('BOOTMAGIC_ENABLE') == 'full': + rules['BOOTMAGIC_ENABLE'] = 'on' + + # Skip non-boolean features we haven't implemented special handling for + for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE': + if rules.get(feature): + del(rules[feature]) + + # Process the rest of the rules as booleans 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 + value = True if value.lower() in true_values else False if value.lower() in false_values else value if 'config_h_features' not in info_data: info_data['config_h_features'] = {} @@ -277,12 +295,21 @@ def _extract_rgblight(info_data, config_c): rgblight = info_data.get('rgblight', {}) animations = rgblight.get('animations', {}) - for json_key, config_key in rgblight_properties.items(): + if 'RGBLED_SPLIT' in config_c: + raw_split = config_c.get('RGBLED_SPLIT', '').replace('{', '').replace('}', '').strip() + rgblight['split_count'] = [int(i) for i in raw_split.split(',')] + + for json_key, config_key_type in rgblight_properties.items(): + config_key, config_type = config_key_type + 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] + try: + rgblight[json_key] = config_type(config_c[config_key]) + except ValueError as e: + cli.log.error('%s: config.h: Could not convert "%s" to %s: %s', info_data['keyboard_folder'], config_c[config_key], config_type.__name__, e) for json_key, config_key in rgblight_toggles.items(): if config_key in config_c: @@ -329,11 +356,16 @@ def _extract_matrix_info(info_data, config_c): info_data['matrix_pins'] = {} + # FIXME(skullydazed/anyone): Should really check every pin, not just the first if row_pins: - info_data['matrix_pins']['rows'] = row_pins.split(',') + row_pins = [pin.strip() for pin in row_pins.split(',') if pin] + if row_pins[0][0] in 'ABCDEFGHIJK' and row_pins[0][1].isdigit(): + info_data['matrix_pins']['rows'] = row_pins if col_pins: - info_data['matrix_pins']['cols'] = col_pins.split(',') + col_pins = [pin.strip() for pin in col_pins.split(',') if pin] + if col_pins[0][0] in 'ABCDEFGHIJK' and col_pins[0][1].isdigit(): + info_data['matrix_pins']['cols'] = col_pins if direct_pins: if 'matrix_pins' in info_data: @@ -342,6 +374,9 @@ def _extract_matrix_info(info_data, config_c): info_data['matrix_pins'] = {} direct_pin_array = [] + while direct_pins[-1] != '}': + direct_pins = direct_pins[:-1] + for row in direct_pins.split('},{'): if row.startswith('{'): row = row[1:] @@ -365,8 +400,6 @@ def _extract_matrix_info(info_data, config_c): 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'] = {} @@ -375,10 +408,7 @@ def _extract_usb_info(info_data, 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)) + info_data['usb'][info_name] = '0x' + config_c[config_name][2:].upper() return info_data @@ -516,8 +546,9 @@ def arm_processor_rules(info_data, rules): 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.') + # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first + # 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'] @@ -555,8 +586,9 @@ def avr_processor_rules(info_data, rules): 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.') + # FIXME(skullydazed/anyone): need to remove the massive amounts of duplication first + # 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: @@ -590,8 +622,8 @@ def merge_info_jsons(keyboard, info_data): 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) + json_path = '.'.join([str(p) for p in e.absolute_path]) + cli.log.error('Invalid info.json data: %s: %s: %s', info_file, json_path, e.message) continue if not isinstance(new_info_data, dict):