Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data driven g_led_config #16728

Merged
merged 5 commits into from May 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion builddefs/build_keyboard.mk
Expand Up @@ -322,12 +322,18 @@ ifneq ("$(wildcard $(KEYBOARD_PATH_5)/info.json)","")
endif

CONFIG_H += $(KEYBOARD_OUTPUT)/src/info_config.h $(KEYBOARD_OUTPUT)/src/layouts.h
KEYBOARD_SRC += $(KEYBOARD_OUTPUT)/src/default_keyboard.c

$(KEYBOARD_OUTPUT)/src/info_config.h: $(INFO_JSON_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-config-h --quiet --keyboard $(KEYBOARD) --output $(KEYBOARD_OUTPUT)/src/info_config.h)
@$(BUILD_CMD)

$(KEYBOARD_OUTPUT)/src/default_keyboard.c: $(INFO_JSON_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-keyboard-c --quiet --keyboard $(KEYBOARD) --output $(KEYBOARD_OUTPUT)/src/default_keyboard.c)
@$(BUILD_CMD)

$(KEYBOARD_OUTPUT)/src/default_keyboard.h: $(INFO_JSON_FILES)
@$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD)
$(eval CMD=$(QMK_BIN) generate-keyboard-h --quiet --keyboard $(KEYBOARD) --output $(KEYBOARD_OUTPUT)/src/default_keyboard.h)
Expand All @@ -338,7 +344,7 @@ $(KEYBOARD_OUTPUT)/src/layouts.h: $(INFO_JSON_FILES)
$(eval CMD=$(QMK_BIN) generate-layouts --quiet --keyboard $(KEYBOARD) --output $(KEYBOARD_OUTPUT)/src/layouts.h)
@$(BUILD_CMD)

generated-files: $(KEYBOARD_OUTPUT)/src/info_config.h $(KEYBOARD_OUTPUT)/src/default_keyboard.h $(KEYBOARD_OUTPUT)/src/layouts.h
generated-files: $(KEYBOARD_OUTPUT)/src/info_config.h $(KEYBOARD_OUTPUT)/src/default_keyboard.c $(KEYBOARD_OUTPUT)/src/default_keyboard.h $(KEYBOARD_OUTPUT)/src/layouts.h

.INTERMEDIATE : generated-files

Expand Down
56 changes: 56 additions & 0 deletions data/schemas/keyboard.jsonschema
Expand Up @@ -212,6 +212,62 @@
"timeout": {"$ref": "qmk.definitions.v1#/unsigned_int"}
}
},
"led_matrix": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"layout": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"matrix": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"min": 0,
"multipleOf": 1
}
},
"x": {"$ref": "qmk.definitions.v1#/key_unit"},
"y": {"$ref": "qmk.definitions.v1#/key_unit"},
"flags": {"$ref": "qmk.definitions.v1#/unsigned_decimal"}
}
}
}
}
},
"rgb_matrix": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"layout": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"matrix": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"min": 0,
"multipleOf": 1
}
},
"x": {"$ref": "qmk.definitions.v1#/key_unit"},
"y": {"$ref": "qmk.definitions.v1#/key_unit"},
"flags": {"$ref": "qmk.definitions.v1#/unsigned_decimal"}
}
}
}
}
},
"rgblight": {
"type": "object",
"additionalProperties": false,
Expand Down
118 changes: 118 additions & 0 deletions lib/python/qmk/c_parse.py
@@ -1,5 +1,9 @@
"""Functions for working with config.h files.
"""
from pygments.lexers.c_cpp import CLexer
from pygments.token import Token
from pygments import lex
from itertools import islice
from pathlib import Path
import re

Expand All @@ -13,6 +17,13 @@
layout_macro_define_regex = re.compile(r'^#\s*define')


def _get_chunks(it, size):
"""Break down a collection into smaller parts
"""
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())


def strip_line_comment(string):
"""Removes comments from a single line string.
"""
Expand Down Expand Up @@ -170,3 +181,110 @@ def _parse_matrix_locations(matrix, file, macro_name):
matrix_locations[identifier] = [row_num, col_num]

return matrix_locations


