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.py355
1 files changed, 296 insertions, 59 deletions
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index f476dc666..d7b128aa6 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,179 @@ 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 info_data['indicators'][json_key] = config_c.get(config_key)
138
139 return info_data
140
141
142def _extract_community_layouts(info_data, rules):
143 """Find the community layouts in rules.mk.
144 """
145 community_layouts = rules['LAYOUTS'].split() if 'LAYOUTS' in rules else []
146
147 if 'community_layouts' in info_data:
148 for layout in community_layouts:
149 if layout not in info_data['community_layouts']:
150 community_layouts.append(layout)
151
152 else:
153 info_data['community_layouts'] = community_layouts
154
155 return info_data
156
157
158def _extract_features(info_data, rules):
159 """Find all the features enabled in rules.mk.
160 """
161 for key, value in rules.items():
162 if key.endswith('_ENABLE'):
163 key = '_'.join(key.split('_')[:-1]).lower()
164 value = True if value in true_values else False if value in false_values else value
165
166 if 'config_h_features' not in info_data:
167 info_data['config_h_features'] = {}
168
169 if 'features' not in info_data:
170 info_data['features'] = {}
171
172 if key in info_data['features']:
173 _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
174
175 info_data['features'][key] = value
176 info_data['config_h_features'][key] = value
177
178 return info_data
179
180
181def _extract_rgblight(info_data, config_c):
182 """Handle the rgblight configuration
183 """
184 rgblight = info_data.get('rgblight', {})
185 animations = rgblight.get('animations', {})
186
187 for json_key, config_key in rgblight_properties.items():
188 if config_key in config_c:
189 if json_key in rgblight:
190 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
191
192 rgblight[json_key] = config_c[config_key]
193
194 for json_key, config_key in rgblight_toggles.items():
195 if config_key in config_c:
196 if json_key in rgblight:
197 _log_warning(info_data, 'RGB Light: %s is specified in both info.json and config.h, the config.h value wins.', json_key)
198
199 rgblight[json_key] = config_c[config_key]
200
201 for json_key, config_key in rgblight_animations.items():
202 if config_key in config_c:
203 if json_key in animations:
204 _log_warning(info_data, 'RGB Light: animations: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
205
206 animations[json_key] = config_c[config_key]
207
208 if animations:
209 rgblight['animations'] = animations
210
211 if rgblight:
212 info_data['rgblight'] = rgblight
213
214 return info_data
215
216
217def _extract_matrix_info(info_data, config_c):
218 """Populate the matrix information.
55 """ 219 """
56 config_c = config_h(info_data['keyboard_folder'])
57 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip() 220 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
58 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip() 221 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
59 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1] 222 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
60 223
61 info_data['diode_direction'] = config_c.get('DIODE_DIRECTION') 224 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
62 info_data['matrix_size'] = { 225 if 'matrix_size' in info_data:
63 'rows': compute(config_c.get('MATRIX_ROWS', '0')), 226 _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')),
65 }
66 info_data['matrix_pins'] = {}
67 227
68 if row_pins: 228 info_data['matrix_size'] = {
69 info_data['matrix_pins']['rows'] = row_pins.split(',') 229 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
70 if col_pins: 230 'cols': compute(config_c.get('MATRIX_COLS', '0')),
71 info_data['matrix_pins']['cols'] = col_pins.split(',') 231 }
232
233 if row_pins and col_pins:
234 if 'matrix_pins' in info_data:
235 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
236
237 info_data['matrix_pins'] = {}
238
239 if row_pins:
240 info_data['matrix_pins']['rows'] = row_pins.split(',')
241
242 if col_pins:
243 info_data['matrix_pins']['cols'] = col_pins.split(',')
72 244
73 if direct_pins: 245 if direct_pins:
246 if 'matrix_pins' in info_data:
247 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
248
249 info_data['matrix_pins'] = {}
74 direct_pin_array = [] 250 direct_pin_array = []
251
75 for row in direct_pins.split('},{'): 252 for row in direct_pins.split('},{'):
76 if row.startswith('{'): 253 if row.startswith('{'):
77 row = row[1:] 254 row = row[1:]
255
78 if row.endswith('}'): 256 if row.endswith('}'):
79 row = row[:-1] 257 row = row[:-1]
80 258
@@ -86,15 +264,43 @@ def _extract_config_h(info_data):
86 264
87 direct_pin_array[-1].append(pin) 265 direct_pin_array[-1].append(pin)
88 266
89 info_data['matrix_pins']['direct'] = direct_pin_array 267 info_data['matrix_pins']['direct'] = direct_pin_array
90 268
91 info_data['usb'] = { 269 return info_data
92 'vid': config_c.get('VENDOR_ID'), 270
93 'pid': config_c.get('PRODUCT_ID'), 271
94 'device_ver': config_c.get('DEVICE_VER'), 272def _extract_usb_info(info_data, config_c):
95 'manufacturer': config_c.get('MANUFACTURER'), 273 """Populate the USB information.
96 'product': config_c.get('PRODUCT'), 274 """
97 } 275 usb_properties = {'vid': 'VENDOR_ID', 'pid': 'PRODUCT_ID', 'device_ver': 'DEVICE_VER'}
276
277 if 'usb' not in info_data:
278 info_data['usb'] = {}
279
280 for info_name, config_name in usb_properties.items():
281 if config_name in config_c:
282 if info_name in info_data['usb']:
283 _log_warning(info_data, '%s in config.h is overwriting usb.%s in info.json' % (config_name, info_name))
284
285 info_data['usb'][info_name] = config_c[config_name]
286
287 elif info_name not in info_data['usb']:
288 _log_error(info_data, '%s not specified in config.h, and %s not specified in info.json. One is required.' % (config_name, info_name))
289
290 return info_data
291
292
293def _extract_config_h(info_data):
294 """Pull some keyboard information from existing config.h files
295 """
296 config_c = config_h(info_data['keyboard_folder'])
297
298 _extract_debounce(info_data, config_c)
299 _extract_diode_direction(info_data, config_c)
300 _extract_indicators(info_data, config_c)
301 _extract_matrix_info(info_data, config_c)
302 _extract_usb_info(info_data, config_c)
303 _extract_rgblight(info_data, config_c)
98 304
99 return info_data 305 return info_data
100 306
@@ -106,16 +312,52 @@ def _extract_rules_mk(info_data):
106 mcu = rules.get('MCU') 312 mcu = rules.get('MCU')
107 313
108 if mcu in CHIBIOS_PROCESSORS: 314 if mcu in CHIBIOS_PROCESSORS:
109 return arm_processor_rules(info_data, rules) 315 arm_processor_rules(info_data, rules)
110 316
111 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS: 317 elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
112 return avr_processor_rules(info_data, rules) 318 avr_processor_rules(info_data, rules)
319
320 else:
321 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], mcu))
322 unknown_processor_rules(info_data, rules)
323
324 _extract_community_layouts(info_data, rules)
325 _extract_features(info_data, rules)
326
327 return info_data
113 328
114 msg = "Unknown MCU: " + str(mcu)
115 329
116 _log_warning(info_data, msg) 330def _merge_layouts(info_data, new_info_data):
331 """Merge new_info_data into info_data in an intelligent way.
332 """
333 for layout_name, layout_json in new_info_data['layouts'].items():
334 if layout_name in info_data['layouts']:
335 # Pull in layouts we have a macro for
336 if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
337 msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
338 _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
339 else:
340 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
341 key.update(layout_json['layout'][i])
342 else:
343 # Pull in layouts that have matrix data
344 missing_matrix = False
345 for key in layout_json['layout']:
346 if 'matrix' not in key:
347 missing_matrix = True
348
349 if not missing_matrix:
350 if layout_name in info_data['layouts']:
351 # Update an existing layout with new data
352 for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
353 key.update(layout_json['layout'][i])
117 354
118 return unknown_processor_rules(info_data, rules) 355 else:
356 # Copy in the new layout wholesale
357 layout_json['c_macro'] = False
358 info_data['layouts'][layout_name] = layout_json
359
360 return info_data
119 361
120 362
121def _search_keyboard_h(path): 363def _search_keyboard_h(path):
@@ -131,34 +373,21 @@ def _search_keyboard_h(path):
131 return layouts 373 return layouts
132 374
133 375
134def _find_all_layouts(info_data, keyboard, rules): 376def _find_all_layouts(info_data, keyboard):
135 """Looks for layout macros associated with this keyboard. 377 """Looks for layout macros associated with this keyboard.
136 """ 378 """
137 layouts = _search_keyboard_h(Path(keyboard)) 379 layouts = _search_keyboard_h(Path(keyboard))
138 380
139 if not layouts: 381 if not layouts:
140 # If we didn't find any layouts above we widen our search. This is error 382 # 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. 383 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.') 384
143 for file in glob('keyboards/%s/*.h' % keyboard): 385 for file in glob('keyboards/%s/*.h' % keyboard):
144 if file.endswith('.h'): 386 if file.endswith('.h'):
145 these_layouts = find_layouts(file) 387 these_layouts = find_layouts(file)
146 if these_layouts: 388 if these_layouts:
147 layouts.update(these_layouts) 389 layouts.update(these_layouts)
148 390
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 391 return layouts
163 392
164 393
@@ -231,32 +460,40 @@ def merge_info_jsons(keyboard, info_data):
231 for info_file in find_info_json(keyboard): 460 for info_file in find_info_json(keyboard):
232 # Load and validate the JSON data 461 # Load and validate the JSON data
233 try: 462 try:
234 with info_file.open('r') as info_fd: 463 new_info_data = json.load(info_file.open('r'))
235 new_info_data = json.load(info_fd)
236 except Exception as e: 464 except Exception as e:
237 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e)) 465 _log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
238 continue 466 new_info_data = {}
239 467
240 if not isinstance(new_info_data, dict): 468 if not isinstance(new_info_data, dict):
241 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) 469 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
242 continue 470 continue
243 471
244 # Copy whitelisted keys into `info_data` 472 # Copy whitelisted keys into `info_data`
245 for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'): 473 for key in ('debounce', 'diode_direction', 'indicators', 'keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
246 if key in new_info_data: 474 if key in new_info_data:
247 info_data[key] = new_info_data[key] 475 info_data[key] = new_info_data[key]
248 476
249 # Merge the layouts in 477 # Deep merge certain keys
478 # 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.
479 for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
480 if key in new_info_data:
481 if key not in info_data:
482 info_data[key] = {}
483
484 info_data[key].update(new_info_data[key])
485
486 # Merge the layouts
487 if 'community_layouts' in new_info_data:
488 if 'community_layouts' in info_data:
489 for layout in new_info_data['community_layouts']:
490 if layout not in info_data['community_layouts']:
491 info_data['community_layouts'].append(layout)
492 else:
493 info_data['community_layouts'] = new_info_data['community_layouts']
494
250 if 'layouts' in new_info_data: 495 if 'layouts' in new_info_data:
251 for layout_name, json_layout in new_info_data['layouts'].items(): 496 _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 497
261 return info_data 498 return info_data
262 499