info.py 8.9 KB

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