info.py 9.1 KB

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