2021-08-11 11:08:32 +00:00
|
|
|
"""This script handles the XAP protocol data files.
|
|
|
|
"""
|
2022-10-13 01:21:15 +00:00
|
|
|
import re
|
2022-02-14 18:19:13 +00:00
|
|
|
import os
|
2021-08-11 11:08:32 +00:00
|
|
|
import hjson
|
2022-05-23 19:02:14 +00:00
|
|
|
import jsonschema
|
2022-05-10 01:29:30 +00:00
|
|
|
from pathlib import Path
|
|
|
|
from typing import OrderedDict
|
2022-02-14 18:19:13 +00:00
|
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
2022-05-10 01:29:30 +00:00
|
|
|
|
2022-10-16 19:24:37 +00:00
|
|
|
import qmk.constants
|
2022-10-16 02:19:15 +00:00
|
|
|
from qmk.git import git_get_version
|
2023-01-01 19:20:04 +00:00
|
|
|
from qmk.json_schema import json_load, validate, merge_ordered_dicts
|
2023-01-09 19:35:47 +00:00
|
|
|
from qmk.makefile import parse_rules_mk_file
|
2022-05-10 02:48:48 +00:00
|
|
|
from qmk.decorators import lru_cache
|
2022-05-22 23:12:36 +00:00
|
|
|
from qmk.keymap import locate_keymap
|
|
|
|
from qmk.path import keyboard
|
2022-10-16 02:19:15 +00:00
|
|
|
from qmk.xap.jinja2_filters import attach_filters
|
2021-08-11 11:08:32 +00:00
|
|
|
|
2023-01-09 19:35:47 +00:00
|
|
|
USERSPACE_DIR = Path('users')
|
2022-05-23 01:14:42 +00:00
|
|
|
XAP_SPEC = 'xap.hjson'
|
|
|
|
|
2022-02-14 18:19:13 +00:00
|
|
|
|
2022-10-13 01:21:15 +00:00
|
|
|
def list_lighting_versions(feature):
|
|
|
|
"""Return available versions - sorted newest first
|
|
|
|
"""
|
|
|
|
ret = []
|
|
|
|
for file in Path('data/constants/').glob(f'{feature}_[0-9].[0-9].[0-9].json'):
|
|
|
|
ret.append(file.stem.split('_')[-1])
|
|
|
|
|
|
|
|
ret.sort(reverse=True)
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
|
def load_lighting_spec(feature, version='latest'):
|
|
|
|
"""Build lighting data from the requested spec file
|
|
|
|
"""
|
|
|
|
if version == 'latest':
|
|
|
|
version = list_lighting_versions(feature)[0]
|
|
|
|
|
|
|
|
spec = json_load(Path(f'data/constants/{feature}_{version}.json'))
|
|
|
|
|
|
|
|
# preprocess for gross rgblight "mode + n"
|
|
|
|
for obj in spec.get('effects', {}).values():
|
|
|
|
define = obj['key']
|
|
|
|
offset = 0
|
|
|
|
found = re.match('(.*)_(\\d+)$', define)
|
|
|
|
if found:
|
|
|
|
define = found.group(1)
|
|
|
|
offset = int(found.group(2)) - 1
|
|
|
|
obj['define'] = define
|
|
|
|
obj['offset'] = offset
|
|
|
|
|
|
|
|
return spec
|
|
|
|
|
|
|
|
|
2022-02-14 18:19:13 +00:00
|
|
|
def _get_jinja2_env(data_templates_xap_subdir: str):
|
2022-10-16 19:24:37 +00:00
|
|
|
templates_dir = os.path.join(qmk.constants.QMK_FIRMWARE, 'data', 'templates', 'xap', data_templates_xap_subdir)
|
2022-02-14 18:19:13 +00:00
|
|
|
j2 = Environment(loader=FileSystemLoader(templates_dir), autoescape=select_autoescape())
|
|
|
|
return j2
|
|
|
|
|
|
|
|
|
2022-10-16 02:19:15 +00:00
|
|
|
def render_xap_output(data_templates_xap_subdir, file_to_render, defs=None, **kwargs):
|
|
|
|
if defs is None:
|
|
|
|
defs = latest_xap_defs()
|
2022-02-14 18:19:13 +00:00
|
|
|
j2 = _get_jinja2_env(data_templates_xap_subdir)
|
2022-10-13 00:42:27 +00:00
|
|
|
|
2022-10-16 02:19:15 +00:00
|
|
|
attach_filters(j2)
|
2022-10-13 00:42:27 +00:00
|
|
|
|
2022-10-16 19:24:37 +00:00
|
|
|
specs = {}
|
2022-10-13 01:21:15 +00:00
|
|
|
for feature in ['rgblight', 'rgb_matrix', 'led_matrix']:
|
2022-10-16 19:24:37 +00:00
|
|
|
specs[feature] = load_lighting_spec(feature)
|
2022-10-13 00:42:27 +00:00
|
|
|
|
2022-10-16 19:24:37 +00:00
|
|
|
return j2.get_template(file_to_render).render(xap=defs, qmk_version=git_get_version(), xap_str=hjson.dumps(defs), specs=specs, constants=qmk.constants, **kwargs)
|
2021-08-11 11:08:32 +00:00
|
|
|
|
|
|
|
|
2022-05-23 01:14:42 +00:00
|
|
|
def _find_kb_spec(kb):
|
|
|
|
base_path = Path('keyboards')
|
|
|
|
keyboard_parent = keyboard(kb)
|
|
|
|
|
|
|
|
for _ in range(5):
|
|
|
|
if keyboard_parent == base_path:
|
|
|
|
break
|
|
|
|
|
|
|
|
spec = keyboard_parent / XAP_SPEC
|
|
|
|
if spec.exists():
|
|
|
|
return spec
|
|
|
|
|
|
|
|
keyboard_parent = keyboard_parent.parent
|
|
|
|
|
|
|
|
# Just return something we know doesn't exist
|
|
|
|
return keyboard(kb) / XAP_SPEC
|
|
|
|
|
|
|
|
|
|
|
|
def _find_km_spec(kb, km):
|
2023-01-09 19:35:47 +00:00
|
|
|
keymap_dir = locate_keymap(kb, km).parent
|
|
|
|
if not keymap_dir.exists():
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Resolve any potential USER_NAME overrides - default back to keymap name
|
|
|
|
keymap_rules_mk = parse_rules_mk_file(keymap_dir / 'rules.mk')
|
|
|
|
username = keymap_rules_mk.get('USER_NAME', km)
|
|
|
|
|
|
|
|
keymap_spec = keymap_dir / XAP_SPEC
|
|
|
|
userspace_spec = USERSPACE_DIR / username / XAP_SPEC
|
|
|
|
|
|
|
|
# In the case of both userspace and keymap - keymap wins
|
|
|
|
return keymap_spec if keymap_spec.exists() else userspace_spec
|
2022-05-23 01:14:42 +00:00
|
|
|
|
|
|
|
|
2021-08-11 11:08:32 +00:00
|
|
|
def get_xap_definition_files():
|
|
|
|
"""Get the sorted list of XAP definition files, from <QMK>/data/xap.
|
|
|
|
"""
|
2022-10-16 19:24:37 +00:00
|
|
|
xap_defs = qmk.constants.QMK_FIRMWARE / "data" / "xap"
|
2021-08-11 11:08:32 +00:00
|
|
|
return list(sorted(xap_defs.glob('**/xap_*.hjson')))
|
|
|
|
|
|
|
|
|
|
|
|
def update_xap_definitions(original, new):
|
|
|
|
"""Creates a new XAP definition object based on an original and the new supplied object.
|
|
|
|
|
|
|
|
Both inputs must be of type OrderedDict.
|
|
|
|
Later input dicts overrides earlier dicts for plain values.
|
|
|
|
Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
|
|
|
|
Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
|
|
|
|
"""
|
|
|
|
if original is None:
|
|
|
|
original = OrderedDict()
|
2023-01-01 19:20:04 +00:00
|
|
|
return merge_ordered_dicts([original, new])
|
2021-08-11 11:08:32 +00:00
|
|
|
|
|
|
|
|
2022-05-10 02:48:48 +00:00
|
|
|
@lru_cache(timeout=5)
|
2022-05-10 01:29:30 +00:00
|
|
|
def get_xap_defs(version):
|
|
|
|
"""Gets the required version of the XAP definitions.
|
|
|
|
"""
|
|
|
|
files = get_xap_definition_files()
|
|
|
|
|
|
|
|
# Slice off anything newer than specified version
|
2022-05-10 02:48:48 +00:00
|
|
|
if version != 'latest':
|
|
|
|
index = [idx for idx, s in enumerate(files) if version in str(s)][0]
|
|
|
|
files = files[:(index + 1)]
|
2022-05-10 01:29:30 +00:00
|
|
|
|
|
|
|
definitions = [hjson.load(file.open(encoding='utf-8')) for file in files]
|
2023-01-01 19:20:04 +00:00
|
|
|
return merge_ordered_dicts(definitions)
|
2022-05-10 01:29:30 +00:00
|
|
|
|
|
|
|
|
2022-05-10 02:48:48 +00:00
|
|
|
def latest_xap_defs():
|
|
|
|
"""Gets the latest version of the XAP definitions.
|
|
|
|
"""
|
|
|
|
return get_xap_defs('latest')
|
|
|
|
|
|
|
|
|
2022-05-22 23:12:36 +00:00
|
|
|
def merge_xap_defs(kb, km):
|
|
|
|
"""Gets the latest version of the XAP definitions and merges in optional keyboard/keymap specs
|
|
|
|
"""
|
|
|
|
definitions = [get_xap_defs('latest')]
|
|
|
|
|
|
|
|
kb_xap = _find_kb_spec(kb)
|
|
|
|
if kb_xap.exists():
|
|
|
|
definitions.append({'routes': {'0x02': hjson.load(kb_xap.open(encoding='utf-8'))}})
|
|
|
|
|
|
|
|
km_xap = _find_km_spec(kb, km)
|
|
|
|
if km_xap.exists():
|
|
|
|
definitions.append({'routes': {'0x03': hjson.load(km_xap.open(encoding='utf-8'))}})
|
|
|
|
|
2023-01-01 19:20:04 +00:00
|
|
|
defs = merge_ordered_dicts(definitions)
|
2022-05-23 19:02:14 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
validate(defs, 'qmk.xap.v1')
|
|
|
|
|
|
|
|
except jsonschema.ValidationError as e:
|
|
|
|
print(f'Invalid XAP spec: {e.message}')
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
return defs
|
2022-05-22 23:12:36 +00:00
|
|
|
|
|
|
|
|
2021-08-11 11:08:32 +00:00
|
|
|
def route_conditions(route_stack):
|
|
|
|
"""Handles building the C preprocessor conditional based on the current route.
|
|
|
|
"""
|
|
|
|
conditions = []
|
|
|
|
for route in route_stack:
|
|
|
|
if 'enable_if_preprocessor' in route:
|
|
|
|
conditions.append(route['enable_if_preprocessor'])
|
|
|
|
|
|
|
|
if len(conditions) == 0:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return "(" + ' && '.join([f'({c})' for c in conditions]) + ")"
|