diff options
| author | Joel Challis <git@zvecr.com> | 2022-05-15 22:39:29 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-16 07:39:29 +1000 |
| commit | 608fa5154c01420ff8f0946655ef16c99dec56a4 (patch) | |
| tree | 7c4c1f34b5a9015560825e7fc1b25d9376f385c1 /lib | |
| parent | b7771ec25b96f2b88a7fa4201081e10ca6fbb9d4 (diff) | |
Data driven `g_led_config` (#16728)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/python/qmk/c_parse.py | 118 | ||||
| -rw-r--r-- | lib/python/qmk/cli/__init__.py | 1 | ||||
| -rwxr-xr-x | lib/python/qmk/cli/generate/keyboard_c.py | 75 | ||||
| -rw-r--r-- | lib/python/qmk/info.py | 45 | ||||
| -rwxr-xr-x | lib/python/qmk/json_encoders.py | 4 |
5 files changed, 240 insertions, 3 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 72be690019..359aaccbbc 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py | |||
| @@ -1,5 +1,9 @@ | |||
| 1 | """Functions for working with config.h files. | 1 | """Functions for working with config.h files. |
| 2 | """ | 2 | """ |
| 3 | from pygments.lexers.c_cpp import CLexer | ||
| 4 | from pygments.token import Token | ||
| 5 | from pygments import lex | ||
| 6 | from itertools import islice | ||
| 3 | from pathlib import Path | 7 | from pathlib import Path |
| 4 | import re | 8 | import re |
| 5 | 9 | ||
| @@ -13,6 +17,13 @@ multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE) | |||
| 13 | layout_macro_define_regex = re.compile(r'^#\s*define') | 17 | layout_macro_define_regex = re.compile(r'^#\s*define') |
| 14 | 18 | ||
| 15 | 19 | ||
| 20 | def _get_chunks(it, size): | ||
| 21 | """Break down a collection into smaller parts | ||
| 22 | """ | ||
| 23 | it = iter(it) | ||
| 24 | return iter(lambda: tuple(islice(it, size)), ()) | ||
| 25 | |||
| 26 | |||
| 16 | def strip_line_comment(string): | 27 | def strip_line_comment(string): |
| 17 | """Removes comments from a single line string. | 28 | """Removes comments from a single line string. |
| 18 | """ | 29 | """ |
| @@ -170,3 +181,110 @@ def _parse_matrix_locations(matrix, file, macro_name): | |||
| 170 | matrix_locations[identifier] = [row_num, col_num] | 181 | matrix_locations[identifier] = [row_num, col_num] |
| 171 | 182 | ||
| 172 | return matrix_locations | 183 | return matrix_locations |
| 184 | |||
| 185 | |||
| 186 | def _coerce_led_token(_type, value): | ||
| 187 | """ Convert token to valid info.json content | ||
| 188 | """ | ||
| 189 | value_map = { | ||
| 190 | 'NO_LED': None, | ||
| 191 | 'LED_FLAG_ALL': 0xFF, | ||
| 192 | 'LED_FLAG_NONE': 0x00, | ||
| 193 | 'LED_FLAG_MODIFIER': 0x01, | ||
| 194 | 'LED_FLAG_UNDERGLOW': 0x02, | ||
| 195 | 'LED_FLAG_KEYLIGHT': 0x04, | ||
| 196 | 'LED_FLAG_INDICATOR': 0x08, | ||
| 197 | } | ||
| 198 | if _type is Token.Literal.Number.Integer: | ||
| 199 | return int(value) | ||
| 200 | if _type is Token.Literal.Number.Float: | ||
| 201 | return float(value) | ||
| 202 | if _type is Token.Literal.Number.Hex: | ||
| 203 | return int(value, 0) | ||
| 204 | if _type is Token.Name and value in value_map.keys(): | ||
| 205 | return value_map[value] | ||
| 206 | |||
| 207 | |||
| 208 | def _parse_led_config(file, matrix_cols, matrix_rows): | ||
| 209 | """Return any 'raw' led/rgb matrix config | ||
| 210 | """ | ||
| 211 | file_contents = file.read_text(encoding='utf-8') | ||
| 212 | file_contents = comment_remover(file_contents) | ||
| 213 | file_contents = file_contents.replace('\\\n', '') | ||
| 214 | |||
| 215 | matrix_raw = [] | ||
| 216 | position_raw = [] | ||
| 217 | flags = [] | ||
| 218 | |||
| 219 | found_led_config = False | ||
| 220 | bracket_count = 0 | ||
| 221 | section = 0 | ||
| 222 | for _type, value in lex(file_contents, CLexer()): | ||
| 223 | # Assume g_led_config..stuff..; | ||
| 224 | if value == 'g_led_config': | ||
| 225 | found_led_config = True | ||
| 226 | elif value == ';': | ||
| 227 | found_led_config = False | ||
| 228 | elif found_led_config: | ||
| 229 | # Assume bracket count hints to section of config we are within | ||
| 230 | if value == '{': | ||
| 231 | bracket_count += 1 | ||
| 232 | if bracket_count == 2: | ||
| 233 | section += 1 | ||
| 234 | elif value == '}': | ||
| 235 | bracket_count -= 1 | ||
| 236 | else: | ||
| 237 | # Assume any non whitespace value here is important enough to stash | ||
| 238 | if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]: | ||
| 239 | if section == 1 and bracket_count == 3: | ||
| 240 | matrix_raw.append(_coerce_led_token(_type, value)) | ||
| 241 | if section == 2 and bracket_count == 3: | ||
| 242 | position_raw.append(_coerce_led_token(_type, value)) | ||
| 243 | if section == 3 and bracket_count == 2: | ||
| 244 | flags.append(_coerce_led_token(_type, value)) | ||
| 245 | |||
| 246 | # Slightly better intrim format | ||
| 247 | matrix = list(_get_chunks(matrix_raw, matrix_cols)) | ||
| 248 | position = list(_get_chunks(position_raw, 2)) | ||
| 249 | matrix_indexes = list(filter(lambda x: x is not None, matrix_raw)) | ||
| 250 | |||
| 251 | # If we have not found anything - bail | ||
| 252 | if not section: | ||
| 253 | return None | ||
| 254 | |||
| 255 | # TODO: Improve crude parsing/validation | ||
| 256 | if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2): | ||
| 257 | raise ValueError("Unable to parse g_led_config matrix data") | ||
| 258 | if len(position) != len(flags): | ||
| 259 | raise ValueError("Unable to parse g_led_config position data") | ||
| 260 | if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)): | ||
| 261 | raise ValueError("OOB within g_led_config matrix data") | ||
| 262 | |||
| 263 | return (matrix, position, flags) | ||
| 264 | |||
| 265 | |||
| 266 | def find_led_config(file, matrix_cols, matrix_rows): | ||
| 267 | """Search file for led/rgb matrix config | ||
| 268 | """ | ||
| 269 | found = _parse_led_config(file, matrix_cols, matrix_rows) | ||
| 270 | if not found: | ||
| 271 | return None | ||
| 272 | |||
| 273 | # Expand collected content | ||
| 274 | (matrix, position, flags) = found | ||
| 275 | |||
| 276 | # Align to output format | ||
| 277 | led_config = [] | ||
| 278 | for index, item in enumerate(position, start=0): | ||
| 279 | led_config.append({ | ||
| 280 | 'x': item[0], | ||
| 281 | 'y': item[1], | ||
| 282 | 'flags': flags[index], | ||
| 283 | }) | ||
| 284 | for r in range(len(matrix)): | ||
| 285 | for c in range(len(matrix[r])): | ||
| 286 | index = matrix[r][c] | ||
| 287 | if index is not None: | ||
| 288 | led_config[index]['matrix'] = [r, c] | ||
| 289 | |||
| 290 | return led_config | ||
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 85baa238a8..d7192631a3 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py | |||
| @@ -52,6 +52,7 @@ subcommands = [ | |||
| 52 | 'qmk.cli.generate.dfu_header', | 52 | 'qmk.cli.generate.dfu_header', |
| 53 | 'qmk.cli.generate.docs', | 53 | 'qmk.cli.generate.docs', |
| 54 | 'qmk.cli.generate.info_json', | 54 | 'qmk.cli.generate.info_json', |
| 55 | 'qmk.cli.generate.keyboard_c', | ||
| 55 | 'qmk.cli.generate.keyboard_h', | 56 | 'qmk.cli.generate.keyboard_h', |
| 56 | 'qmk.cli.generate.layouts', | 57 | 'qmk.cli.generate.layouts', |
| 57 | 'qmk.cli.generate.rgb_breathe_table', | 58 | 'qmk.cli.generate.rgb_breathe_table', |
diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py new file mode 100755 index 0000000000..a9b742f323 --- /dev/null +++ b/lib/python/qmk/cli/generate/keyboard_c.py | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | """Used by the make system to generate keyboard.c from info.json. | ||
| 2 | """ | ||
| 3 | from milc import cli | ||
| 4 | |||
| 5 | from qmk.info import info_json | ||
| 6 | from qmk.commands import dump_lines | ||
| 7 | from qmk.keyboard import keyboard_completer, keyboard_folder | ||
| 8 | from qmk.path import normpath | ||
| 9 | from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE | ||
| 10 | |||
| 11 | |||
| 12 | def _gen_led_config(info_data): | ||
| 13 | """Convert info.json content to g_led_config | ||
| 14 | """ | ||
| 15 | cols = info_data['matrix_size']['cols'] | ||
| 16 | rows = info_data['matrix_size']['rows'] | ||
| 17 | |||
| 18 | config_type = None | ||
| 19 | if 'layout' in info_data.get('rgb_matrix', {}): | ||
| 20 | config_type = 'rgb_matrix' | ||
| 21 | elif 'layout' in info_data.get('led_matrix', {}): | ||
| 22 | config_type = 'led_matrix' | ||
| 23 | |||
| 24 | lines = [] | ||
| 25 | if not config_type: | ||
| 26 | return lines | ||
| 27 | |||
| 28 | matrix = [['NO_LED'] * cols for i in range(rows)] | ||
| 29 | pos = [] | ||
| 30 | flags = [] | ||
| 31 | |||
| 32 | led_config = info_data[config_type]['layout'] | ||
| 33 | for index, item in enumerate(led_config, start=0): | ||
| 34 | if 'matrix' in item: | ||
| 35 | (x, y) = item['matrix'] | ||
| 36 | matrix[x][y] = str(index) | ||
| 37 | pos.append(f'{{ {item.get("x", 0)},{item.get("y", 0)} }}') | ||
| 38 | flags.append(str(item.get('flags', 0))) | ||
| 39 | |||
| 40 | if config_type == 'rgb_matrix': | ||
| 41 | lines.append('#ifdef RGB_MATRIX_ENABLE') | ||
| 42 | lines.append('#include "rgb_matrix.h"') | ||
| 43 | elif config_type == 'led_matrix': | ||
| 44 | lines.append('#ifdef LED_MATRIX_ENABLE') | ||
| 45 | lines.append('#include "led_matrix.h"') | ||
| 46 | |||
| 47 | lines.append('__attribute__ ((weak)) led_config_t g_led_config = {') | ||
| 48 | lines.append(' {') | ||
| 49 | for line in matrix: | ||
| 50 | lines.append(f' {{ {",".join(line)} }},') | ||
| 51 | lines.append(' },') | ||
| 52 | lines.append(f' {{ {",".join(pos)} }},') | ||
| 53 | lines.append(f' {{ {",".join(flags)} }},') | ||
| 54 | lines.append('};') | ||
| 55 | lines.append('#endif') | ||
| 56 | |||
| 57 | return lines | ||
| 58 | |||
| 59 | |||
| 60 | @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') | ||
| 61 | @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") | ||
| 62 | @cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.c for.') | ||
| 63 | @cli.subcommand('Used by the make system to generate keyboard.c from info.json', hidden=True) | ||
| 64 | def generate_keyboard_c(cli): | ||
| 65 | """Generates the keyboard.h file. | ||
| 66 | """ | ||
| 67 | kb_info_json = info_json(cli.args.keyboard) | ||
| 68 | |||
| 69 | # Build the layouts.h file. | ||
| 70 | keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] | ||
| 71 | |||
| 72 | keyboard_h_lines.extend(_gen_led_config(kb_info_json)) | ||
| 73 | |||
| 74 | # Show the results | ||
| 75 | dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet) | ||
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 49d1054519..0763433b3d 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py | |||
| @@ -8,7 +8,7 @@ from dotty_dict import dotty | |||
| 8 | from milc import cli | 8 | from milc import cli |
| 9 | 9 | ||
| 10 | from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS | 10 | from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS |
| 11 | from qmk.c_parse import find_layouts, parse_config_h_file | 11 | from qmk.c_parse import find_layouts, parse_config_h_file, find_led_config |
| 12 | from qmk.json_schema import deep_update, json_load, validate | 12 | from qmk.json_schema import deep_update, json_load, validate |
| 13 | from qmk.keyboard import config_h, rules_mk | 13 | from qmk.keyboard import config_h, rules_mk |
| 14 | from qmk.keymap import list_keymaps, locate_keymap | 14 | from qmk.keymap import list_keymaps, locate_keymap |
| @@ -76,6 +76,9 @@ def info_json(keyboard): | |||
| 76 | # Ensure that we have matrix row and column counts | 76 | # Ensure that we have matrix row and column counts |
| 77 | info_data = _matrix_size(info_data) | 77 | info_data = _matrix_size(info_data) |
| 78 | 78 | ||
| 79 | # Merge in data from <keyboard.c> | ||
| 80 | info_data = _extract_led_config(info_data, str(keyboard)) | ||
| 81 | |||
| 79 | # Validate against the jsonschema | 82 | # Validate against the jsonschema |
| 80 | try: | 83 | try: |
| 81 | validate(info_data, 'qmk.api.keyboard.v1') | 84 | validate(info_data, 'qmk.api.keyboard.v1') |
| @@ -590,6 +593,46 @@ def _extract_rules_mk(info_data, rules): | |||
| 590 | return info_data | 593 | return info_data |
| 591 | 594 | ||
| 592 | 595 | ||
| 596 | def find_keyboard_c(keyboard): | ||
| 597 | """Find all <keyboard>.c files | ||
| 598 | """ | ||
| 599 | keyboard = Path(keyboard) | ||
| 600 | current_path = Path('keyboards/') | ||
| 601 | |||
| 602 | files = [] | ||
| 603 | for directory in keyboard.parts: | ||
| 604 | current_path = current_path / directory | ||
| 605 | keyboard_c_path = current_path / f'{directory}.c' | ||
| 606 | if keyboard_c_path.exists(): | ||
| 607 | files.append(keyboard_c_path) | ||
| 608 | |||
| 609 | return files | ||
| 610 | |||
| 611 | |||
| 612 | def _extract_led_config(info_data, keyboard): | ||
| 613 | """Scan all <keyboard>.c files for led config | ||
| 614 | """ | ||
| 615 | cols = info_data['matrix_size']['cols'] | ||
| 616 | rows = info_data['matrix_size']['rows'] | ||
| 617 | |||
| 618 | # Assume what feature owns g_led_config | ||
| 619 | feature = "rgb_matrix" | ||
| 620 | if info_data.get("features", {}).get("led_matrix", False): | ||
| 621 | feature = "led_matrix" | ||
| 622 | |||
| 623 | # Process | ||
| 624 | for file in find_keyboard_c(keyboard): | ||
| 625 | try: | ||
| 626 | ret = find_led_config(file, cols, rows) | ||
| 627 | if ret: | ||
| 628 | info_data[feature] = info_data.get(feature, {}) | ||
| 629 | info_data[feature]["layout"] = ret | ||
| 630 | except Exception as e: | ||
| 631 | _log_warning(info_data, f'led_config: {file.name}: {e}') | ||
| 632 | |||
| 633 | return info_data | ||
| 634 | |||
| 635 | |||
| 593 | def _matrix_size(info_data): | 636 | def _matrix_size(info_data): |
| 594 | """Add info_data['matrix_size'] if it doesn't exist. | 637 | """Add info_data['matrix_size'] if it doesn't exist. |
| 595 | """ | 638 | """ |
diff --git a/lib/python/qmk/json_encoders.py b/lib/python/qmk/json_encoders.py index 40a5c1dea8..f968b3dbb2 100755 --- a/lib/python/qmk/json_encoders.py +++ b/lib/python/qmk/json_encoders.py | |||
| @@ -75,8 +75,8 @@ class InfoJSONEncoder(QMKJSONEncoder): | |||
| 75 | """Encode info.json dictionaries. | 75 | """Encode info.json dictionaries. |
| 76 | """ | 76 | """ |
| 77 | if obj: | 77 | if obj: |
| 78 | if self.indentation_level == 4: | 78 | if set(("x", "y")).issubset(obj.keys()): |
| 79 | # These are part of a layout, put them on a single line. | 79 | # These are part of a layout/led_config, put them on a single line. |
| 80 | return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }" | 80 | return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }" |
| 81 | 81 | ||
| 82 | else: | 82 | else: |
