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