summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJoel Challis <git@zvecr.com>2022-05-15 22:39:29 +0100
committerGitHub <noreply@github.com>2022-05-16 07:39:29 +1000
commit608fa5154c01420ff8f0946655ef16c99dec56a4 (patch)
tree7c4c1f34b5a9015560825e7fc1b25d9376f385c1 /lib
parentb7771ec25b96f2b88a7fa4201081e10ca6fbb9d4 (diff)
Data driven `g_led_config` (#16728)
Diffstat (limited to 'lib')
-rw-r--r--lib/python/qmk/c_parse.py118
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rwxr-xr-xlib/python/qmk/cli/generate/keyboard_c.py75
-rw-r--r--lib/python/qmk/info.py45
-rwxr-xr-xlib/python/qmk/json_encoders.py4
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"""
3from pygments.lexers.c_cpp import CLexer
4from pygments.token import Token
5from pygments import lex
6from itertools import islice
3from pathlib import Path 7from pathlib import Path
4import re 8import re
5 9
@@ -13,6 +17,13 @@ multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
13layout_macro_define_regex = re.compile(r'^#\s*define') 17layout_macro_define_regex = re.compile(r'^#\s*define')
14 18
15 19
20def _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
16def strip_line_comment(string): 27def 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
186def _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
208def _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
266def 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"""
3from milc import cli
4
5from qmk.info import info_json
6from qmk.commands import dump_lines
7from qmk.keyboard import keyboard_completer, keyboard_folder
8from qmk.path import normpath
9from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
10
11
12def _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)
64def 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
8from milc import cli 8from milc import cli
9 9
10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS 10from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
11from qmk.c_parse import find_layouts, parse_config_h_file 11from qmk.c_parse import find_layouts, parse_config_h_file, find_led_config
12from qmk.json_schema import deep_update, json_load, validate 12from qmk.json_schema import deep_update, json_load, validate
13from qmk.keyboard import config_h, rules_mk 13from qmk.keyboard import config_h, rules_mk
14from qmk.keymap import list_keymaps, locate_keymap 14from 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
596def 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
612def _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
593def _matrix_size(info_data): 636def _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: