keyboard stuff
1"""Functions that help us generate and use info.json files.
2"""
3import re
4import os
5from pathlib import Path
6import jsonschema
7from dotty_dict import dotty
8from enum import IntFlag
9
10from milc import cli
11
12from qmk.constants import COL_LETTERS, ROW_LETTERS, CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, JOYSTICK_AXES
13from qmk.c_parse import find_layouts, parse_config_h_file, find_led_config
14from qmk.json_schema import deep_update, json_load, validate
15from qmk.keyboard import config_h, rules_mk
16from qmk.commands import parse_configurator_json
17from qmk.makefile import parse_rules_mk_file
18from qmk.math_ops import compute
19from qmk.util import maybe_exit, truthy
20
21true_values = ['1', 'on', 'yes']
22false_values = ['0', 'off', 'no']
23
24
25class LedFlags(IntFlag):
26 ALL = 0xFF
27 NONE = 0x00
28 MODIFIER = 0x01
29 UNDERGLOW = 0x02
30 KEYLIGHT = 0x04
31 INDICATOR = 0x08
32
33
34def _keyboard_in_layout_name(keyboard, layout):
35 """Validate that a layout macro does not contain name of keyboard
36 """
37 # TODO: reduce this list down
38 safe_layout_tokens = {
39 'ansi',
40 'iso',
41 'jp',
42 'jis',
43 'ortho',
44 'wkl',
45 'tkl',
46 'preonic',
47 'planck',
48 }
49
50 # Ignore tokens like 'split_3x7_4' or just '2x4'
51 layout = re.sub(r"_split_\d+x\d+_\d+", '', layout)
52 layout = re.sub(r"_\d+x\d+", '', layout)
53
54 name_fragments = set(keyboard.split('/')) - safe_layout_tokens
55
56 return any(fragment in layout for fragment in name_fragments)
57
58
59def _valid_community_layout(layout):
60 """Validate that a declared community list exists
61 """
62 return (Path('layouts/default') / layout).exists()
63
64
65def _get_key_left_position(key):
66 # Special case for ISO enter
67 return key['x'] - 0.25 if key.get('h', 1) == 2 and key.get('w', 1) == 1.25 else key['x']
68
69
70def _find_invalid_encoder_index(info_data):
71 """Perform additional validation of encoders
72 """
73 enc_left = info_data.get('encoder', {}).get('rotary', [])
74 enc_right = []
75
76 if info_data.get('split', {}).get('enabled', False):
77 enc_right = info_data.get('split', {}).get('encoder', {}).get('right', {}).get('rotary', enc_left)
78
79 enc_count = len(enc_left) + len(enc_right)
80
81 ret = []
82 layouts = info_data.get('layouts', {})
83 for layout_name, layout_data in layouts.items():
84 found = set()
85 for key in layout_data['layout']:
86 if 'encoder' in key:
87 if enc_count == 0:
88 ret.append((layout_name, key['encoder'], 'non-configured'))
89 elif key['encoder'] >= enc_count:
90 ret.append((layout_name, key['encoder'], 'out of bounds'))
91 elif key['encoder'] in found:
92 ret.append((layout_name, key['encoder'], 'duplicate'))
93 found.add(key['encoder'])
94
95 return ret
96
97
98def _validate_build_target(keyboard, info_data):
99 """Non schema checks
100 """
101 keyboard_json_path = Path('keyboards') / keyboard / 'keyboard.json'
102 config_files = find_info_json(keyboard)
103
104 # keyboard.json can only exist at the deepest part of the tree
105 keyboard_json_count = 0
106 for info_file in config_files:
107 if info_file.name == 'keyboard.json':
108 keyboard_json_count += 1
109 if info_file != keyboard_json_path:
110 _log_error(info_data, f'Invalid keyboard.json location detected: {info_file}.')
111
112 # No keyboard.json next to info.json
113 for conf_file in config_files:
114 if conf_file.name == 'keyboard.json':
115 info_file = conf_file.parent / 'info.json'
116 if info_file.exists():
117 _log_error(info_data, f'Invalid info.json location detected: {info_file}.')
118
119 # Moving forward keyboard.json should be used as a build target
120 if keyboard_json_count == 0:
121 _log_warning(info_data, 'Build marker "keyboard.json" not found.')
122
123
124def _validate_layouts(keyboard, info_data): # noqa C901
125 """Non schema checks
126 """
127 col_num = info_data.get('matrix_size', {}).get('cols', 0)
128 row_num = info_data.get('matrix_size', {}).get('rows', 0)
129 layouts = info_data.get('layouts', {})
130 layout_aliases = info_data.get('layout_aliases', {})
131 community_layouts = info_data.get('community_layouts', [])
132 community_layouts_names = list(map(lambda layout: f'LAYOUT_{layout}', community_layouts))
133
134 # Make sure we have at least one layout
135 if len(layouts) == 0 or all(not layout.get('json_layout', False) for layout in layouts.values()):
136 _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in info.json.')
137
138 # Make sure all layouts are DD
139 for layout_name, layout_data in layouts.items():
140 if layout_data.get('c_macro', False):
141 _log_error(info_data, f'{layout_name}: Layout macro should not be defined within ".h" files.')
142
143 # Make sure all matrix values are in bounds
144 for layout_name, layout_data in layouts.items():
145 for index, key_data in enumerate(layout_data['layout']):
146 row, col = key_data['matrix']
147 key_name = key_data.get('label', f'k{ROW_LETTERS[row]}{COL_LETTERS[col]}')
148 if row >= row_num:
149 _log_error(info_data, f'{layout_name}: Matrix row for key {index} ({key_name}) is {row} but must be less than {row_num}')
150 if col >= col_num:
151 _log_error(info_data, f'{layout_name}: Matrix column for key {index} ({key_name}) is {col} but must be less than {col_num}')
152
153 # Reject duplicate matrix locations
154 for layout_name, layout_data in layouts.items():
155 seen = set()
156 for index, key_data in enumerate(layout_data['layout']):
157 key = f"{key_data['matrix']}"
158 if key in seen:
159 _log_error(info_data, f'{layout_name}: Matrix location for key {index} is not unique {key_data}')
160 seen.add(key)
161
162 # Warn if physical positions are offset (at least one key should be at x=0, and at least one key at y=0)
163 for layout_name, layout_data in layouts.items():
164 offset_x = min([_get_key_left_position(k) for k in layout_data['layout']])
165 if offset_x > 0:
166 _log_warning(info_data, f'Layout "{layout_name}" is offset on X axis by {offset_x}')
167
168 offset_y = min([k['y'] for k in layout_data['layout']])
169 if offset_y > 0:
170 _log_warning(info_data, f'Layout "{layout_name}" is offset on Y axis by {offset_y}')
171
172 # Providing only LAYOUT_all "because I define my layouts in a 3rd party tool"
173 if len(layouts) == 1 and 'LAYOUT_all' in layouts:
174 _log_warning(info_data, '"LAYOUT_all" should be "LAYOUT" unless additional layouts are provided.')
175
176 # Extended layout name checks - ignoring community_layouts and "safe" values
177 potential_layouts = set(layouts.keys()) - set(community_layouts_names)
178 for layout in potential_layouts:
179 if _keyboard_in_layout_name(keyboard, layout):
180 _log_warning(info_data, f'Layout "{layout}" should not contain name of keyboard.')
181
182 # Filter out any non-existing community layouts
183 for layout in community_layouts:
184 if not _valid_community_layout(layout):
185 # Ignore layout from future checks
186 info_data['community_layouts'].remove(layout)
187 _log_error(info_data, 'Claims to support a community layout that does not exist: %s' % (layout))
188
189 # Make sure we supply layout macros for the community layouts we claim to support
190 for layout_name in community_layouts_names:
191 if layout_name not in layouts and layout_name not in layout_aliases:
192 _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
193
194
195def _validate_keycodes(keyboard, info_data):
196 """Non schema checks
197 """
198 # keycodes with length > 7 must have short forms for visualisation purposes
199 for decl in info_data.get('keycodes', []):
200 if len(decl["key"]) > 7:
201 if not decl.get("aliases", []):
202 _log_error(info_data, f'Keycode {decl["key"]} has no short form alias')
203
204
205def _validate_encoders(keyboard, info_data):
206 """Non schema checks
207 """
208 # encoder IDs in layouts must be in range and not duplicated
209 found = _find_invalid_encoder_index(info_data)
210 for layout_name, encoder_index, reason in found:
211 _log_error(info_data, f'Layout "{layout_name}" contains {reason} encoder index {encoder_index}.')
212
213
214def _validate(keyboard, info_data):
215 """Perform various validation on the provided info.json data
216 """
217 # First validate against the jsonschema
218 try:
219 validate(info_data, 'qmk.api.keyboard.v1')
220
221 # Additional validation
222 _validate_build_target(keyboard, info_data)
223 _validate_layouts(keyboard, info_data)
224 _validate_keycodes(keyboard, info_data)
225 _validate_encoders(keyboard, info_data)
226
227 except jsonschema.ValidationError as e:
228 json_path = '.'.join([str(p) for p in e.absolute_path])
229 cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
230 maybe_exit(1)
231
232
233def info_json(keyboard, force_layout=None):
234 """Generate the info.json data for a specific keyboard.
235 """
236 info_data = {
237 'keyboard_name': str(keyboard),
238 'keyboard_folder': str(keyboard),
239 'keymaps': {},
240 'layouts': {},
241 'parse_errors': [],
242 'parse_warnings': [],
243 'maintainer': 'qmk',
244 }
245
246 # Populate layout data
247 layouts, aliases = _search_keyboard_h(keyboard)
248
249 if aliases:
250 info_data['layout_aliases'] = aliases
251
252 for layout_name, layout_json in layouts.items():
253 if not layout_name.startswith('LAYOUT_kc'):
254 layout_json['c_macro'] = True
255 layout_json['json_layout'] = False
256 info_data['layouts'][layout_name] = layout_json
257
258 # Merge in the data from info.json, config.h, and rules.mk
259 info_data = merge_info_jsons(keyboard, info_data)
260 info_data = _process_defaults(info_data)
261 info_data = _extract_rules_mk(info_data, rules_mk(str(keyboard)))
262 info_data = _extract_config_h(info_data, config_h(str(keyboard)))
263
264 # Ensure that we have various calculated values
265 info_data = _matrix_size(info_data)
266 info_data = _joystick_axis_count(info_data)
267 info_data = _matrix_masked(info_data)
268
269 # Merge in data from <keyboard.c>
270 info_data = _extract_led_config(info_data, str(keyboard))
271
272 # Force a community layout if requested
273 community_layouts = info_data.get("community_layouts", [])
274 if force_layout in community_layouts:
275 info_data["community_layouts"] = [force_layout]
276
277 # Validate
278 # Skip processing if necessary
279 if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False):
280 _validate(keyboard, info_data)
281
282 # Check that the reported matrix size is consistent with the actual matrix size
283 _check_matrix(info_data)
284
285 return info_data
286
287
288def _extract_features(info_data, rules):
289 """Find all the features enabled in rules.mk.
290 """
291 # Process booleans rules
292 for key, value in rules.items():
293 if key.endswith('_ENABLE'):
294 key = '_'.join(key.split('_')[:-1]).lower()
295 value = True if value.lower() in true_values else False if value.lower() in false_values else value
296
297 if key in ['lto']:
298 continue
299
300 if 'config_h_features' not in info_data:
301 info_data['config_h_features'] = {}
302
303 if 'features' not in info_data:
304 info_data['features'] = {}
305
306 if key in info_data['features']:
307 _log_warning(info_data, 'Feature %s is specified in both info.json (%s) and rules.mk (%s). The rules.mk value wins.' % (key, info_data['features'], value))
308
309 info_data['features'][key] = value
310 info_data['config_h_features'][key] = value
311
312 return info_data
313
314
315def _extract_matrix_rules(info_data, rules):
316 """Find all the features enabled in rules.mk.
317 """
318 if rules.get('CUSTOM_MATRIX', 'no') != 'no':
319 if 'matrix_pins' in info_data and 'custom' in info_data['matrix_pins']:
320 _log_warning(info_data, 'Custom Matrix is specified in both info.json and rules.mk, the rules.mk values win.')
321
322 if 'matrix_pins' not in info_data:
323 info_data['matrix_pins'] = {}
324
325 if rules['CUSTOM_MATRIX'] == 'lite':
326 info_data['matrix_pins']['custom_lite'] = True
327 else:
328 info_data['matrix_pins']['custom'] = True
329
330 return info_data
331
332
333def _pin_name(pin):
334 """Returns the proper representation for a pin.
335 """
336 pin = pin.strip()
337
338 if not pin:
339 return None
340
341 elif pin.isdigit():
342 return int(pin)
343
344 elif pin == 'NO_PIN':
345 return None
346
347 return pin
348
349
350def _extract_pins(pins):
351 """Returns a list of pins from a comma separated string of pins.
352 """
353 return [_pin_name(pin) for pin in pins.split(',')]
354
355
356def _extract_2d_array(raw):
357 """Return a 2d array of strings
358 """
359 out_array = []
360
361 while raw[-1] != '}':
362 raw = raw[:-1]
363
364 for row in raw.split('},{'):
365 if row.startswith('{'):
366 row = row[1:]
367
368 if row.endswith('}'):
369 row = row[:-1]
370
371 out_array.append([])
372
373 for val in row.split(','):
374 out_array[-1].append(val)
375
376 return out_array
377
378
379def _extract_2d_int_array(raw):
380 """Return a 2d array of ints
381 """
382 ret = _extract_2d_array(raw)
383
384 return [list(map(int, x)) for x in ret]
385
386
387def _extract_direct_matrix(direct_pins):
388 """extract direct_matrix
389 """
390 direct_pin_array = _extract_2d_array(direct_pins)
391
392 for i in range(len(direct_pin_array)):
393 for j in range(len(direct_pin_array[i])):
394 if direct_pin_array[i][j] == 'NO_PIN':
395 direct_pin_array[i][j] = None
396
397 return direct_pin_array
398
399
400def _extract_audio(info_data, config_c):
401 """Populate data about the audio configuration
402 """
403 audio_pins = []
404
405 for pin in 'B5', 'B6', 'B7', 'C4', 'C5', 'C6':
406 if config_c.get(f'{pin}_AUDIO'):
407 audio_pins.append(pin)
408
409 if audio_pins:
410 info_data['audio'] = {'pins': audio_pins}
411
412
413def _extract_encoders_values(config_c, postfix=''):
414 """Common encoder extraction logic
415 """
416 a_pad = config_c.get(f'ENCODER_A_PINS{postfix}', '').replace(' ', '')[1:-1]
417 b_pad = config_c.get(f'ENCODER_B_PINS{postfix}', '').replace(' ', '')[1:-1]
418 resolutions = config_c.get(f'ENCODER_RESOLUTIONS{postfix}', '').replace(' ', '')[1:-1]
419
420 default_resolution = config_c.get('ENCODER_RESOLUTION', None)
421
422 if a_pad and b_pad:
423 a_pad = list(filter(None, a_pad.split(',')))
424 b_pad = list(filter(None, b_pad.split(',')))
425 resolutions = list(filter(None, resolutions.split(',')))
426 if default_resolution:
427 resolutions += [default_resolution] * (len(a_pad) - len(resolutions))
428
429 encoders = []
430 for index in range(len(a_pad)):
431 encoder = {'pin_a': a_pad[index], 'pin_b': b_pad[index]}
432 if index < len(resolutions):
433 encoder['resolution'] = int(resolutions[index])
434 encoders.append(encoder)
435
436 return encoders
437
438
439def _extract_encoders(info_data, config_c):
440 """Populate data about encoder pins
441 """
442 encoders = _extract_encoders_values(config_c)
443 if encoders:
444 if 'encoder' not in info_data:
445 info_data['encoder'] = {}
446
447 if 'rotary' in info_data['encoder']:
448 _log_warning(info_data, 'Encoder config is specified in both config.h (%s) and info.json (%s). The config.h value wins.' % (encoders, info_data['encoder']['rotary']))
449
450 info_data['encoder']['rotary'] = encoders
451
452 # TODO: some logic still assumes ENCODER_ENABLED would partially create encoder dict
453 if info_data.get('features', {}).get('encoder', False):
454 if 'encoder' not in info_data:
455 info_data['encoder'] = {}
456 info_data['encoder']['enabled'] = True
457
458
459def _extract_split_encoders(info_data, config_c):
460 """Populate data about split encoder pins
461 """
462 encoders = _extract_encoders_values(config_c, '_RIGHT')
463 if encoders:
464 if 'split' not in info_data:
465 info_data['split'] = {}
466
467 if 'encoder' not in info_data['split']:
468 info_data['split']['encoder'] = {}
469
470 if 'right' not in info_data['split']['encoder']:
471 info_data['split']['encoder']['right'] = {}
472
473 if 'rotary' in info_data['split']['encoder']['right']:
474 _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'])
475
476 info_data['split']['encoder']['right']['rotary'] = encoders
477
478
479def _extract_secure_unlock(info_data, config_c):
480 """Populate data about the secure unlock sequence
481 """
482 unlock = config_c.get('SECURE_UNLOCK_SEQUENCE', '').replace(' ', '')[1:-1]
483 if unlock:
484 unlock_array = _extract_2d_int_array(unlock)
485 if 'secure' not in info_data:
486 info_data['secure'] = {}
487
488 if 'unlock_sequence' in info_data['secure']:
489 _log_warning(info_data, 'Secure unlock sequence is specified in both config.h (SECURE_UNLOCK_SEQUENCE) and info.json (secure.unlock_sequence) (Value: %s), the config.h value wins.' % info_data['secure']['unlock_sequence'])
490
491 info_data['secure']['unlock_sequence'] = unlock_array
492
493
494def _extract_split_handedness(info_data, config_c):
495 # Migrate
496 split = info_data.get('split', {})
497 if 'matrix_grid' in split:
498 split['handedness'] = split.get('handedness', {})
499 split['handedness']['matrix_grid'] = split.pop('matrix_grid')
500
501
502def _extract_split_serial(info_data, config_c):
503 # Migrate
504 split = info_data.get('split', {})
505 if 'soft_serial_pin' in split:
506 split['serial'] = split.get('serial', {})
507 split['serial']['pin'] = split.pop('soft_serial_pin')
508 if 'soft_serial_speed' in split:
509 split['serial'] = split.get('serial', {})
510 split['serial']['speed'] = split.pop('soft_serial_speed')
511
512
513def _extract_split_transport(info_data, config_c):
514 # Figure out the transport method
515 if config_c.get('USE_I2C') is True:
516 if 'split' not in info_data:
517 info_data['split'] = {}
518
519 if 'transport' not in info_data['split']:
520 info_data['split']['transport'] = {}
521
522 if 'protocol' in info_data['split']['transport']:
523 _log_warning(info_data, 'Split transport is specified in both config.h (USE_I2C) and info.json (split.transport.protocol) (Value: %s), the config.h value wins.' % info_data['split']['transport'])
524
525 info_data['split']['transport']['protocol'] = 'i2c'
526
527 # Ignore transport defaults if "SPLIT_KEYBOARD" is unset
528 elif 'enabled' in info_data.get('split', {}):
529 if 'split' not in info_data:
530 info_data['split'] = {}
531
532 if 'transport' not in info_data['split']:
533 info_data['split']['transport'] = {}
534
535 if 'protocol' not in info_data['split']['transport']:
536 info_data['split']['transport']['protocol'] = 'serial'
537
538 # Migrate
539 transport = info_data.get('split', {}).get('transport', {})
540 if 'sync_matrix_state' in transport:
541 transport['sync'] = transport.get('sync', {})
542 transport['sync']['matrix_state'] = transport.pop('sync_matrix_state')
543 if 'sync_modifiers' in transport:
544 transport['sync'] = transport.get('sync', {})
545 transport['sync']['modifiers'] = transport.pop('sync_modifiers')
546
547
548def _extract_split_right_pins(info_data, config_c):
549 # Figure out the right half matrix pins
550 row_pins = config_c.get('MATRIX_ROW_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
551 col_pins = config_c.get('MATRIX_COL_PINS_RIGHT', '').replace('{', '').replace('}', '').strip()
552 direct_pins = config_c.get('DIRECT_PINS_RIGHT', '').replace(' ', '')[1:-1]
553
554 if row_pins or col_pins or direct_pins:
555 if info_data.get('split', {}).get('matrix_pins', {}).get('right', None):
556 _log_warning(info_data, 'Right hand matrix data is specified in both info.json and config.h, the config.h values win.')
557
558 if 'split' not in info_data:
559 info_data['split'] = {}
560
561 if 'matrix_pins' not in info_data['split']:
562 info_data['split']['matrix_pins'] = {}
563
564 if 'right' not in info_data['split']['matrix_pins']:
565 info_data['split']['matrix_pins']['right'] = {}
566
567 if col_pins:
568 info_data['split']['matrix_pins']['right']['cols'] = _extract_pins(col_pins)
569
570 if row_pins:
571 info_data['split']['matrix_pins']['right']['rows'] = _extract_pins(row_pins)
572
573 if direct_pins:
574 info_data['split']['matrix_pins']['right']['direct'] = _extract_direct_matrix(direct_pins)
575
576
577def _extract_matrix_info(info_data, config_c):
578 """Populate the matrix information.
579 """
580 row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
581 col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
582 direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
583
584 if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
585 if 'matrix_size' in info_data:
586 _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
587
588 info_data['matrix_size'] = {
589 'cols': compute(config_c.get('MATRIX_COLS', '0')),
590 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
591 }
592
593 if row_pins and col_pins:
594 if 'matrix_pins' in info_data and 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
595 _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
596
597 if 'matrix_pins' not in info_data:
598 info_data['matrix_pins'] = {}
599
600 info_data['matrix_pins']['cols'] = _extract_pins(col_pins)
601 info_data['matrix_pins']['rows'] = _extract_pins(row_pins)
602
603 if direct_pins:
604 if 'matrix_pins' in info_data and 'direct' in info_data['matrix_pins']:
605 _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
606
607 if 'matrix_pins' not in info_data:
608 info_data['matrix_pins'] = {}
609
610 info_data['matrix_pins']['direct'] = _extract_direct_matrix(direct_pins)
611
612 return info_data
613
614
615def _config_to_json(key_type, config_value):
616 """Convert config value using spec
617 """
618 if key_type.startswith('array'):
619 if key_type.count('.') > 1:
620 raise Exception(f"Conversion of {key_type} not possible")
621
622 if '.' in key_type:
623 key_type, array_type = key_type.split('.', 1)
624 else:
625 array_type = None
626
627 config_value = config_value.replace('{', '').replace('}', '').strip()
628
629 if array_type == 'int':
630 return list(map(int, config_value.split(',')))
631 else:
632 return list(map(str.strip, config_value.split(',')))
633
634 elif key_type in ['bool', 'flag']:
635 if isinstance(config_value, bool):
636 return config_value
637 return config_value in true_values
638
639 elif key_type == 'hex':
640 return '0x' + config_value[2:].upper()
641
642 elif key_type == 'list':
643 return config_value.split()
644
645 elif key_type == 'int':
646 return int(config_value)
647
648 elif key_type == 'str':
649 return config_value.strip('"').replace('\\"', '"').replace('\\\\', '\\')
650
651 elif key_type == 'bcd_version':
652 major = int(config_value[2:4])
653 minor = int(config_value[4])
654 revision = int(config_value[5])
655
656 return f'{major}.{minor}.{revision}'
657
658 return config_value
659
660
661def _extract_config_h(info_data, config_c):
662 """Pull some keyboard information from existing config.h files
663 """
664 # Pull in data from the json map
665 dotty_info = dotty(info_data)
666 info_config_map = json_load(Path('data/mappings/info_config.hjson'))
667
668 for config_key, info_dict in info_config_map.items():
669 info_key = info_dict['info_key']
670 key_type = info_dict.get('value_type', 'raw')
671
672 try:
673 replace_with = info_dict.get('replace_with')
674 if config_key in config_c and info_dict.get('invalid', False):
675 if replace_with:
676 _log_error(info_data, '%s in config.h is no longer a valid option and should be replaced with %s' % (config_key, replace_with))
677 else:
678 _log_error(info_data, '%s in config.h is no longer a valid option and should be removed' % config_key)
679 elif config_key in config_c and info_dict.get('deprecated', False):
680 if replace_with:
681 _log_warning(info_data, '%s in config.h is deprecated in favor of %s and will be removed at a later date' % (config_key, replace_with))
682 else:
683 _log_warning(info_data, '%s in config.h is deprecated and will be removed at a later date' % config_key)
684
685 if config_key in config_c and info_dict.get('to_json', True):
686 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
687 _log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
688
689 dotty_info[info_key] = _config_to_json(key_type, config_c[config_key])
690
691 except Exception as e:
692 _log_warning(info_data, f'{config_key}->{info_key}: {e}')
693
694 info_data.update(dotty_info)
695
696 # Pull data that easily can't be mapped in json
697 _extract_matrix_info(info_data, config_c)
698 _extract_audio(info_data, config_c)
699 _extract_secure_unlock(info_data, config_c)
700 _extract_split_handedness(info_data, config_c)
701 _extract_split_serial(info_data, config_c)
702 _extract_split_transport(info_data, config_c)
703 _extract_split_right_pins(info_data, config_c)
704 _extract_encoders(info_data, config_c)
705 _extract_split_encoders(info_data, config_c)
706
707 return info_data
708
709
710def _process_defaults(info_data):
711 """Process any additional defaults based on currently discovered information
712 """
713 defaults_map = json_load(Path('data/mappings/defaults.hjson'))
714 for default_type in defaults_map.keys():
715 thing_map = defaults_map[default_type]
716 if default_type in info_data:
717 merged_count = 0
718 thing_items = thing_map.get(info_data[default_type], {}).items()
719 for key, value in thing_items:
720 if key not in info_data:
721 info_data[key] = value
722 merged_count += 1
723
724 if merged_count == 0 and len(thing_items) > 0:
725 _log_warning(info_data, 'All defaults for \'%s\' were skipped, potential redundant config or misconfiguration detected' % (default_type))
726
727 return info_data
728
729
730def _extract_rules_mk(info_data, rules):
731 """Pull some keyboard information from existing rules.mk files
732 """
733 info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
734
735 if info_data['processor'] in CHIBIOS_PROCESSORS:
736 arm_processor_rules(info_data, rules)
737
738 elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
739 avr_processor_rules(info_data, rules)
740
741 else:
742 cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
743 unknown_processor_rules(info_data, rules)
744
745 # Pull in data from the json map
746 dotty_info = dotty(info_data)
747 info_rules_map = json_load(Path('data/mappings/info_rules.hjson'))
748
749 for rules_key, info_dict in info_rules_map.items():
750 info_key = info_dict['info_key']
751 key_type = info_dict.get('value_type', 'raw')
752
753 try:
754 replace_with = info_dict.get('replace_with')
755 if rules_key in rules and info_dict.get('invalid', False):
756 if replace_with:
757 _log_error(info_data, '%s in rules.mk is no longer a valid option and should be replaced with %s' % (rules_key, replace_with))
758 else:
759 _log_error(info_data, '%s in rules.mk is no longer a valid option and should be removed' % rules_key)
760 elif rules_key in rules and info_dict.get('deprecated', False):
761 if replace_with:
762 _log_warning(info_data, '%s in rules.mk is deprecated in favor of %s and will be removed at a later date' % (rules_key, replace_with))
763 else:
764 _log_warning(info_data, '%s in rules.mk is deprecated and will be removed at a later date' % rules_key)
765
766 if rules_key in rules and info_dict.get('to_json', True):
767 if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
768 _log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
769
770 dotty_info[info_key] = _config_to_json(key_type, rules[rules_key])
771
772 except Exception as e:
773 _log_warning(info_data, f'{rules_key}->{info_key}: {e}')
774
775 info_data.update(dotty_info)
776
777 # Merge in config values that can't be easily mapped
778 _extract_features(info_data, rules)
779 _extract_matrix_rules(info_data, rules)
780
781 return info_data
782
783
784def find_keyboard_c(keyboard):
785 """Find all <keyboard>.c files
786 """
787 keyboard = Path(keyboard)
788 current_path = Path('keyboards/')
789
790 files = []
791 for directory in keyboard.parts:
792 current_path = current_path / directory
793 keyboard_c_path = current_path / f'{directory}.c'
794 if keyboard_c_path.exists():
795 files.append(keyboard_c_path)
796
797 return files
798
799
800def _extract_led_config(info_data, keyboard):
801 """Scan all <keyboard>.c files for led config
802 """
803 for feature in ['rgb_matrix', 'led_matrix']:
804 if info_data.get('features', {}).get(feature, False) or feature in info_data:
805 # Only attempt search if dd led config is missing
806 if 'layout' not in info_data.get(feature, {}):
807 cols = info_data.get('matrix_size', {}).get('cols')
808 rows = info_data.get('matrix_size', {}).get('rows')
809 if cols and rows:
810 # Process
811 for file in find_keyboard_c(keyboard):
812 try:
813 ret = find_led_config(file, cols, rows)
814 if ret:
815 info_data[feature] = info_data.get(feature, {})
816 info_data[feature]['layout'] = ret
817 except Exception as e:
818 _log_warning(info_data, f'led_config: {file.name}: {e}')
819 else:
820 _log_warning(info_data, 'led_config: matrix size required to parse g_led_config')
821
822 if info_data[feature].get('layout', None) and not info_data[feature].get('led_count', None):
823 info_data[feature]['led_count'] = len(info_data[feature]['layout'])
824
825 if info_data[feature].get('layout', None) and not info_data[feature].get('flag_steps', None):
826 flags = {LedFlags.ALL, LedFlags.NONE}
827 default_flags = {LedFlags.MODIFIER | LedFlags.KEYLIGHT, LedFlags.UNDERGLOW}
828
829 # if only a single flag is used, assume only all+none flags
830 kb_flags = set(x.get('flags', LedFlags.NONE) for x in info_data[feature]['layout'])
831 if len(kb_flags) > 1:
832 # check if any part of LED flag is with the defaults
833 unique_flags = set()
834 for candidate in default_flags:
835 if any(candidate & flag for flag in kb_flags):
836 unique_flags.add(candidate)
837
838 # if we still have a single flag, assume only all+none
839 if len(unique_flags) > 1:
840 flags.update(unique_flags)
841
842 info_data[feature]['flag_steps'] = sorted([int(flag) for flag in flags], reverse=True)
843
844 return info_data
845
846
847def _matrix_size(info_data):
848 """Add info_data['matrix_size'] if it doesn't exist.
849 """
850 if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
851 info_data['matrix_size'] = {}
852
853 if 'direct' in info_data['matrix_pins']:
854 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
855 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
856 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
857 info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
858 info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
859
860 # Assumption of split common
861 if 'split' in info_data:
862 if info_data['split'].get('enabled', False):
863 info_data['matrix_size']['rows'] *= 2
864
865 return info_data
866
867
868def _joystick_axis_count(info_data):
869 """Add info_data['joystick.axis_count'] if required
870 """
871 if 'axes' in info_data.get('joystick', {}):
872 axes_keys = info_data['joystick']['axes'].keys()
873 info_data['joystick']['axis_count'] = max(JOYSTICK_AXES.index(a) for a in axes_keys) + 1 if axes_keys else 0
874
875 return info_data
876
877
878def _matrix_masked(info_data):
879 """"Add info_data['matrix_pins.masked'] if required"""
880 mask_required = False
881
882 if 'matrix_grid' in info_data.get('dip_switch', {}):
883 mask_required = True
884 if 'matrix_grid' in info_data.get('split', {}).get('handedness', {}):
885 mask_required = True
886
887 if mask_required:
888 if 'masked' not in info_data.get('matrix_pins', {}):
889 if 'matrix_pins' not in info_data:
890 info_data['matrix_pins'] = {}
891
892 info_data['matrix_pins']['masked'] = True
893
894 return info_data
895
896
897def _check_matrix(info_data):
898 """Check the matrix to ensure that row/column count is consistent.
899 """
900 if 'matrix_pins' in info_data and 'matrix_size' in info_data:
901 actual_col_count = info_data['matrix_size'].get('cols', 0)
902 actual_row_count = info_data['matrix_size'].get('rows', 0)
903 col_count = row_count = 0
904
905 if 'direct' in info_data['matrix_pins']:
906 col_count = len(info_data['matrix_pins']['direct'][0])
907 row_count = len(info_data['matrix_pins']['direct'])
908 elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
909 col_count = len(info_data['matrix_pins']['cols'])
910 row_count = len(info_data['matrix_pins']['rows'])
911 elif 'cols' not in info_data['matrix_pins'] and 'rows' not in info_data['matrix_pins']:
912 # This case caters for custom matrix implementations where normal rows/cols are specified
913 return
914
915 if col_count != actual_col_count and col_count != (actual_col_count / 2):
916 # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
917 _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
918
919 if row_count != actual_row_count and row_count != (actual_row_count / 2):
920 # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
921 _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
922
923
924def _search_keyboard_h(keyboard):
925 keyboard = Path(keyboard)
926 current_path = Path('keyboards/')
927 aliases = {}
928 layouts = {}
929
930 for directory in keyboard.parts:
931 current_path = current_path / directory
932 keyboard_h = '%s.h' % (directory,)
933 keyboard_h_path = current_path / keyboard_h
934 if keyboard_h_path.exists():
935 new_layouts, new_aliases = find_layouts(keyboard_h_path)
936 layouts.update(new_layouts)
937
938 for alias, alias_text in new_aliases.items():
939 if alias_text in layouts:
940 aliases[alias] = alias_text
941
942 return layouts, aliases
943
944
945def _log_error(info_data, message):
946 """Send an error message to both JSON and the log.
947 """
948 info_data['parse_errors'].append(message)
949 cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
950
951
952def _log_warning(info_data, message):
953 """Send a warning message to both JSON and the log.
954 """
955 info_data['parse_warnings'].append(message)
956 cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
957
958
959def arm_processor_rules(info_data, rules):
960 """Setup the default info for an ARM board.
961 """
962 info_data['processor_type'] = 'arm'
963 info_data['protocol'] = 'ChibiOS'
964 info_data['platform_key'] = 'chibios'
965
966 if 'STM32' in info_data['processor']:
967 info_data['platform'] = 'STM32'
968 elif 'MCU_SERIES' in rules:
969 info_data['platform'] = rules['MCU_SERIES']
970
971 return info_data
972
973
974def avr_processor_rules(info_data, rules):
975 """Setup the default info for an AVR board.
976 """
977 info_data['processor_type'] = 'avr'
978 info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
979 info_data['platform_key'] = 'avr'
980 info_data['protocol'] = 'V-USB' if info_data['processor'] in VUSB_PROCESSORS else 'LUFA'
981
982 # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
983 # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
984
985 return info_data
986
987
988def unknown_processor_rules(info_data, rules):
989 """Setup the default keyboard info for unknown boards.
990 """
991 info_data['bootloader'] = 'unknown'
992 info_data['platform'] = 'unknown'
993 info_data['processor'] = 'unknown'
994 info_data['processor_type'] = 'unknown'
995 info_data['protocol'] = 'unknown'
996
997 return info_data
998
999
1000def merge_info_jsons(keyboard, info_data):
1001 """Return a merged copy of all the info.json files for a keyboard.
1002 """
1003 config_files = find_info_json(keyboard)
1004
1005 for info_file in config_files:
1006 # Load and validate the JSON data
1007 new_info_data = json_load(info_file)
1008
1009 if not isinstance(new_info_data, dict):
1010 _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
1011 continue
1012
1013 if not truthy(os.environ.get('SKIP_SCHEMA_VALIDATION'), False):
1014 try:
1015 validate(new_info_data, 'qmk.keyboard.v1')
1016 except jsonschema.ValidationError as e:
1017 json_path = '.'.join([str(p) for p in e.absolute_path])
1018 cli.log.error('Not including data from file: %s', info_file)
1019 cli.log.error('\t%s: %s', json_path, e.message)
1020 continue
1021
1022 # Merge layout data in
1023 if 'layout_aliases' in new_info_data:
1024 info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']}
1025 del new_info_data['layout_aliases']
1026
1027 for layout_name, layout in new_info_data.get('layouts', {}).items():
1028 if layout_name in info_data.get('layout_aliases', {}):
1029 _log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}")
1030 layout_name = info_data['layout_aliases'][layout_name]
1031
1032 if layout_name in info_data['layouts']:
1033 if len(info_data['layouts'][layout_name]['layout']) != len(layout['layout']):
1034 msg = 'Number of keys for %s does not match! info.json specifies %d keys, C macro specifies %d'
1035 _log_error(info_data, msg % (layout_name, len(layout['layout']), len(info_data['layouts'][layout_name]['layout'])))
1036 else:
1037 info_data['layouts'][layout_name]['json_layout'] = True
1038 for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
1039 existing_key.update(new_key)
1040 else:
1041 if not all('matrix' in key_data.keys() for key_data in layout['layout']):
1042 _log_error(info_data, f'Layout "{layout_name}" has no "matrix" definition in either "info.json" or "<keyboard>.h"!')
1043 else:
1044 layout['c_macro'] = False
1045 layout['json_layout'] = True
1046 info_data['layouts'][layout_name] = layout
1047
1048 # Update info_data with the new data
1049 if 'layouts' in new_info_data:
1050 del new_info_data['layouts']
1051
1052 deep_update(info_data, new_info_data)
1053
1054 return info_data
1055
1056
1057def find_info_json(keyboard):
1058 """Finds all the info.json files associated with a keyboard.
1059 """
1060 # Find the most specific first
1061 base_path = Path('keyboards')
1062 keyboard_path = base_path / keyboard
1063 keyboard_parent = keyboard_path.parent
1064 info_jsons = [keyboard_path / 'info.json', keyboard_path / 'keyboard.json']
1065
1066 # Add in parent folders for least specific
1067 for _ in range(5):
1068 if keyboard_parent == base_path:
1069 break
1070 info_jsons.append(keyboard_parent / 'info.json')
1071 info_jsons.append(keyboard_parent / 'keyboard.json')
1072 keyboard_parent = keyboard_parent.parent
1073
1074 # Return a list of the info.json files that actually exist
1075 return [info_json for info_json in info_jsons if info_json.exists()]
1076
1077
1078def keymap_json_config(keyboard, keymap, force_layout=None):
1079 """Extract keymap level config
1080 """
1081 # TODO: resolve keymap.py and info.py circular dependencies
1082 from qmk.keymap import locate_keymap
1083
1084 keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent
1085
1086 km_info_json = parse_configurator_json(keymap_folder / 'keymap.json')
1087 return km_info_json.get('config', {})
1088
1089
1090def keymap_json(keyboard, keymap, force_layout=None):
1091 """Generate the info.json data for a specific keymap.
1092 """
1093 # TODO: resolve keymap.py and info.py circular dependencies
1094 from qmk.keymap import locate_keymap
1095
1096 keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent
1097
1098 # Files to scan
1099 keymap_config = keymap_folder / 'config.h'
1100 keymap_rules = keymap_folder / 'rules.mk'
1101 keymap_file = keymap_folder / 'keymap.json'
1102
1103 # Build the info.json file
1104 kb_info_json = info_json(keyboard, force_layout=force_layout)
1105
1106 # Merge in the data from keymap.json
1107 km_info_json = keymap_json_config(keyboard, keymap, force_layout=force_layout) if keymap_file.exists() else {}
1108 deep_update(kb_info_json, km_info_json)
1109
1110 # Merge in the data from config.h, and rules.mk
1111 _extract_rules_mk(kb_info_json, parse_rules_mk_file(keymap_rules))
1112 _extract_config_h(kb_info_json, parse_config_h_file(keymap_config))
1113
1114 return kb_info_json
1115
1116
1117def get_modules(keyboard, keymap_filename):
1118 """Get the modules for a keyboard/keymap.
1119 """
1120 modules = []
1121
1122 kb_info_json = info_json(keyboard)
1123 modules.extend(kb_info_json.get('modules', []))
1124
1125 if keymap_filename:
1126 keymap_json = parse_configurator_json(keymap_filename)
1127
1128 if keymap_json:
1129 modules.extend(keymap_json.get('modules', []))
1130
1131 return list(dict.fromkeys(modules)) # remove dupes