You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

495 lines
17 KiB

  1. """Functions that help us generate and use info.json files.
  2. """
  3. from functools import lru_cache
  4. from glob import glob
  5. from pathlib import Path
  6. import jsonschema
  7. from dotty_dict import dotty
  8. from milc import cli
  9. from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
  10. from qmk.c_parse import find_layouts
  11. from qmk.json_schema import deep_update, json_load, keyboard_validate
  12. from qmk.keyboard import config_h, rules_mk
  13. from qmk.makefile import parse_rules_mk_file
  14. from qmk.math import compute
  15. true_values = ['1', 'on', 'yes', 'true']
  16. false_values = ['0', 'off', 'no', 'false']
  17. @lru_cache(maxsize=None)
  18. def basic_info_json(keyboard):
  19. """Generate a subset of info.json for a specific keyboard.
  20. This does no validation, and should only be used as needed to avoid loops or when performance is critical.
  21. """
  22. cur_dir = Path('keyboards')
  23. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  24. if 'DEFAULT_FOLDER' in rules:
  25. keyboard = rules['DEFAULT_FOLDER']
  26. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk', rules)
  27. info_data = {
  28. 'keyboard_name': str(keyboard),
  29. 'keyboard_folder': str(keyboard),
  30. 'keymaps': {},
  31. 'layouts': {},
  32. 'parse_errors': [],
  33. 'parse_warnings': [],
  34. 'maintainer': 'qmk',
  35. }
  36. # Populate layout data
  37. layouts, aliases = _find_all_layouts(info_data, keyboard)
  38. if aliases:
  39. info_data['layout_aliases'] = aliases
  40. for layout_name, layout_json in layouts.items():
  41. if not layout_name.startswith('LAYOUT_kc'):
  42. layout_json['c_macro'] = True
  43. info_data['layouts'][layout_name] = layout_json
  44. # Merge in the data from info.json, config.h, and rules.mk
  45. info_data = merge_info_jsons(keyboard, info_data)
  46. info_data = _extract_config_h(info_data)
  47. info_data = _extract_rules_mk(info_data)
  48. return info_data
  49. def _extract_features(info_data, rules):
  50. """Find all the features enabled in rules.mk.
  51. """
  52. # Special handling for bootmagic which also supports a "lite" mode.
  53. if rules.get('BOOTMAGIC_ENABLE') == 'lite':
  54. rules['BOOTMAGIC_LITE_ENABLE'] = 'on'
  55. del rules['BOOTMAGIC_ENABLE']
  56. if rules.get('BOOTMAGIC_ENABLE') == 'full':
  57. rules['BOOTMAGIC_ENABLE'] = 'on'
  58. # Skip non-boolean features we haven't implemented special handling for
  59. for feature in 'HAPTIC_ENABLE', 'QWIIC_ENABLE':
  60. if rules.get(feature):
  61. del rules[feature]
  62. # Process the rest of the rules as booleans
  63. for key, value in rules.items():
  64. if key.endswith('_ENABLE'):
  65. key = '_'.join(key.split('_')[:-1]).lower()
  66. value = True if value.lower() in true_values else False if value.lower() in false_values else value
  67. if 'config_h_features' not in info_data:
  68. info_data['config_h_features'] = {}
  69. if 'features' not in info_data:
  70. info_data['features'] = {}
  71. if key in info_data['features']:
  72. info_log_warning(info_data, 'Feature %s is specified in both info.json and rules.mk, the rules.mk value wins.' % (key,))
  73. info_data['features'][key] = value
  74. info_data['config_h_features'][key] = value
  75. return info_data
  76. def _pin_name(pin):
  77. """Returns the proper representation for a pin.
  78. """
  79. pin = pin.strip()
  80. if not pin:
  81. return None
  82. elif pin.isdigit():
  83. return int(pin)
  84. elif pin == 'NO_PIN':
  85. return None
  86. elif pin[0] in 'ABCDEFGHIJK' and pin[1].isdigit():
  87. return pin
  88. raise ValueError(f'Invalid pin: {pin}')
  89. def _extract_pins(pins):
  90. """Returns a list of pins from a comma separated string of pins.
  91. """
  92. return [_pin_name(pin) for pin in pins.split(',')]
  93. def _extract_direct_matrix(info_data, direct_pins):
  94. """
  95. """
  96. info_data['matrix_pins'] = {}
  97. direct_pin_array = []
  98. while direct_pins[-1] != '}':
  99. direct_pins = direct_pins[:-1]
  100. for row in direct_pins.split('},{'):
  101. if row.startswith('{'):
  102. row = row[1:]
  103. if row.endswith('}'):
  104. row = row[:-1]
  105. direct_pin_array.append([])
  106. for pin in row.split(','):
  107. if pin == 'NO_PIN':
  108. pin = None
  109. direct_pin_array[-1].append(pin)
  110. return direct_pin_array
  111. def _extract_matrix_info(info_data, config_c):
  112. """Populate the matrix information.
  113. """
  114. row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
  115. col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
  116. direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
  117. if 'MATRIX_ROWS' in config_c and 'MATRIX_COLS' in config_c:
  118. if 'matrix_size' in info_data:
  119. info_log_warning(info_data, 'Matrix size is specified in both info.json and config.h, the config.h values win.')
  120. info_data['matrix_size'] = {
  121. 'cols': compute(config_c.get('MATRIX_COLS', '0')),
  122. 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
  123. }
  124. if row_pins and col_pins:
  125. if 'matrix_pins' in info_data:
  126. info_log_warning(info_data, 'Matrix pins are specified in both info.json and config.h, the config.h values win.')
  127. info_data['matrix_pins'] = {
  128. 'cols': _extract_pins(col_pins),
  129. 'rows': _extract_pins(row_pins),
  130. }
  131. if direct_pins:
  132. if 'matrix_pins' in info_data:
  133. info_log_warning(info_data, 'Direct pins are specified in both info.json and config.h, the config.h values win.')
  134. info_data['matrix_pins']['direct'] = _extract_direct_matrix(info_data, direct_pins)
  135. return info_data
  136. def _extract_config_h(info_data):
  137. """Pull some keyboard information from existing config.h files
  138. """
  139. config_c = config_h(info_data['keyboard_folder'])
  140. # Pull in data from the json map
  141. dotty_info = dotty(info_data)
  142. info_config_map = json_load(Path('data/mappings/info_config.json'))
  143. for config_key, info_dict in info_config_map.items():
  144. info_key = info_dict['info_key']
  145. key_type = info_dict.get('value_type', 'str')
  146. try:
  147. if config_key in config_c and info_dict.get('to_json', True):
  148. if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
  149. info_log_warning(info_data, '%s in config.h is overwriting %s in info.json' % (config_key, info_key))
  150. if key_type.startswith('array'):
  151. if '.' in key_type:
  152. key_type, array_type = key_type.split('.', 1)
  153. else:
  154. array_type = None
  155. config_value = config_c[config_key].replace('{', '').replace('}', '').strip()
  156. if array_type == 'int':
  157. dotty_info[info_key] = list(map(int, config_value.split(',')))
  158. else:
  159. dotty_info[info_key] = config_value.split(',')
  160. elif key_type == 'bool':
  161. dotty_info[info_key] = config_c[config_key] in true_values
  162. elif key_type == 'hex':
  163. dotty_info[info_key] = '0x' + config_c[config_key][2:].upper()
  164. elif key_type == 'list':
  165. dotty_info[info_key] = config_c[config_key].split()
  166. elif key_type == 'int':
  167. dotty_info[info_key] = int(config_c[config_key])
  168. else:
  169. dotty_info[info_key] = config_c[config_key]
  170. except Exception as e:
  171. info_log_warning(info_data, f'{config_key}->{info_key}: {e}')
  172. info_data.update(dotty_info)
  173. # Pull data that easily can't be mapped in json
  174. _extract_matrix_info(info_data, config_c)
  175. return info_data
  176. def _extract_rules_mk(info_data):
  177. """Pull some keyboard information from existing rules.mk files
  178. """
  179. rules = rules_mk(info_data['keyboard_folder'])
  180. info_data['processor'] = rules.get('MCU', info_data.get('processor', 'atmega32u4'))
  181. if info_data['processor'] in CHIBIOS_PROCESSORS:
  182. arm_processor_rules(info_data, rules)
  183. elif info_data['processor'] in LUFA_PROCESSORS + VUSB_PROCESSORS:
  184. avr_processor_rules(info_data, rules)
  185. else:
  186. cli.log.warning("%s: Unknown MCU: %s" % (info_data['keyboard_folder'], info_data['processor']))
  187. unknown_processor_rules(info_data, rules)
  188. # Pull in data from the json map
  189. dotty_info = dotty(info_data)
  190. info_rules_map = json_load(Path('data/mappings/info_rules.json'))
  191. for rules_key, info_dict in info_rules_map.items():
  192. info_key = info_dict['info_key']
  193. key_type = info_dict.get('value_type', 'str')
  194. try:
  195. if rules_key in rules and info_dict.get('to_json', True):
  196. if dotty_info.get(info_key) and info_dict.get('warn_duplicate', True):
  197. info_log_warning(info_data, '%s in rules.mk is overwriting %s in info.json' % (rules_key, info_key))
  198. if key_type.startswith('array'):
  199. if '.' in key_type:
  200. key_type, array_type = key_type.split('.', 1)
  201. else:
  202. array_type = None
  203. rules_value = rules[rules_key].replace('{', '').replace('}', '').strip()
  204. if array_type == 'int':
  205. dotty_info[info_key] = list(map(int, rules_value.split(',')))
  206. else:
  207. dotty_info[info_key] = rules_value.split(',')
  208. elif key_type == 'list':
  209. dotty_info[info_key] = rules[rules_key].split()
  210. elif key_type == 'bool':
  211. dotty_info[info_key] = rules[rules_key] in true_values
  212. elif key_type == 'hex':
  213. dotty_info[info_key] = '0x' + rules[rules_key][2:].upper()
  214. elif key_type == 'int':
  215. dotty_info[info_key] = int(rules[rules_key])
  216. else:
  217. dotty_info[info_key] = rules[rules_key]
  218. except Exception as e:
  219. info_log_warning(info_data, f'{rules_key}->{info_key}: {e}')
  220. info_data.update(dotty_info)
  221. # Merge in config values that can't be easily mapped
  222. _extract_features(info_data, rules)
  223. return info_data
  224. def _search_keyboard_h(path):
  225. current_path = Path('keyboards/')
  226. aliases = {}
  227. layouts = {}
  228. for directory in path.parts:
  229. current_path = current_path / directory
  230. keyboard_h = '%s.h' % (directory,)
  231. keyboard_h_path = current_path / keyboard_h
  232. if keyboard_h_path.exists():
  233. new_layouts, new_aliases = find_layouts(keyboard_h_path)
  234. layouts.update(new_layouts)
  235. for alias, alias_text in new_aliases.items():
  236. if alias_text in layouts:
  237. aliases[alias] = alias_text
  238. return layouts, aliases
  239. def _find_all_layouts(info_data, keyboard):
  240. """Looks for layout macros associated with this keyboard.
  241. """
  242. found_layouts = False
  243. layouts, aliases = _search_keyboard_h(Path(keyboard))
  244. if layouts:
  245. found_layouts = True
  246. else:
  247. for layout in info_data['layouts'].values():
  248. if 'matrix' in layout['layout']:
  249. found_layouts = True
  250. break
  251. if not found_layouts:
  252. # 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.
  253. info_data['parse_warnings'].append('%s: Falling back to searching for KEYMAP/LAYOUT macros.' % (keyboard))
  254. for file in glob('keyboards/%s/*.h' % keyboard):
  255. if file.endswith('.h'):
  256. these_layouts, these_aliases = find_layouts(file)
  257. if these_layouts:
  258. layouts.update(these_layouts)
  259. for alias, alias_text in these_aliases.items():
  260. if alias_text in layouts:
  261. aliases[alias] = alias_text
  262. return layouts, aliases
  263. def info_log_error(info_data, message):
  264. """Send an error message to both JSON and the log.
  265. """
  266. info_data['parse_errors'].append(message)
  267. cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  268. def info_log_warning(info_data, message):
  269. """Send a warning message to both JSON and the log.
  270. """
  271. info_data['parse_warnings'].append(message)
  272. cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  273. def arm_processor_rules(info_data, rules):
  274. """Setup the default info for an ARM board.
  275. """
  276. info_data['processor_type'] = 'arm'
  277. info_data['protocol'] = 'ChibiOS'
  278. if 'bootloader' not in info_data:
  279. if 'STM32' in info_data['processor']:
  280. info_data['bootloader'] = 'stm32-dfu'
  281. else:
  282. info_data['bootloader'] = 'unknown'
  283. if 'STM32' in info_data['processor']:
  284. info_data['platform'] = 'STM32'
  285. elif 'MCU_SERIES' in rules:
  286. info_data['platform'] = rules['MCU_SERIES']
  287. elif 'ARM_ATSAM' in rules:
  288. info_data['platform'] = 'ARM_ATSAM'
  289. return info_data
  290. def avr_processor_rules(info_data, rules):
  291. """Setup the default info for an AVR board.
  292. """
  293. info_data['processor_type'] = 'avr'
  294. info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
  295. info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
  296. if 'bootloader' not in info_data:
  297. info_data['bootloader'] = 'atmel-dfu'
  298. # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
  299. # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
  300. return info_data
  301. def unknown_processor_rules(info_data, rules):
  302. """Setup the default keyboard info for unknown boards.
  303. """
  304. info_data['bootloader'] = 'unknown'
  305. info_data['platform'] = 'unknown'
  306. info_data['processor'] = 'unknown'
  307. info_data['processor_type'] = 'unknown'
  308. info_data['protocol'] = 'unknown'
  309. return info_data
  310. def merge_info_jsons(keyboard, info_data):
  311. """Return a merged copy of all the info.json files for a keyboard.
  312. """
  313. for info_file in find_info_json(keyboard):
  314. # Load and validate the JSON data
  315. new_info_data = json_load(info_file)
  316. if not isinstance(new_info_data, dict):
  317. info_log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
  318. continue
  319. try:
  320. keyboard_validate(new_info_data)
  321. except jsonschema.ValidationError as e:
  322. json_path = '.'.join([str(p) for p in e.absolute_path])
  323. cli.log.error('Not including data from file: %s', info_file)
  324. cli.log.error('\t%s: %s', json_path, e.message)
  325. continue
  326. # Merge layout data in
  327. if 'layout_aliases' in new_info_data:
  328. info_data['layout_aliases'] = {**info_data.get('layout_aliases', {}), **new_info_data['layout_aliases']}
  329. del new_info_data['layout_aliases']
  330. for layout_name, layout in new_info_data.get('layouts', {}).items():
  331. if layout_name in info_data.get('layout_aliases', {}):
  332. info_log_warning(info_data, f"info.json uses alias name {layout_name} instead of {info_data['layout_aliases'][layout_name]}")
  333. layout_name = info_data['layout_aliases'][layout_name]
  334. if layout_name in info_data['layouts']:
  335. for new_key, existing_key in zip(layout['layout'], info_data['layouts'][layout_name]['layout']):
  336. existing_key.update(new_key)
  337. else:
  338. layout['c_macro'] = False
  339. info_data['layouts'][layout_name] = layout
  340. # Update info_data with the new data
  341. if 'layouts' in new_info_data:
  342. del new_info_data['layouts']
  343. deep_update(info_data, new_info_data)
  344. return info_data
  345. def find_info_json(keyboard):
  346. """Finds all the info.json files associated with a keyboard.
  347. """
  348. # Find the most specific first
  349. base_path = Path('keyboards')
  350. keyboard_path = base_path / keyboard
  351. keyboard_parent = keyboard_path.parent
  352. info_jsons = [keyboard_path / 'info.json']
  353. # Add DEFAULT_FOLDER before parents, if present
  354. rules = rules_mk(keyboard)
  355. if 'DEFAULT_FOLDER' in rules:
  356. info_jsons.append(Path(rules['DEFAULT_FOLDER']) / 'info.json')
  357. # Add in parent folders for least specific
  358. for _ in range(5):
  359. info_jsons.append(keyboard_parent / 'info.json')
  360. if keyboard_parent.parent == base_path:
  361. break
  362. keyboard_parent = keyboard_parent.parent
  363. # Return a list of the info.json files that actually exist
  364. return [info_json for info_json in info_jsons if info_json.exists()]