at master 9.2 kB view raw
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)