def _coerce_led_token(_type, value):
""" Convert token to valid info.json content
"""
value_map = {
'NO_LED': None,
'LED_FLAG_ALL': 0xFF,
'LED_FLAG_NONE': 0x00,
'LED_FLAG_MODIFIER': 0x01,
'LED_FLAG_UNDERGLOW': 0x02,
'LED_FLAG_KEYLIGHT': 0x04,
'LED_FLAG_INDICATOR': 0x08,
}
if _type is Token.Literal.Number.Integer:
return int(value)
if _type is Token.Literal.Number.Float:
return float(value)
if _type is Token.Literal.Number.Hex:
return int(value, 0)
if _type is Token.Name and value in value_map.keys():
return value_map[value]


def _parse_led_config(file, matrix_cols, matrix_rows):
"""Return any 'raw' led/rgb matrix config
"""
file_contents = file.read_text(encoding='utf-8')
file_contents = comment_remover(file_contents)
file_contents = file_contents.replace('\\\n', '')

matrix_raw = []
position_raw = []
flags = []

found_led_config = False
bracket_count = 0
section = 0
for _type, value in lex(file_contents, CLexer()):
# Assume g_led_config..stuff..;
if value == 'g_led_config':
found_led_config = True
elif value == ';':
found_led_config = False
elif found_led_config:
# Assume bracket count hints to section of config we are within
if value == '{':
bracket_count += 1
if bracket_count == 2:
section += 1
elif value == '}':
bracket_count -= 1
else:
# Assume any non whitespace value here is important enough to stash
if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
if section == 1 and bracket_count == 3:
matrix_raw.append(_coerce_led_token(_type, value))
if section == 2 and bracket_count == 3:
position_raw.append(_coerce_led_token(_type, value))
if section == 3 and bracket_count == 2:
flags.append(_coerce_led_token(_type, value))

# Slightly better intrim format
matrix = list(_get_chunks(matrix_raw, matrix_cols))
position = list(_get_chunks(position_raw, 2))
matrix_indexes = list(filter(lambda x: x is not None, matrix_raw))

# If we have not found anything - bail
if not section:
return None

# TODO: Improve crude parsing/validation
if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
raise ValueError("Unable to parse g_led_config matrix data")
if len(position) != len(flags):
raise ValueError("Unable to parse g_led_config position data")
if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
raise ValueError("OOB within g_led_config matrix data")

return (matrix, position, flags)


def find_led_config(file, matrix_cols, matrix_rows):
"""Search file for led/rgb matrix config
"""
found = _parse_led_config(file, matrix_cols, matrix_rows)
if not found:
return None

# Expand collected content
(matrix, position, flags) = found

# Align to output format
led_config = []
for index, item in enumerate(position, start=0):
led_config.append({
'x': item[0],
'y': item[1],
'flags': flags[index],
})
for r in range(len(matrix)):
for c in range(len(matrix[r])):
index = matrix[r][c]
if index is not None:
led_config[index]['matrix'] = [r, c]

return led_config
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Expand Up @@ -52,6 +52,7 @@
'qmk.cli.generate.dfu_header',
'qmk.cli.generate.docs',
'qmk.cli.generate.info_json',
'qmk.cli.generate.keyboard_c',
'qmk.cli.generate.keyboard_h',
'qmk.cli.generate.layouts',
'qmk.cli.generate.rgb_breathe_table',
Expand Down
75 changes: 75 additions & 0 deletions lib/python/qmk/cli/generate/keyboard_c.py
@@ -0,0 +1,75 @@
"""Used by the make system to generate keyboard.c from info.json.
"""
from milc import cli

from qmk.info import info_json
from qmk.commands import dump_lines
from qmk.keyboard import keyboard_completer, keyboard_folder
from qmk.path import normpath
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE


def _gen_led_config(info_data):
"""Convert info.json content to g_led_config
"""
cols = info_data['matrix_size']['cols']
rows = info_data['matrix_size']['rows']

config_type = None
if 'layout' in info_data.get('rgb_matrix', {}):
config_type = 'rgb_matrix'
elif 'layout' in info_data.get('led_matrix', {}):
config_type = 'led_matrix'

lines = []
if not config_type:
return lines

zvecr marked this conversation as resolved.
Show resolved Hide resolved
matrix = [['NO_LED'] * cols for i in range(rows)]
pos = []
flags = []

