logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

keyboard_c.py (9191B)


  1. """Used by the make system to generate keyboard.c from info.json.
  2. """
  3. import bisect
  4. import dataclasses
  5. from typing import Optional
  6. from milc import cli
  7. from qmk.info import info_json
  8. from qmk.commands import dump_lines
  9. from qmk.keyboard import keyboard_completer, keyboard_folder
  10. from qmk.path import normpath
  11. from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, JOYSTICK_AXES
  12. def _gen_led_configs(info_data):
  13. lines = []
  14. if 'layout' in info_data.get('rgb_matrix', {}):
  15. lines.extend(_gen_led_config(info_data, 'rgb_matrix'))
  16. if 'layout' in info_data.get('led_matrix', {}):
  17. lines.extend(_gen_led_config(info_data, 'led_matrix'))
  18. return lines
  19. def _gen_led_config(info_data, config_type):
  20. """Convert info.json content to g_led_config
  21. """
  22. cols = info_data['matrix_size']['cols']
  23. rows = info_data['matrix_size']['rows']
  24. lines = []
  25. matrix = [['NO_LED'] * cols for _ in range(rows)]
  26. pos = []
  27. flags = []
  28. led_layout = info_data[config_type]['layout']
  29. for index, led_data in enumerate(led_layout):
  30. if 'matrix' in led_data:
  31. row, col = led_data['matrix']
  32. matrix[row][col] = str(index)
  33. pos.append(f'{{{led_data.get("x", 0)}, {led_data.get("y", 0)}}}')
  34. flags.append(str(led_data.get('flags', 0)))
  35. if config_type == 'rgb_matrix':
  36. lines.append('#ifdef RGB_MATRIX_ENABLE')
  37. lines.append('#include "rgb_matrix.h"')
  38. elif config_type == 'led_matrix':
  39. lines.append('#ifdef LED_MATRIX_ENABLE')
  40. lines.append('#include "led_matrix.h"')
  41. lines.append('__attribute__ ((weak)) led_config_t g_led_config = {')
  42. lines.append(' {')
  43. for line in matrix:
  44. lines.append(f' {{ {", ".join(line)} }},')
  45. lines.append(' },')
  46. lines.append(f' {{ {", ".join(pos)} }},')
  47. lines.append(f' {{ {", ".join(flags)} }},')
  48. lines.append('};')
  49. lines.append('#endif')
  50. lines.append('')
  51. return lines
  52. def _gen_matrix_mask(info_data):
  53. """Convert info.json content to matrix_mask
  54. """
  55. cols = info_data['matrix_size']['cols']
  56. rows = info_data['matrix_size']['rows']
  57. # Default mask to everything disabled
  58. mask = [['0'] * cols for _ in range(rows)]
  59. # Mirror layout macros squashed on top of each other
  60. for layout_name, layout_data in info_data['layouts'].items():
  61. for key_data in layout_data['layout']:
  62. row, col = key_data['matrix']
  63. if row >= rows or col >= cols:
  64. cli.log.error(f'Skipping matrix_mask due to {layout_name} containing invalid matrix values')
  65. return []
  66. mask[row][col] = '1'
  67. lines = []
  68. lines.append('#ifdef MATRIX_MASKED')
  69. lines.append('__attribute__((weak)) const matrix_row_t matrix_mask[] = {')
  70. for i in range(rows):
  71. lines.append(f' 0b{"".join(reversed(mask[i]))},')
  72. lines.append('};')
  73. lines.append('#endif')
  74. lines.append('')
  75. return lines
  76. def _gen_joystick_axes(info_data):
  77. """Convert info.json content to joystick_axes
  78. """
  79. if 'axes' not in info_data.get('joystick', {}):
  80. return []
  81. axes = info_data['joystick']['axes']
  82. axes_keys = list(axes.keys())
  83. lines = []
  84. lines.append('#ifdef JOYSTICK_ENABLE')
  85. lines.append('joystick_config_t joystick_axes[JOYSTICK_AXIS_COUNT] = {')
  86. # loop over all available axes - injecting virtual axis for those not specified
  87. for index, cur in enumerate(JOYSTICK_AXES):
  88. # bail out if we have generated all requested axis
  89. if len(axes_keys) == 0:
  90. break
  91. axis = 'virtual'
  92. if cur in axes:
  93. axis = axes[cur]
  94. axes_keys.remove(cur)
  95. if axis == 'virtual':
  96. lines.append(f" [{index}] = JOYSTICK_AXIS_VIRTUAL,")
  97. else:
  98. lines.append(f" [{index}] = JOYSTICK_AXIS_IN({axis['input_pin']}, {axis['low']}, {axis['rest']}, {axis['high']}),")
  99. lines.append('};')
  100. lines.append('#endif')
  101. lines.append('')
  102. return lines
  103. @dataclasses.dataclass
  104. class LayoutKey:
  105. """Geometric info for one key in a layout."""
  106. row: int
  107. col: int
  108. x: float
  109. y: float
  110. w: float = 1.0
  111. h: float = 1.0
  112. hand: Optional[str] = None
  113. @staticmethod
  114. def from_json(key_json):
  115. row, col = key_json['matrix']
  116. return LayoutKey(
  117. row=row,
  118. col=col,
  119. x=key_json['x'],
  120. y=key_json['y'],
  121. w=key_json.get('w', 1.0),
  122. h=key_json.get('h', 1.0),
  123. hand=key_json.get('hand', None),
  124. )
  125. @property
  126. def cx(self):
  127. """Center x coordinate of the key."""
  128. return self.x + self.w / 2.0
  129. @property
  130. def cy(self):
  131. """Center y coordinate of the key."""
  132. return self.y + self.h / 2.0
  133. class Layout:
  134. """Geometric info of a layout."""
  135. def __init__(self, layout_json):
  136. self.keys = [LayoutKey.from_json(key_json) for key_json in layout_json['layout']]
  137. self.x_min = min(key.cx for key in self.keys)
  138. self.x_max = max(key.cx for key in self.keys)
  139. self.x_mid = (self.x_min + self.x_max) / 2
  140. # If there is one key with width >= 6u, it is probably the spacebar.
  141. i = [i for i, key in enumerate(self.keys) if key.w >= 6.0]
  142. self.spacebar = self.keys[i[0]] if len(i) == 1 else None
  143. def is_symmetric(self, tol: float = 0.02):
  144. """Whether the key positions are symmetric about x_mid."""
  145. x = sorted([key.cx for key in self.keys])
  146. for i in range(len(x)):
  147. x_i_mirrored = 2.0 * self.x_mid - x[i]
  148. # Find leftmost x element greater than or equal to (x_i_mirrored - tol).
  149. j = bisect.bisect_left(x, x_i_mirrored - tol)
  150. if j == len(x) or abs(x[j] - x_i_mirrored) > tol:
  151. return False
  152. return True
  153. def widest_horizontal_gap(self):
  154. """Finds the x midpoint of the widest horizontal gap between keys."""
  155. x = sorted([key.cx for key in self.keys])
  156. x_mid = self.x_mid
  157. max_sep = 0
  158. for i in range(len(x) - 1):
  159. sep = x[i + 1] - x[i]
  160. if sep > max_sep:
  161. max_sep = sep
  162. x_mid = (x[i + 1] + x[i]) / 2
  163. return x_mid
  164. def _gen_chordal_hold_layout(info_data):
  165. """Convert info.json content to chordal_hold_layout
  166. """
  167. # NOTE: If there are multiple layouts, only the first is read.
  168. for layout_name, layout_json in info_data['layouts'].items():
  169. layout = Layout(layout_json)
  170. break
  171. if layout.is_symmetric():
  172. # If the layout is symmetric (e.g. most split keyboards), guess the
  173. # handedness based on the sign of (x - layout.x_mid).
  174. hand_signs = [key.x - layout.x_mid for key in layout.keys]
  175. elif layout.spacebar is not None:
  176. # If the layout has a spacebar, form a dividing line through the spacebar,
  177. # nearly vertical but with a slight angle to follow typical row stagger.
  178. x0 = layout.spacebar.cx - 0.05
  179. y0 = layout.spacebar.cy - 1.0
  180. hand_signs = [(key.x - x0) - (key.y - y0) / 3.0 for key in layout.keys]
  181. else:
  182. # Fallback: assume handedness based on the widest horizontal separation.
  183. x_mid = layout.widest_horizontal_gap()
  184. hand_signs = [key.x - x_mid for key in layout.keys]
  185. for key, hand_sign in zip(layout.keys, hand_signs):
  186. if key.hand is None:
  187. if key == layout.spacebar or abs(hand_sign) <= 0.02:
  188. key.hand = '*'
  189. else:
  190. key.hand = 'L' if hand_sign < 0.0 else 'R'
  191. lines = []
  192. lines.append('#ifdef CHORDAL_HOLD')
  193. line = ('__attribute__((weak)) const char chordal_hold_layout[MATRIX_ROWS][MATRIX_COLS] PROGMEM = ' + layout_name + '(')
  194. x_prev = None
  195. for key in layout.keys:
  196. if x_prev is None or key.x < x_prev:
  197. lines.append(line)
  198. line = ' '
  199. line += f"'{key.hand}', "
  200. x_prev = key.x
  201. lines.append(line[:-2])
  202. lines.append(');')
  203. lines.append('#endif')
  204. return lines
  205. @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
  206. @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
  207. @cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.c for.')
  208. @cli.subcommand('Used by the make system to generate keyboard.c from info.json', hidden=True)
  209. def generate_keyboard_c(cli):
  210. """Generates the keyboard.h file.
  211. """
  212. kb_info_json = info_json(cli.args.keyboard)
  213. # Build the layouts.h file.
  214. keyboard_c_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', '']
  215. keyboard_c_lines.extend(_gen_led_configs(kb_info_json))
  216. keyboard_c_lines.extend(_gen_matrix_mask(kb_info_json))
  217. keyboard_c_lines.extend(_gen_joystick_axes(kb_info_json))
  218. keyboard_c_lines.extend(_gen_chordal_hold_layout(kb_info_json))
  219. # Show the results
  220. dump_lines(cli.args.output, keyboard_c_lines, cli.args.quiet)