at master 44 kB view raw
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