aboutsummaryrefslogtreecommitdiff
path: root/lib/python/qmk/info.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python/qmk/info.py')
-rw-r--r--lib/python/qmk/info.py358
1 files changed, 299 insertions, 59 deletions
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index f476dc666..4611874e8 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -6,13 +6,45 @@ from pathlib import Path
6 6
7from milc import cli 7from milc import cli
8 8
9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 9from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
10from qmk.c_parse import find_layouts 10from qmk.c_parse import find_layouts
11from qmk.keyboard import config_h, rules_mk 11from qmk.keyboard import config_h, rules_mk
12from qmk.keymap import list_keymaps 12from qmk.keymap import list_keymaps
13from qmk.makefile import parse_rules_mk_file 13from qmk.makefile import parse_rules_mk_file
14from qmk.math import compute 14from qmk.math import compute
15 15
16rgblight_properties = {
17 'led_count': 'RGBLED_NUM',
18 'pin': 'RGB_DI_PIN',
19 'split_count': 'RGBLED_SPLIT',
20 'max_brightness': 'RGBLIGHT_LIMIT_VAL',
21 'hue_steps': 'RGBLIGHT_HUE_STEP',
22 'saturation_steps': 'RGBLIGHT_SAT_STEP',
23 'brightness_steps': 'RGBLIGHT_VAL_STEP'
24}
25
26rgblight_toggles = {
27 'sleep': 'RGBLIGHT_SLEEP',
28 'split': 'RGBLIGHT_SPLIT',
29}
30
31rgblight_animations = {
32 'all': 'RGBLIGHT_ANIMATIONS',
33 'alternating': 'RGBLIGHT_EFFECT_ALTERNATING',
34 'breathing': 'RGBLIGHT_EFFECT_BREATHING',
35 'christmas': 'RGBLIGHT_EFFECT_CHRISTMAS',
36 'knight': 'RGBLIGHT_EFFECT_KNIGHT',
37 'rainbow_mood': 'RGBLIGHT_EFFECT_RAINBOW_MOOD',
38 'rainbow_swirl': 'RGBLIGHT_EFFECT_RAINBOW_SWIRL',
39 'rgb_test': 'RGBLIGHT_EFFECT_RGB_TEST',
40 'snake': 'RGBLIGHT_EFFECT_SNAKE',
41 'static_gradient': 'RGBLIGHT_EFFECT_STATIC_GRADIENT',
42 'twinkle': 'RGBLIGHT_EFFECT_TWINKLE'
43}
44
45true_values = ['1', 'on', 'yes']
46false_values = ['0', 'off', 'no']
47
16 48
17def info_json(keyboard): 49def info_json(keyboard):
18 """Generate the info.json data for a specific keyboard. 50 """Generate the info.json data for a specific keyboard.
@@ -38,8 +70,9 @@ def info_json(keyboard):
38 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'} 70 info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
39 71
40 # Populate layout data 72 # Populate layout data
41 for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items(): 73 for layout_name, layout_json in _find_all_layouts(info_data, keyboard).items():
42 if not layout_name.startswith('LAYOUT_kc'): 74 if not layout_name.startswith('LAYOUT_kc'):
75 layout_json['c_macro'] = True
43 info_data['layouts'][layout_name] = layout_json 76 info_data['layouts'][layout_name] = layout_json
44 77
45 # Merge in the data from info.json, config.h, and rules.mk 78 # Merge in the data from info.json, config.h, and rules.mk
@@ -47,34 +80,182 @@ def info_json(keyboard):
47 info_data = _extract_config_h(info_data) 80 info_data = _extract_config_h(info_data)
48 info_data = _extract_rules_mk(info_data) 81 info_data = _extract_rules_mk(info_data)
49 82
83 # Make sure we have at least one layout
84 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.')
86
87 # Make sure we supply layout macros for the community layouts we claim to support
88 # FIXME(skullydazed): This should be populated into info.json and read from there instead
89 if 'LAYOUTS' in rules and info_data.get('layouts'):
90 # Match these up against the supplied layouts
91 supported_layouts = rules['LAYOUTS'].strip().split()
92 for layout_name in sorted(info_data['layouts']):
93 layout_name = layout_name[7:]
94
95 if layout_name in supported_layouts:
96 supported_layouts.remove(layout_name)
97
98 if supported_layouts:
99 for supported_layout in supported_layouts:
100 _log_error(info_data, 'Claims to support community layout %s but no LAYOUT_%s() macro found' % (supported_layout, supported_layout))
101
50 return info_data 102 return info_data
51 103
52 104
53def _extract_config_h(info_data): 105def _extract_debounce(info_data, config_c):
54 """Pull some keyboard information from existing rules.mk files 106 """Handle debounce.
107 """
108 if 'debounce' in info_data and 'DEBOUNCE' in config_c:
109 _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
110
111 if 'DEBOUNCE' in config_c:
112 info_data['debounce'] = config_c.get('DEBOUNCE')
113
114 return info_data
115
116
117def _extract_diode_direction(info_data, config_c):
118 """Handle the diode direction.
119 """
120 if 'diode_direction' in info_data and 'DIODE_DIRECTION' in config_c:
121 _log_warning(info_data, 'Diode direction is specified in both info.json and config.h, the config.h value wins.')
122
123 if 'DIODE_DIRECTION' in config_c:
124 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
125
126 return info_data
127
128
129def _extract_indicators(info_data, config_c):
130 """Find the LED indicator information.
131 """
132 for json_key, config_key in LED_INDICATORS.items():
133 if json_key in info_data.get('indicators', []) and config_key in config_c:
134 _log_warning(info_data, f'Indicator {json_key} is specified in both info.json and config.h, the config.h value wins.')
135
136 if config_key in config_c:
137 if 'indicators' not in info_data:
138 info_data['indicators'] = {}
139
140 info_data['indicators'][json_key] = config_c.get(config_key)
141
142 return info_data
143
144
145def _extract_community_layouts(info_data, rules):
146 """Find the community layouts in rules.mk.
147 """
148 community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else []
149
150 if 'community_layouts' in info_data:
151 for layout in community_layouts:
152 if layout not in info_data['community_layouts']:
153 community_layouts.append(layout)
154
155 else:
156 info_data['community_layouts'] = community_layouts
157
158 return info_data
159
160
161def _extract_features(info_data, rules):
162 """Find all the features enabled in rules.mk.
163 """
164 for key, value in rules.items():
165 if key.endswith('_ENABLE'):
166 key = '_'.join(key.split('_')[:-1]).lower()
167 value = True if value in true_values else False if value in false_values else value
168
169 if 'config_h_features' not in info_data:
170 info_data['config_h_features'] = {}
171
172 if 'features' not in info_data:
173 info_data['features'] = {}
174
175 if key in info_data['features']:
176 _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
177
178 info_data['features'][key] = value
179 info_data['config_h_features'][key] = value
180
181 return info_data
182
183
184def _extract_rgblight(info_data, config_c):
185 """Handle the rgblight configuration
186 """
187 rgblight = info_data.get('rgblight', {})
188 animations = rgblight.get('animations', {})
189
190 for json_key, config_key in rgblight_properties.items():
191 if config_key in config_c:
192 if json_key in rgblight:
193 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
194
195 rgblight[json_key] = config_c[config_key]
196
197 for json_key, config_key in rgblight_toggles.items():
198 if config_key in config_c:
199 if json_key in rgblight:
200 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
201
202 rgblight[json_key] = config_c[config_key]
203
204 for json_key, config_key in rgblight_animations.items():
205 if config_key in config_c:
206 if json_key in animations:
207 _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
208
209 animations[json_key] = config_c[config_key]
210
211 if animations:
212 rgblight['animations'] = animations
213
214 if rgblight:
215 info_data['rgblight'] = rgblight
216
217 return info_data
218
219
220def _extract_matrix_info(info_data, config_c):
221 """Populate the matrix information.
55 """ 222 """
56 config_c = config_h(info_data['keyboard_folder'])
57 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 223 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
58 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 224 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
59 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 225 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
60 226
61 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') 227 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
62 info_data['matrix_size'] = { 228 if 'matrix_size' in info_data:
63 'rows': compute(config_c.get('MATRIX_ROWS', '0')), 229 _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
64 'cols': compute(config_c.get('MATRIX_COLS', '0')), 230
65 } 231 info_data['matrix_size'] = {
66 info_data['matrix_pins'] = {} 232 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
233 'cols': compute(config_c.get('MATRIX_COLS', '0')),
234 }
235
236 if row_pins and col_pins:
237 if 'matrix_pins' in info_data:
238 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
239
240 info_data['matrix_pins'] = {}
67 241
68 if row_pins: 242 if row_pins:
69 info_data['matrix_pins']['rows'] = row_pins.split(',') 243 info_data['matrix_pins']['rows'] = row_pins.split(',')
70 if col_pins: 244
71 info_data['matrix_pins']['cols'] = col_pins.split(',') 245 if col_pins:
246 info_data['matrix_pins']['cols'] = col_pins.split(',')
72 247
73 if direct_pins: 248 if direct_pins:
249 if 'matrix_pins' in info_data:
250 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
251
252 info_data['matrix_pins'] = {}
74 direct_pin_array = [] 253 direct_pin_array = []
254
75 for row in direct_pins.split('},{'): 255 for row in direct_pins.split('},{'):
76 if row.startswith('{'): 256 if row.startswith('{'):
77 row = row[1:] 257 row = row[1:]
258
78 if row.endswith('}'): 259 if row.endswith('}'):
79 row = row[:-1] 260 row = row[:-1]
80 261
@@ -86,15 +267,43 @@ def _extract_config_h(info_data):
86 267
87 direct_pin_array[-1].append(pin) 268 direct_pin_array[-1].append(pin)
88 269
89 info_data['matrix_pins']['direct'] = direct_pin_array 270 info_data['matrix_pins']['direct'] = direct_pin_array
90 271
91 info_data['usb'] = { 272 return info_data
92 'vid': config_c.get('VENDOR_ID'), 273
93 'pid': config_c.get('PRODUCT_ID'), 274
94 'device_ver': config_c.get('DEVICE_VER'), 275def _extract_usb_info(info_data, config_c):
95 'manufacturer': config_c.get('MANUFACTURER'), 276 """Populate the USB information.
96 'product': config_c.get('PRODUCT'), 277 """
97 } 278 usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
279
280 if 'usb' not in info_data:
281 info_data['usb'] = {}
282
283 for info_name, config_name in usb_properties.items():
284 if config_name in config_c:
285 if info_name in info_data['usb']:
286 _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
287
288 info_data['usb'][info_name] = config_c[config_name]
289
290 elif info_name not in info_data['usb']:
291 _log_error(info_data, '%s not specified in config.h, and %s not specified in info.json. One is required.' % (config_name, info_name))
292
293 return info_data
294
295
296def _extract_config_h(info_data):
297 """Pull some keyboard information from existing config.h files
298 """
299 config_c = config_h(info_data['keyboard_folder'])
300
301 _extract_debounce(info_data, config_c)
302 _extract_diode_direction(info_data, config_c)
303 _extract_indicators(info_data, config_c)
304 _extract_matrix_info(info_data, config_c)
305 _extract_usb_info(info_data, config_c)
306 _extract_rgblight(info_data, config_c)
98 307
99 return info_data 308 return info_data
100 309
@@ -106,16 +315,52 @@ def _extract_rules_mk(info_data):
106 mcu = rules.get('MCU') 315 mcu = rules.get('MCU')
107 316
108 if mcu in CHIBIOS_PROCESSORS: 317 if mcu in CHIBIOS_PROCESSORS:
109 return arm_processor_rules(info_data, rules) 318 arm_processor_rules(info_data, rules)
110 319
111 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: 320 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
112 return avr_processor_rules(info_data, rules) 321 avr_processor_rules(info_data, rules)
322
323 else:
324 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
325 unknown_processor_rules(info_data, rules)
326
327 _extract_community_layouts(info_data, rules)
328 _extract_features(info_data, rules)
329
330 return info_data
113 331
114 msg = "Unknown MCU: " + str(mcu)
115 332
116 _log_warning(info_data, msg) 333def _merge_layouts(info_data, new_info_data):
334 """Merge new_info_data into info_data in an intelligent way.
335 """
336 for layout_name, layout_json in new_info_data['layouts'].items():
337 if layout_name in info_data['layouts']:
338 # Pull in layouts we have a macro for
339 if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
340 msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
341 _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
342 else:
343 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
344 key.update(layout_json['layout'][i])
345 else:
346 # Pull in layouts that have matrix data
347 missing_matrix = False
348 for key in layout_json.get('layout', {}):
349 if 'matrix' not in key:
350 missing_matrix = True
351
352 if not missing_matrix:
353 if layout_name in info_data['layouts']:
354 # Update an existing layout with new data
355 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
356 key.update(layout_json['layout'][i])
117 357
118 return unknown_processor_rules(info_data, rules) 358 else:
359 # Copy in the new layout wholesale
360 layout_json['c_macro'] = False
361 info_data['layouts'][layout_name] = layout_json
362
363 return info_data
119 364
120 365
121def _search_keyboard_h(path): 366def _search_keyboard_h(path):
@@ -131,34 +376,21 @@ def _search_keyboard_h(path):
131 return layouts 376 return layouts
132 377
133 378
134def _find_all_layouts(info_data, keyboard, rules): 379def _find_all_layouts(info_data, keyboard):
135 """Looks for layout macros associated with this keyboard. 380 """Looks for layout macros associated with this keyboard.
136 """ 381 """
137 layouts = _search_keyboard_h(Path(keyboard)) 382 layouts = _search_keyboard_h(Path(keyboard))
138 383
139 if not layouts: 384 if not layouts:
140 # If we didn't find any layouts above we widen our search. This is error 385 # 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.
141 # prone which is why we want to encourage people to follow the standard above. 386 info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
142 _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.') 387
143 for file in glob('keyboards/%s/*.h' % keyboard): 388 for file in glob('keyboards/%s/*.h' % keyboard):
144 if file.endswith('.h'): 389 if file.endswith('.h'):
145 these_layouts = find_layouts(file) 390 these_layouts = find_layouts(file)
146 if these_layouts: 391 if these_layouts:
147 layouts.update(these_layouts) 392 layouts.update(these_layouts)
148 393
149 if 'LAYOUTS' in rules:
150 # Match these up against the supplied layouts
151 supported_layouts = rules['LAYOUTS'].strip().split()
152 for layout_name in sorted(layouts):
153 if not layout_name.startswith('LAYOUT_'):
154 continue
155 layout_name = layout_name[7:]
156 if layout_name in supported_layouts:
157 supported_layouts.remove(layout_name)
158
159 if supported_layouts:
160 _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts)))
161
162 return layouts 394 return layouts
163 395
164 396
@@ -231,32 +463,40 @@ def merge_info_jsons(keyboard, info_data):
231 for info_file in find_info_json(keyboard): 463 for info_file in find_info_json(keyboard):
232 # Load and validate the JSON data 464 # Load and validate the JSON data
233 try: 465 try:
234 with info_file.open('r') as info_fd: 466 new_info_data = json.load(info_file.open('r'))
235 new_info_data = json.load(info_fd)
236 except Exception as e: 467 except Exception as e:
237 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e)) 468 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
238 continue 469 new_info_data = {}
239 470
240 if not isinstance(new_info_data, dict): 471 if not isinstance(new_info_data, dict):
241 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 472 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
242 continue 473 continue
243 474
244 # Copy whitelisted keys into `info_data` 475 # Copy whitelisted keys into `info_data`
245 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): 476 for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
246 if key in new_info_data: 477 if key in new_info_data:
247 info_data[key] = new_info_data[key] 478 info_data[key] = new_info_data[key]
248 479
249 # Merge the layouts in 480 # 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.
482 for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
483 if key in new_info_data:
484 if key not in info_data:
485 info_data[key] = {}
486
487 info_data[key].update(new_info_data[key])
488
489 # Merge the layouts
490 if 'community_layouts' in new_info_data:
491 if 'community_layouts' in info_data:
492 for layout in new_info_data['community_layouts']:
493 if layout not in info_data['community_layouts']:
494 info_data['community_layouts'].append(layout)
495 else:
496 info_data['community_layouts'] = new_info_data['community_layouts']
497
250 if 'layouts' in new_info_data: 498 if 'layouts' in new_info_data:
251 for layout_name, json_layout in new_info_data['layouts'].items(): 499 _merge_layouts(info_data, new_info_data)
252 # Only pull in layouts we have a macro for
253 if layout_name in info_data['layouts']:
254 if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
255 msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s'
256 _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
257 else:
258 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
259 key.update(json_layout['layout'][i])
260 500
261 return info_data 501 return info_data
262 502