info.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. """Functions that help us generate and use info.json files.
  2. """
  3. from glob import glob
  4. from pathlib import Path
  5. import jsonschema
  6. from dotty_dict import dotty
  7. from milc import cli
  8. from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
  9. from qmk.c_parse import find_layouts
  10. from qmk.json_schema import deep_update, json_load, validate
  11. from qmk.keyboard import config_h, rules_mk
  12. from qmk.keymap import list_keymaps
  13. from qmk.makefile import parse_rules_mk_file
  14. from qmk.math import compute
  15. true_values = ['1', 'on', 'yes']
  16. false_values = ['0', 'off', 'no']
  17. def _valid_community_layout(layout):
  18. """Validate that a declared community list exists
  19. """
  20. return (Path('layouts/default') / layout).exists()
  21. def info_json(keyboard):
  22. """Generate the info.json data for a specific keyboard.
  23. """
  24. cur_dir = Path('keyboards')
  25. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  26. if 'DEFAULT_FOLDER' in rules:
  27. keyboard = rules['DEFAULT_FOLDER']
  28. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk', rules)
  29. info_data = {
  30. 'keyboard_name': str(keyboard),
  31. 'keyboard_folder': str(keyboard),
  32. 'keymaps': {},
  33. 'layouts': {},
  34. 'parse_errors': [],
  35. 'parse_warnings': [],
  36. 'maintainer': 'qmk',
  37. }
  38. # Populate the list of JSON keymaps
  39. for keymap in list_keymaps(keyboard, c=False, fullpath=True):
  40. info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
  41. # Populate layout data
  42. layouts, aliases = _find_all_layouts(info_data, keyboard)
  43. if aliases:
  44. info_data['layout_aliases'] = aliases
  45. for layout_name, layout_json in layouts.items():
  46. if not layout_name.startswith('LAYOUT_kc'):
  47. layout_json['c_macro'] = True
  48. info_data['layouts'][layout_name] = layout_json
  49. # Merge in the data from info.json, config.h, and rules.mk
  50. info_data = merge_info_jsons(keyboard, info_data)
  51. info_data = _extract_config_h(info_data)
  52. info_data = _extract_rules_mk(info_data)
  53. # Ensure that we have matrix row and column counts
  54. info_data = _matrix_size(info_data)
  55. # Validate against the jsonschema
  56. try:
  57. validate(info_data, 'qmk.api.keyboard.v1')
  58. except jsonschema.ValidationError as e:
  59. json_path = '.'.join([str(p) for p in e.absolute_path])
  60. cli.log.error('Invalid API data: %s: %s: %s', keyboard, json_path, e.message)
  61. exit(1)
  62. # Make sure we have at least one layout
  63. if not info_data.get('layouts'):
  64. _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
  65. # Filter out any non-existing community layouts
  66. for layout in info_data.get('community_layouts', []):
  67. if not _valid_community_layout(layout):
  68. # Ignore layout from future checks
  69. info_data['community_layouts'].remove(layout)
  70. _log_error(info_data, 'Claims to support a community layout that does not exist: %s' % (layout))
  71. # Make sure we supply layout macros for the community layouts we claim to support
  72. for layout in info_data.get('community_layouts', []):
  73. layout_name = 'LAYOUT_' + layout
  74. if layout_name not in info_data.get('layouts', {}) and layout_name not in info_data.get('layout_aliases', {}):
  75. _log_error(info_data, 'Claims to support community layout %s but no %s() macro found' % (layout, layout_name))
  76. # Check that the reported matrix size is consistent with the actual matrix size
  77. _check_matrix(info_data)
  78. return info_data
  79. def _extract_features(info_data, rules):
  80. """Find all the features enabled in rules.mk.
  81. """
  82. # Special handling for bootmagic which also supports a "lite" mode.
  83. if rules.get('BOOTMAGIC_ENABLE') == 'lite':
  84. rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
  85. del rules['BOOTMAGIC_ENABLE']
  86. if rules.get('BOOTMAGIC_ENABLE') == 'full':
  87. rules['BOOTMAGIC_ENABLE'] = 'on'
  88. # Skip non-boolean features we haven't implemented special handling for
  89. for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
  90. if rules.get(feature):
  91. del rules[feature]
  92. # Process the rest of the rules as booleans
  93. for key, value in rules.items():
  94. if key.endswith('_ENABLE'):
  95. key = '_'.join(key.split('_')[:-1]).lower()
  96. value = True if value.lower() in true_values else False if value.lower() in false_values else value
  97. if 'config_h_features' not in info_data:
  98. info_data['config_h_features'] = {}
  99. if 'features' not in info_data:
  100. info_data['features'] = {}
  101. if key in info_data['features']:
  102. _log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
  103. info_data['features'][key] = value
  104. info_data['config_h_features'][key] = value
  105. return info_data
  106. def _pin_name(pin):
  107. """Returns the proper representation for a pin.
  108. """
  109. pin = pin.strip()
  110. if not pin:
  111. return None
  112. elif pin.isdigit():
  113. return int(pin)
  114. elif pin == 'NO_PIN':
  115. return None
  116. return pin
  117. def _extract_pins(pins):
  118. """Returns a list of pins from a comma separated string of pins.
  119. """
  120. return [_pin_name(pin) for pin in pins.split(',')]
  121. def _extract_direct_matrix(info_data, direct_pins):
  122. """
  123. """
  124. info_data['matrix_pins'] = {}
  125. direct_pin_array = []
  126. while direct_pins[-1] != '}':
  127. direct_pins = direct_pins[:-1]
  128. for row in direct_pins.split('},{'):
  129. if row.startswith('{'):
  130. row = row[1:]
  131. if row.endswith('}'):
  132. row = row[:-1]
  133. direct_pin_array.append([])
  134. for pin in row.split(','):
  135. if pin == 'NO_PIN':
  136. pin = None
  137. direct_pin_array[-1].append(pin)
  138. return direct_pin_array
  139. def _extract_matrix_info(info_data, config_c):
  140. """Populate the matrix information.
  141. """
  142. row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
  143. col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
  144. direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
  145. if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
  146. if 'matrix_size' in info_data:
  147. _log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
  148. info_data['matrix_size'] = {
  149. 'cols': compute(config_c.get('MATRIX_COLS', '0')),
  150. 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
  151. }
  152. if row_pins and col_pins:
  153. if 'matrix_pins' in info_data:
  154. _log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
  155. info_data['matrix_pins'] = {
  156. 'cols': _extract_pins(col_pins),
  157. 'rows': _extract_pins(row_pins),
  158. }
  159. if direct_pins:
  160. if 'matrix_pins' in info_data:
  161. _log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
  162. info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
  163. return info_data
  164. def _extract_config_h(info_data):
  165. """Pull some keyboard information from existing config.h files
  166. """
  167. config_c = config_h(info_data['keyboard_folder'])
  168. # Pull in data from the json map
  169. dotty_info = dotty(info_data)
  170. info_config_map = json_load(Path('data/mappings/info_config.json'))
  171. for config_key, info_dict in info_config_map.items():
  172. info_key = info_dict['info_key']
  173. key_type = info_dict.get('value_type', 'str')
  174. try:
  175. if config_key in config_c and info_dict.get('to_json', True):
  176. if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
  177. _log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
  178. if key_type.startswith('array'):
  179. if '.' in key_type:
  180. key_type, array_type = key_type.split('.', 1)
  181. else:
  182. array_type = None
  183. config_value = config_c[config_key].replace('{', '').replace('}', '').strip()
  184. if array_type == 'int':
  185. dotty_info[info_key] = list(map(int, config_value.split(',')))
  186. else:
  187. dotty_info[info_key] = config_value.split(',')
  188. elif key_type == 'bool':
  189. dotty_info[info_key] = config_c[config_key] in true_values
  190. elif key_type == 'hex':
  191. dotty_info[info_key] = '0x' + config_c[config_key][2:].upper()
  192. elif key_type == 'list':
  193. dotty_info[info_key] = config_c[config_key].split()
  194. elif key_type == 'int':
  195. dotty_info[info_key] = int(config_c[config_key])
  196. else:
  197. dotty_info[info_key] = config_c[config_key]
  198. except Exception as e:
  199. _log_warning(info_data, f'{config_key}->{info_key}: {e}')
  200. info_data.update(dotty_info)
  201. # Pull data that easily can't be mapped in json
  202. _extract_matrix_info(info_data, config_c)
  203. return info_data
  204. def _extract_rules_mk(info_data):
  205. """Pull some keyboard information from existing rules.mk files
  206. """
  207. rules = rules_mk(info_data['keyboard_folder'])
  208. info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
  209. if info_data['processor'] in CHIBIOS_PROCESSORS:
  210. arm_processor_rules(info_data, rules)
  211. elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
  212. avr_processor_rules(info_data, rules)
  213. else:
  214. cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
  215. unknown_processor_rules(info_data, rules)
  216. # Pull in data from the json map
  217. dotty_info = dotty(info_data)
  218. info_rules_map = json_load(Path('data/mappings/info_rules.json'))
  219. for rules_key, info_dict in info_rules_map.items():
  220. info_key = info_dict['info_key']
  221. key_type = info_dict.get('value_type', 'str')
  222. try:
  223. if rules_key in rules and info_dict.get('to_json', True):
  224. if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
  225. _log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
  226. if key_type.startswith('array'):
  227. if '.' in key_type:
  228. key_type, array_type = key_type.split('.', 1)
  229. else:
  230. array_type = None
  231. rules_value = rules[rules_key].replace('{', '').replace('}', '').strip()
  232. if array_type == 'int':
  233. dotty_info[info_key] = list(map(int, rules_value.split(',')))
  234. else:
  235. dotty_info[info_key] = rules_value.split(',')
  236. elif key_type == 'list':
  237. dotty_info[info_key] = rules[rules_key].split()
  238. elif key_type == 'bool':
  239. dotty_info[info_key] = rules[rules_key] in true_values
  240. elif key_type == 'hex':
  241. dotty_info[info_key] = '0x' + rules[rules_key][2:].upper()
  242. elif key_type == 'int':
  243. dotty_info[info_key] = int(rules[rules_key])
  244. else:
  245. dotty_info[info_key] = rules[rules_key]
  246. except Exception as e:
  247. _log_warning(info_data, f'{rules_key}->{info_key}: {e}')
  248. info_data.update(dotty_info)
  249. # Merge in config values that can't be easily mapped
  250. _extract_features(info_data, rules)
  251. return info_data
  252. def _matrix_size(info_data):
  253. """Add info_data['matrix_size'] if it doesn't exist.
  254. """
  255. if 'matrix_size' not in info_data and 'matrix_pins' in info_data:
  256. info_data['matrix_size'] = {}
  257. if 'direct' in info_data['matrix_pins']:
  258. info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['direct'][0])
  259. info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['direct'])
  260. elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
  261. info_data['matrix_size']['cols'] = len(info_data['matrix_pins']['cols'])
  262. info_data['matrix_size']['rows'] = len(info_data['matrix_pins']['rows'])
  263. return info_data
  264. def _check_matrix(info_data):
  265. """Check the matrix to ensure that row/column count is consistent.
  266. """
  267. if 'matrix_pins' in info_data and 'matrix_size' in info_data:
  268. actual_col_count = info_data['matrix_size'].get('cols', 0)
  269. actual_row_count = info_data['matrix_size'].get('rows', 0)
  270. col_count = row_count = 0
  271. if 'direct' in info_data['matrix_pins']:
  272. col_count = len(info_data['matrix_pins']['direct'][0])
  273. row_count = len(info_data['matrix_pins']['direct'])
  274. elif 'cols' in info_data['matrix_pins'] and 'rows' in info_data['matrix_pins']:
  275. col_count = len(info_data['matrix_pins']['cols'])
  276. row_count = len(info_data['matrix_pins']['rows'])
  277. if col_count != actual_col_count and col_count != (actual_col_count / 2):
  278. # FIXME: once we can we should detect if split is enabled to do the actual_col_count/2 check.
  279. _log_error(info_data, f'MATRIX_COLS is inconsistent with the size of MATRIX_COL_PINS: {col_count} != {actual_col_count}')
  280. if row_count != actual_row_count and row_count != (actual_row_count / 2):
  281. # FIXME: once we can we should detect if split is enabled to do the actual_row_count/2 check.
  282. _log_error(info_data, f'MATRIX_ROWS is inconsistent with the size of MATRIX_ROW_PINS: {row_count} != {actual_row_count}')
  283. def _merge_layouts(info_data, new_info_data):
  284. """Merge new_info_data into info_data in an intelligent way.
  285. """
  286. for layout_name, layout_json in new_info_data['layouts'].items():
  287. if layout_name in info_data['layouts']:
  288. # Pull in layouts we have a macro for
  289. if len(info_data['layouts'][layout_name]['layout']) != len(layout_json['layout']):
  290. msg = '%s: %s: Number of elements in info.json does not match! info.json:%s != %s:%s'
  291. _log_error(info_data, msg % (info_data['keyboard_folder'], layout_name, len(layout_json['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
  292. else:
  293. for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
  294. key.update(layout_json['layout'][i])
  295. else:
  296. # Pull in layouts that have matrix data
  297. missing_matrix = False
  298. for key in layout_json.get('layout', {}):
  299. if 'matrix' not in key:
  300. missing_matrix = True
  301. if not missing_matrix:
  302. if layout_name in info_data['layouts']:
  303. # Update an existing layout with new data
  304. for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
  305. key.update(layout_json['layout'][i])
  306. else:
  307. # Copy in the new layout wholesale
  308. layout_json['c_macro'] = False
  309. info_data['layouts'][layout_name] = layout_json
  310. return info_data
  311. def _search_keyboard_h(path):
  312. current_path = Path('keyboards/')
  313. aliases = {}
  314. layouts = {}
  315. for directory in path.parts:
  316. current_path = current_path / directory
  317. keyboard_h = '%s.h' % (directory,)
  318. keyboard_h_path = current_path / keyboard_h
  319. if keyboard_h_path.exists():
  320. new_layouts, new_aliases = find_layouts(keyboard_h_path)
  321. layouts.update(new_layouts)
  322. for alias, alias_text in new_aliases.items():
  323. if alias_text in layouts:
  324. aliases[alias] = alias_text
  325. return layouts, aliases
  326. def _find_all_layouts(info_data, keyboard):
  327. """Looks for layout macros associated with this keyboard.
  328. """
  329. layouts, aliases = _search_keyboard_h(Path(keyboard))
  330. if not layouts:
  331. # If we don't find any layouts from info.json or keyboard.h we widen our search. This is error prone which is why we want to encourage people to follow the standard above.
  332. info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
  333. for file in glob('keyboards/%s/*.h' % keyboard):
  334. if file.endswith('.h'):
  335. these_layouts, these_aliases = find_layouts(file)
  336. if these_layouts:
  337. layouts.update(these_layouts)
  338. for alias, alias_text in these_aliases.items():
  339. if alias_text in layouts:
  340. aliases[alias] = alias_text
  341. return layouts, aliases
  342. def _log_error(info_data, message):
  343. """Send an error message to both JSON and the log.
  344. """
  345. info_data['parse_errors'].append(message)
  346. cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  347. def _log_warning(info_data, message):
  348. """Send a warning message to both JSON and the log.
  349. """
  350. info_data['parse_warnings'].append(message)
  351. cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  352. def arm_processor_rules(info_data, rules):
  353. """Setup the default info for an ARM board.
  354. """
  355. info_data['processor_type'] = 'arm'
  356. info_data['protocol'] = 'ChibiOS'
  357. if 'bootloader' not in info_data:
  358. if 'STM32' in info_data['processor']:
  359. info_data['bootloader'] = 'stm32-dfu'
  360. else:
  361. info_data['bootloader'] = 'unknown'
  362. if 'STM32' in info_data['processor']:
  363. info_data['platform'] = 'STM32'
  364. elif 'MCU_SERIES' in rules:
  365. info_data['platform'] = rules['MCU_SERIES']
  366. elif 'ARM_ATSAM' in rules:
  367. info_data['platform'] = 'ARM_ATSAM'
  368. return info_data
  369. def avr_processor_rules(info_data, rules):
  370. """Setup the default info for an AVR board.
  371. """
  372. info_data['processor_type'] = 'avr'
  373. info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
  374. info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
  375. if 'bootloader' not in info_data:
  376. info_data['bootloader'] = 'atmel-dfu'
  377. # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
  378. # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
  379. return info_data
  380. def unknown_processor_rules(info_data, rules):
  381. """Setup the default keyboard info for unknown boards.
  382. """
  383. info_data['bootloader'] = 'unknown'
  384. info_data['platform'] = 'unknown'
  385. info_data['processor'] = 'unknown'
  386. info_data['processor_type'] = 'unknown'
  387. info_data['protocol'] = 'unknown'
  388. return info_data
  389. def merge_info_jsons(keyboard, info_data):
  390. """Return a merged copy of all the info.json files for a keyboard.
  391. """
  392. for info_file in find_info_json(keyboard):
  393. # Load and validate the JSON data
  394. new_info_data = json_load(info_file)
  395. if not isinstance(new_info_data, dict):
  396. _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
  397. continue
  398. try:
  399. validate(new_info_data, 'qmk.keyboard.v1')
  400. except jsonschema.ValidationError as e:
  401. json_path = '.'.join([str(p) for p in e.absolute_path])
  402. cli.log.error('Not including data from file: %s', info_file)
  403. cli.log.error('\t%s: %s', json_path, e.message)
  404. continue
  405. # Merge layout data in
  406. if 'layout_aliases' in new_info_data:
  407. info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']}
  408. del new_info_data['layout_aliases']
  409. for layout_name, layout in new_info_data.get('layouts', {}).items():
  410. if layout_name in info_data.get('layout_aliases', {}):
  411. _log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}")
  412. layout_name = info_data['layout_aliases'][layout_name]
  413. if layout_name in info_data['layouts']:
  414. for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
  415. existing_key.update(new_key)
  416. else:
  417. layout['c_macro'] = False
  418. info_data['layouts'][layout_name] = layout
  419. # Update info_data with the new data
  420. if 'layouts' in new_info_data:
  421. del new_info_data['layouts']
  422. deep_update(info_data, new_info_data)
  423. return info_data
  424. def find_info_json(keyboard):
  425. """Finds all the info.json files associated with a keyboard.
  426. """
  427. # Find the most specific first
  428. base_path = Path('keyboards')
  429. keyboard_path = base_path / keyboard
  430. keyboard_parent = keyboard_path.parent
  431. info_jsons = [keyboard_path / 'info.json']
  432. # Add DEFAULT_FOLDER before parents, if present
  433. rules = rules_mk(keyboard)
  434. if 'DEFAULT_FOLDER' in rules:
  435. info_jsons.append(Path(rules['DEFAULT_FOLDER']) / 'info.json')
  436. # Add in parent folders for least specific
  437. for _ in range(5):
  438. info_jsons.append(keyboard_parent / 'info.json')
  439. if keyboard_parent.parent == base_path:
  440. break
  441. keyboard_parent = keyboard_parent.parent
  442. # Return a list of the info.json files that actually exist
  443. return [info_json for info_json in info_jsons if info_json.exists()]