info.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. """Functions that help us generate and use info.json files.
  2. """
  3. import json
  4. from glob import glob
  5. from pathlib import Path
  6. from milc import cli
  7. from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
  8. from qmk.c_parse import find_layouts
  9. from qmk.keyboard import config_h, rules_mk
  10. from qmk.keymap import list_keymaps
  11. from qmk.makefile import parse_rules_mk_file
  12. from qmk.math import compute
  13. def info_json(keyboard):
  14. """Generate the info.json data for a specific keyboard.
  15. """
  16. cur_dir = Path('keyboards')
  17. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk')
  18. if 'DEFAULT_FOLDER' in rules:
  19. keyboard = rules['DEFAULT_FOLDER']
  20. rules = parse_rules_mk_file(cur_dir / keyboard / 'rules.mk', rules)
  21. info_data = {
  22. 'keyboard_name': str(keyboard),
  23. 'keyboard_folder': str(keyboard),
  24. 'keymaps': {},
  25. 'layouts': {},
  26. 'parse_errors': [],
  27. 'parse_warnings': [],
  28. 'maintainer': 'qmk',
  29. }
  30. # Populate the list of JSON keymaps
  31. for keymap in list_keymaps(keyboard, c=False, fullpath=True):
  32. info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}
  33. # Populate layout data
  34. for layout_name, layout_json in _find_all_layouts(info_data, keyboard, rules).items():
  35. if not layout_name.startswith('LAYOUT_kc'):
  36. info_data['layouts'][layout_name] = layout_json
  37. # Merge in the data from info.json, config.h, and rules.mk
  38. info_data = merge_info_jsons(keyboard, info_data)
  39. info_data = _extract_config_h(info_data)
  40. info_data = _extract_rules_mk(info_data)
  41. return info_data
  42. def _extract_config_h(info_data):
  43. """Pull some keyboard information from existing rules.mk files
  44. """
  45. config_c = config_h(info_data['keyboard_folder'])
  46. row_pins = config_c.get('MATRIX_ROW_PINS', '').replace('{', '').replace('}', '').strip()
  47. col_pins = config_c.get('MATRIX_COL_PINS', '').replace('{', '').replace('}', '').strip()
  48. direct_pins = config_c.get('DIRECT_PINS', '').replace(' ', '')[1:-1]
  49. info_data['diode_direction'] = config_c.get('DIODE_DIRECTION')
  50. info_data['matrix_size'] = {
  51. 'rows': compute(config_c.get('MATRIX_ROWS', '0')),
  52. 'cols': compute(config_c.get('MATRIX_COLS', '0')),
  53. }
  54. info_data['matrix_pins'] = {}
  55. if row_pins:
  56. info_data['matrix_pins']['rows'] = row_pins.split(',')
  57. if col_pins:
  58. info_data['matrix_pins']['cols'] = col_pins.split(',')
  59. if direct_pins:
  60. direct_pin_array = []
  61. for row in direct_pins.split('},{'):
  62. if row.startswith('{'):
  63. row = row[1:]
  64. if row.endswith('}'):
  65. row = row[:-1]
  66. direct_pin_array.append([])
  67. for pin in row.split(','):
  68. if pin == 'NO_PIN':
  69. pin = None
  70. direct_pin_array[-1].append(pin)
  71. info_data['matrix_pins']['direct'] = direct_pin_array
  72. info_data['usb'] = {
  73. 'vid': config_c.get('VENDOR_ID'),
  74. 'pid': config_c.get('PRODUCT_ID'),
  75. 'device_ver': config_c.get('DEVICE_VER'),
  76. 'manufacturer': config_c.get('MANUFACTURER'),
  77. 'product': config_c.get('PRODUCT'),
  78. }
  79. return info_data
  80. def _extract_rules_mk(info_data):
  81. """Pull some keyboard information from existing rules.mk files
  82. """
  83. rules = rules_mk(info_data['keyboard_folder'])
  84. mcu = rules.get('MCU')
  85. if mcu in CHIBIOS_PROCESSORS:
  86. return arm_processor_rules(info_data, rules)
  87. elif mcu in LUFA_PROCESSORS + VUSB_PROCESSORS:
  88. return avr_processor_rules(info_data, rules)
  89. msg = "Unknown MCU: " + str(mcu)
  90. _log_warning(info_data, msg)
  91. return unknown_processor_rules(info_data, rules)
  92. def _search_keyboard_h(path):
  93. current_path = Path('keyboards/')
  94. layouts = {}
  95. for directory in path.parts:
  96. current_path = current_path / directory
  97. keyboard_h = '%s.h' % (directory,)
  98. keyboard_h_path = current_path / keyboard_h
  99. if keyboard_h_path.exists():
  100. layouts.update(find_layouts(keyboard_h_path))
  101. return layouts
  102. def _find_all_layouts(info_data, keyboard, rules):
  103. """Looks for layout macros associated with this keyboard.
  104. """
  105. layouts = _search_keyboard_h(Path(keyboard))
  106. if not layouts:
  107. # If we didn't find any layouts above we widen our search. This is error
  108. # prone which is why we want to encourage people to follow the standard above.
  109. _log_warning(info_data, 'Falling back to searching for KEYMAP/LAYOUT macros.')
  110. for file in glob('keyboards/%s/*.h' % keyboard):
  111. if file.endswith('.h'):
  112. these_layouts = find_layouts(file)
  113. if these_layouts:
  114. layouts.update(these_layouts)
  115. if 'LAYOUTS' in rules:
  116. # Match these up against the supplied layouts
  117. supported_layouts = rules['LAYOUTS'].strip().split()
  118. for layout_name in sorted(layouts):
  119. if not layout_name.startswith('LAYOUT_'):
  120. continue
  121. layout_name = layout_name[7:]
  122. if layout_name in supported_layouts:
  123. supported_layouts.remove(layout_name)
  124. if supported_layouts:
  125. _log_error(info_data, 'Missing LAYOUT() macro for %s' % (', '.join(supported_layouts)))
  126. return layouts
  127. def _log_error(info_data, message):
  128. """Send an error message to both JSON and the log.
  129. """
  130. info_data['parse_errors'].append(message)
  131. cli.log.error('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  132. def _log_warning(info_data, message):
  133. """Send a warning message to both JSON and the log.
  134. """
  135. info_data['parse_warnings'].append(message)
  136. cli.log.warning('%s: %s', info_data.get('keyboard_folder', 'Unknown Keyboard!'), message)
  137. def arm_processor_rules(info_data, rules):
  138. """Setup the default info for an ARM board.
  139. """
  140. info_data['processor_type'] = 'arm'
  141. info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
  142. info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
  143. info_data['protocol'] = 'ChibiOS'
  144. if info_data['bootloader'] == 'unknown':
  145. if 'STM32' in info_data['processor']:
  146. info_data['bootloader'] = 'stm32-dfu'
  147. if 'STM32' in info_data['processor']:
  148. info_data['platform'] = 'STM32'
  149. elif 'MCU_SERIES' in rules:
  150. info_data['platform'] = rules['MCU_SERIES']
  151. elif 'ARM_ATSAM' in rules:
  152. info_data['platform'] = 'ARM_ATSAM'
  153. return info_data
  154. def avr_processor_rules(info_data, rules):
  155. """Setup the default info for an AVR board.
  156. """
  157. info_data['processor_type'] = 'avr'
  158. info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
  159. info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
  160. info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
  161. info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
  162. # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
  163. # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
  164. return info_data
  165. def unknown_processor_rules(info_data, rules):
  166. """Setup the default keyboard info for unknown boards.
  167. """
  168. info_data['bootloader'] = 'unknown'
  169. info_data['platform'] = 'unknown'
  170. info_data['processor'] = 'unknown'
  171. info_data['processor_type'] = 'unknown'
  172. info_data['protocol'] = 'unknown'
  173. return info_data
  174. def merge_info_jsons(keyboard, info_data):
  175. """Return a merged copy of all the info.json files for a keyboard.
  176. """
  177. for info_file in find_info_json(keyboard):
  178. # Load and validate the JSON data
  179. with info_file.open('r') as info_fd:
  180. new_info_data = json.load(info_fd)
  181. if not isinstance(new_info_data, dict):
  182. _log_error(info_data, "Invalid file %s, root object should be a dictionary.", str(info_file))
  183. continue
  184. # Copy whitelisted keys into `info_data`
  185. for key in ('keyboard_name', 'manufacturer', 'identifier', 'url', 'maintainer', 'processor', 'bootloader', 'width', 'height'):
  186. if key in new_info_data:
  187. info_data[key] = new_info_data[key]
  188. # Merge the layouts in
  189. if 'layouts' in new_info_data:
  190. for layout_name, json_layout in new_info_data['layouts'].items():
  191. # Only pull in layouts we have a macro for
  192. if layout_name in info_data['layouts']:
  193. if info_data['layouts'][layout_name]['key_count'] != len(json_layout['layout']):
  194. msg = '%s: Number of elements in info.json does not match! info.json:%s != %s:%s'
  195. _log_error(info_data, msg % (layout_name, len(json_layout['layout']), layout_name, len(info_data['layouts'][layout_name]['layout'])))
  196. else:
  197. for i, key in enumerate(info_data['layouts'][layout_name]['layout']):
  198. key.update(json_layout['layout'][i])
  199. return info_data
  200. def find_info_json(keyboard):
  201. """Finds all the info.json files associated with a keyboard.
  202. """
  203. # Find the most specific first
  204. base_path = Path('keyboards')
  205. keyboard_path = base_path / keyboard
  206. keyboard_parent = keyboard_path.parent
  207. info_jsons = [keyboard_path / 'info.json']
  208. # Add DEFAULT_FOLDER before parents, if present
  209. rules = rules_mk(keyboard)
  210. if 'DEFAULT_FOLDER' in rules:
  211. info_jsons.append(Path(rules['DEFAULT_FOLDER']) / 'info.json')
  212. # Add in parent folders for least specific
  213. for _ in range(5):
  214. info_jsons.append(keyboard_parent / 'info.json')
  215. if keyboard_parent.parent == base_path:
  216. break
  217. keyboard_parent = keyboard_parent.parent
  218. # Return a list of the info.json files that actually exist
  219. return [info_json for info_json in info_jsons if info_json.exists()]