logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

info.py (41550B)


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