aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
authorZach White <skullydazed@gmail.com>2020-12-01 12:52:02 -0800
committerZach White <skullydazed@drpepper.org>2021-01-07 21:21:12 -0800
commitededff8556daff544633cb143cb6d939afd09014 (patch)
treefa7d69a513ec8f8d5dd23058c1ea650cb00944df /lib/python
parent95cbcef34fee3727a224fc13c727ea744fd869e7 (diff)
downloadqmk_firmware-ededff8556daff544633cb143cb6d939afd09014.tar.gz
qmk_firmware-ededff8556daff544633cb143cb6d939afd09014.zip
validate keyboard data with jsonschema
Diffstat (limited to 'lib/python')
-rwxr-xr-xlib/python/qmk/cli/generate/info_json.py2
-rwxr-xr-xlib/python/qmk/cli/generate/rules_mk.py13
-rw-r--r--lib/python/qmk/info.py151
3 files changed, 154 insertions, 12 deletions
diff --git a/lib/python/qmk/cli/generate/info_json.py b/lib/python/qmk/cli/generate/info_json.py
index 7e6654e45..fba4b1c01 100755
--- a/lib/python/qmk/cli/generate/info_json.py
+++ b/lib/python/qmk/cli/generate/info_json.py
@@ -39,7 +39,7 @@ def generate_info_json(cli):
39 pared_down_json[key] = kb_info_json[key] 39 pared_down_json[key] = kb_info_json[key]
40 40
41 pared_down_json['layouts'] = {} 41 pared_down_json['layouts'] = {}
42 if 'layouts' in pared_down_json: 42 if 'layouts' in kb_info_json:
43 for layout_name, layout in kb_info_json['layouts'].items(): 43 for layout_name, layout in kb_info_json['layouts'].items():
44 pared_down_json['layouts'][layout_name] = {} 44 pared_down_json['layouts'][layout_name] = {}
45 pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout'])) 45 pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))
diff --git a/lib/python/qmk/cli/generate/rules_mk.py b/lib/python/qmk/cli/generate/rules_mk.py
index 4268ae047..72ed3c45f 100755
--- a/lib/python/qmk/cli/generate/rules_mk.py
+++ b/lib/python/qmk/cli/generate/rules_mk.py
@@ -6,6 +6,10 @@ from qmk.decorators import automagic_keyboard, automagic_keymap
6from qmk.info import info_json 6from qmk.info import info_json
7from qmk.path import is_keyboard, normpath 7from qmk.path import is_keyboard, normpath
8 8
9info_to_rules = {
10 'bootloader': 'BOOTLOADER',
11 'processor': 'MCU'
12}
9 13
10@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') 14@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
11@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") 15@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):
30 kb_info_json = info_json(cli.config.generate_rules_mk.keyboard) 34 kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
31 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', ''] 35 rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
32 36
37 # Bring in settings
38 for info_key, rule_key in info_to_rules.items():
39 rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}')
40
33 # Find features that should be enabled 41 # Find features that should be enabled
34 if 'features' in kb_info_json: 42 if 'features' in kb_info_json:
35 for feature, enabled in kb_info_json['features'].items(): 43 for feature, enabled in kb_info_json['features'].items():
@@ -37,6 +45,11 @@ def generate_rules_mk(cli):
37 enabled = 'yes' if enabled else 'no' 45 enabled = 'yes' if enabled else 'no'
38 rules_mk_lines.append(f'{feature}_ENABLE := {enabled}') 46 rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')
39 47
48 # Set the LED driver
49 if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
50 driver = kb_info_json['led_matrix']['driver']
51 rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}')
52
40 # Add community layouts 53 # Add community layouts
41 if 'community_layouts' in kb_info_json: 54 if 'community_layouts' in kb_info_json:
42 rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}') 55 rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index 4611874e8..1cf12190d 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -4,6 +4,7 @@ import json
4from glob import glob 4from glob import glob
5from pathlib import Path 5from pathlib import Path
6 6
7import jsonschema
7from milc import cli 8from milc import cli
8 9
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS 10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
@@ -13,6 +14,17 @@ from qmk.keymap import list_keymaps
13from qmk.makefile import parse_rules_mk_file 14from qmk.makefile import parse_rules_mk_file
14from qmk.math import compute 15from qmk.math import compute
15 16
17led_matrix_properties = {
18 'driver_count': 'LED_DRIVER_COUNT',
19 'driver_addr1': 'LED_DRIVER_ADDR_1',
20 'driver_addr2': 'LED_DRIVER_ADDR_2',
21 'driver_addr3': 'LED_DRIVER_ADDR_3',
22 'driver_addr4': 'LED_DRIVER_ADDR_4',
23 'led_count': 'LED_DRIVER_LED_COUNT',
24 'timeout': 'ISSI_TIMEOUT',
25 'persistence': 'ISSI_PERSISTENCE'
26}
27
16rgblight_properties = { 28rgblight_properties = {
17 'led_count': 'RGBLED_NUM', 29 'led_count': 'RGBLED_NUM',
18 'pin': 'RGB_DI_PIN', 30 'pin': 'RGB_DI_PIN',
@@ -80,6 +92,15 @@ def info_json(keyboard):
80 info_data = _extract_config_h(info_data) 92 info_data = _extract_config_h(info_data)
81 info_data = _extract_rules_mk(info_data) 93 info_data = _extract_rules_mk(info_data)
82 94
95 # Validate against the jsonschema
96 try:
97 keyboard_api_validate(info_data)
98
99 except jsonschema.ValidationError as e:
100 cli.log.error('Invalid info.json data: %s', e.message)
101 print(dir(e))
102 exit()
103
83 # Make sure we have at least one layout 104 # Make sure we have at least one layout
84 if not info_data.get('layouts'): 105 if not info_data.get('layouts'):
85 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.') 106 _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):
102 return info_data 123 return info_data
103 124
104 125
126def _json_load(json_file):
127 """Load a json file from disk.
128
129 Note: file must be a Path object.
130 """
131 try:
132 return json.load(json_file.open())
133
134 except json.decoder.JSONDecodeError as e:
135 cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
136 exit(1)
137
138
139def _jsonschema(schema_name):
140 """Read a jsonschema file from disk.
141 """
142 schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
143
144 if not schema_path.exists():
145 schema_path = Path('data/schemas/false.jsonschema')
146
147 return _json_load(schema_path)
148
149
150def keyboard_validate(data):
151 """Validates data against the keyboard jsonschema.
152 """
153 schema = _jsonschema('keyboard')
154 validator = jsonschema.Draft7Validator(schema).validate
155
156 return validator(data)
157
158
159def keyboard_api_validate(data):
160 """Validates data against the api_keyboard jsonschema.
161 """
162 base = _jsonschema('keyboard')
163 relative = _jsonschema('api_keyboard')
164 resolver = jsonschema.RefResolver.from_schema(base)
165 validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
166
167 return validator(data)
168
169
105def _extract_debounce(info_data, config_c): 170def _extract_debounce(info_data, config_c):
106 """Handle debounce. 171 """Handle debounce.
107 """ 172 """
@@ -109,7 +174,7 @@ def _extract_debounce(info_data, config_c):
109 _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.') 174 _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
110 175
111 if 'DEBOUNCE' in config_c: 176 if 'DEBOUNCE' in config_c:
112 info_data['debounce'] = config_c.get('DEBOUNCE') 177 info_data['debounce'] = int(config_c['DEBOUNCE'])
113 178
114 return info_data 179 return info_data
115 180
@@ -181,8 +246,36 @@ def _extract_features(info_data, rules):
181 return info_data 246 return info_data
182 247
183 248
249def _extract_led_drivers(info_data, rules):
250 """Find all the LED drivers set in rules.mk.
251 """
252 if 'LED_MATRIX_DRIVER' in rules:
253 if 'led_matrix' not in info_data:
254 info_data['led_matrix'] = {}
255
256 if info_data['led_matrix'].get('driver'):
257 _log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')
258
259 info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']
260
261 return info_data
262
263
264def _extract_led_matrix(info_data, config_c):
265 """Handle the led_matrix configuration.
266 """
267 led_matrix = info_data.get('led_matrix', {})
268
269 for json_key, config_key in led_matrix_properties.items():
270 if config_key in config_c:
271 if json_key in led_matrix:
272 _log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
273
274 led_matrix[json_key] = config_c[config_key]
275
276
184def _extract_rgblight(info_data, config_c): 277def _extract_rgblight(info_data, config_c):
185 """Handle the rgblight configuration 278 """Handle the rgblight configuration.
186 """ 279 """
187 rgblight = info_data.get('rgblight', {}) 280 rgblight = info_data.get('rgblight', {})
188 animations = rgblight.get('animations', {}) 281 animations = rgblight.get('animations', {})
@@ -303,6 +396,7 @@ def _extract_config_h(info_data):
303 _extract_indicators(info_data, config_c) 396 _extract_indicators(info_data, config_c)
304 _extract_matrix_info(info_data, config_c) 397 _extract_matrix_info(info_data, config_c)
305 _extract_usb_info(info_data, config_c) 398 _extract_usb_info(info_data, config_c)
399 _extract_led_matrix(info_data, config_c)
306 _extract_rgblight(info_data, config_c) 400 _extract_rgblight(info_data, config_c)
307 401
308 return info_data 402 return info_data
@@ -326,6 +420,7 @@ def _extract_rules_mk(info_data):
326 420
327 _extract_community_layouts(info_data, rules) 421 _extract_community_layouts(info_data, rules)
328 _extract_features(info_data, rules) 422 _extract_features(info_data, rules)
423 _extract_led_drivers(info_data, rules)
329 424
330 return info_data 425 return info_data
331 426
@@ -412,13 +507,28 @@ def arm_processor_rules(info_data, rules):
412 """Setup the default info for an ARM board. 507 """Setup the default info for an ARM board.
413 """ 508 """
414 info_data['processor_type'] = 'arm' 509 info_data['processor_type'] = 'arm'
415 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
416 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
417 info_data['protocol'] = 'ChibiOS' 510 info_data['protocol'] = 'ChibiOS'
418 511
419 if info_data['bootloader'] == 'unknown': 512 if 'MCU' in rules:
513 if 'processor' in info_data:
514 _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
515
516 info_data['processor'] = rules['MCU']
517
518 elif 'processor' not in info_data:
519 info_data['processor'] = 'unknown'
520
521 if 'BOOTLOADER' in rules:
522 if 'bootloader' in info_data:
523 _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
524
525 info_data['bootloader'] = rules['BOOTLOADER']
526
527 else:
420 if 'STM32' in info_data['processor']: 528 if 'STM32' in info_data['processor']:
421 info_data['bootloader'] = 'stm32-dfu' 529 info_data['bootloader'] = 'stm32-dfu'
530 else:
531 info_data['bootloader'] = 'unknown'
422 532
423 if 'STM32' in info_data['processor']: 533 if 'STM32' in info_data['processor']:
424 info_data['platform'] = 'STM32' 534 info_data['platform'] = 'STM32'
@@ -436,9 +546,25 @@ def avr_processor_rules(info_data, rules):
436 info_data['processor_type'] = 'avr' 546 info_data['processor_type'] = 'avr'
437 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu' 547 info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
438 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown' 548 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
439 info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
440 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA' 549 info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
441 550
551 if 'MCU' in rules:
552 if 'processor' in info_data:
553 _log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
554
555 info_data['processor'] = rules['MCU']
556
557 elif 'processor' not in info_data:
558 info_data['processor'] = 'unknown'
559
560 if 'BOOTLOADER' in rules:
561 if 'bootloader' in info_data:
562 _log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
563
564 info_data['bootloader'] = rules['BOOTLOADER']
565 else:
566 info_data['bootloader'] = 'atmel-dfu'
567
442 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk: 568 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
443 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA' 569 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
444 570
@@ -463,10 +589,13 @@ def merge_info_jsons(keyboard, info_data):
463 for info_file in find_info_json(keyboard): 589 for info_file in find_info_json(keyboard):
464 # Load and validate the JSON data 590 # Load and validate the JSON data
465 try: 591 try:
466 new_info_data = json.load(info_file.open('r')) 592 new_info_data = _json_load(info_file)
467 except Exception as e: 593 keyboard_validate(new_info_data)
468 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e)) 594
469 new_info_data = {} 595 except jsonschema.ValidationError as e:
596 cli.log.error('Invalid info.json data: %s', e.message)
597 cli.log.error('Not including file %s', info_file)
598 continue
470 599
471 if not isinstance(new_info_data, dict): 600 if not isinstance(new_info_data, dict):
472 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 601 _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):
479 608
480 # Deep merge certain keys 609 # Deep merge certain keys
481 # 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. 610 # 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.
482 for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'): 611 for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'):
483 if key in new_info_data: 612 if key in new_info_data:
484 if key not in info_data: 613 if key not in info_data:
485 info_data[key] = {} 614 info_data[key] = {}