led_config = info_data[config_type]['layout']
for index, item in enumerate(led_config, start=0):
if 'matrix' in item:
(x, y) = item['matrix']
matrix[x][y] = str(index)
pos.append(f'{{ {item.get("x", 0)},{item.get("y", 0)} }}')
flags.append(str(item.get('flags', 0)))

if config_type == 'rgb_matrix':
lines.append('#ifdef RGB_MATRIX_ENABLE')
lines.append('#include "rgb_matrix.h"')
elif config_type == 'led_matrix':
lines.append('#ifdef LED_MATRIX_ENABLE')
lines.append('#include "led_matrix.h"')

lines.append('__attribute__ ((weak)) led_config_t g_led_config = {')
lines.append(' {')
for line in matrix:
lines.append(f' {{ {",".join(line)} }},')
lines.append(' },')
lines.append(f' {{ {",".join(pos)} }},')
lines.append(f' {{ {",".join(flags)} }},')
lines.append('};')
lines.append('#endif')

zvecr marked this conversation as resolved.
Show resolved Hide resolved
return lines


@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@cli.argument('-kb', '--keyboard', arg_only=True, type=keyboard_folder, completer=keyboard_completer, required=True, help='Keyboard to generate keyboard.c for.')
@cli.subcommand('Used by the make system to generate keyboard.c from info.json', hidden=True)
def generate_keyboard_c(cli):
"""Generates the keyboard.h file.
"""
kb_info_json = info_json(cli.args.keyboard)

# Build the layouts.h file.
keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', '']

keyboard_h_lines.extend(_gen_led_config(kb_info_json))

# Show the results
dump_lines(cli.args.output, keyboard_h_lines, cli.args.quiet)
45 changes: 44 additions & 1 deletion lib/python/qmk/info.py
Expand Up @@ -8,7 +8,7 @@
from milc import cli

from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
from qmk.c_parse import find_layouts, parse_config_h_file
from qmk.c_parse import find_layouts, parse_config_h_file, find_led_config
from qmk.json_schema import deep_update, json_load, validate
from qmk.keyboard import config_h, rules_mk
from qmk.keymap import list_keymaps, locate_keymap
Expand Down Expand Up @@ -76,6 +76,9 @@ def info_json(keyboard):
# Ensure that we have matrix row and column counts
info_data = _matrix_size(info_data)

# Merge in data from <keyboard.c>
info_data = _extract_led_config(info_data, str(keyboard))

# Validate against the jsonschema
try:
validate(info_data, 'qmk.api.keyboard.v1')
Expand Down Expand Up @@ -590,6 +593,46 @@ def _extract_rules_mk(info_data, rules):
return info_data


def find_keyboard_c(keyboard):
"""Find all <keyboard>.c files
"""
keyboard = Path(keyboard)
current_path = Path('keyboards/')

files = []
for directory in keyboard.parts:
current_path = current_path / directory
keyboard_c_path = current_path / f'{directory}.c'
if keyboard_c_path.exists():
files.append(keyboard_c_path)

return files


def _extract_led_config(info_data, keyboard):
"""Scan all <keyboard>.c files for led config
"""
cols = info_data['matrix_size']['cols']
rows = info_data['matrix_size']['rows']

# Assume what feature owns g_led_config
feature = "rgb_matrix"
if info_data.get("features", {}).get("led_matrix", False):
feature = "led_matrix"

# Process
for file in find_keyboard_c(keyboard):
try:
ret = find_led_config(file, cols, rows)
if ret:
info_data[feature] = info_data.get(feature, {})
info_data[feature]["layout"] = ret
except Exception as e:
_log_warning(info_data, f'led_config: {file.name}: {e}')

return info_data


def _matrix_size(info_data):
"""Add info_data['matrix_size'] if it doesn't exist.
"""
Expand Down
4 changes: 2 additions & 2 deletions lib/python/qmk/json_encoders.py
Expand Up @@ -75,8 +75,8 @@ def encode_dict(self, obj):
"""Encode info.json dictionaries.
"""
if obj:
if self.indentation_level == 4:
# These are part of a layout, put them on a single line.
if set(("x", "y")).issubset(obj.keys()):
# These are part of a layout/led_config, put them on a single line.
return "{ " + ", ".join(f"{self.encode(key)}: {self.encode(element)}" for key, element in sorted(obj.items())) + " }"

else:
Expand Down