common.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. """This script handles the XAP protocol data files.
  2. """
  3. import re
  4. import os
  5. import hjson
  6. import jsonschema
  7. from pathlib import Path
  8. from typing import OrderedDict
  9. from jinja2 import Environment, FileSystemLoader, select_autoescape
  10. import qmk.constants
  11. from qmk.git import git_get_version
  12. from qmk.json_schema import json_load, validate, merge_ordered_dicts
  13. from qmk.makefile import parse_rules_mk_file
  14. from qmk.decorators import lru_cache
  15. from qmk.keymap import locate_keymap
  16. from qmk.path import keyboard
  17. from qmk.xap.jinja2_filters import attach_filters
  18. USERSPACE_DIR = Path('users')
  19. XAP_SPEC = 'xap.hjson'
  20. def list_lighting_versions(feature):
  21. """Return available versions - sorted newest first
  22. """
  23. ret = []
  24. for file in Path('data/constants/').glob(f'{feature}_[0-9].[0-9].[0-9].json'):
  25. ret.append(file.stem.split('_')[-1])
  26. ret.sort(reverse=True)
  27. return ret
  28. def load_lighting_spec(feature, version='latest'):
  29. """Build lighting data from the requested spec file
  30. """
  31. if version == 'latest':
  32. version = list_lighting_versions(feature)[0]
  33. spec = json_load(Path(f'data/constants/{feature}_{version}.json'))
  34. # preprocess for gross rgblight "mode + n"
  35. for obj in spec.get('effects', {}).values():
  36. define = obj['key']
  37. offset = 0
  38. found = re.match('(.*)_(\\d+)$', define)
  39. if found:
  40. define = found.group(1)
  41. offset = int(found.group(2)) - 1
  42. obj['define'] = define
  43. obj['offset'] = offset
  44. return spec
  45. def _get_jinja2_env(data_templates_xap_subdir: str):
  46. templates_dir = os.path.join(qmk.constants.QMK_FIRMWARE, 'data', 'templates', 'xap', data_templates_xap_subdir)
  47. j2 = Environment(loader=FileSystemLoader(templates_dir), autoescape=select_autoescape(), lstrip_blocks=True, trim_blocks=True)
  48. return j2
  49. def render_xap_output(data_templates_xap_subdir, file_to_render, defs=None, **kwargs):
  50. if defs is None:
  51. defs = latest_xap_defs()
  52. j2 = _get_jinja2_env(data_templates_xap_subdir)
  53. attach_filters(j2)
  54. specs = {}
  55. for feature in ['rgblight', 'rgb_matrix', 'led_matrix']:
  56. specs[feature] = load_lighting_spec(feature)
  57. 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)
  58. def _find_kb_spec(kb):
  59. base_path = Path('keyboards')
  60. keyboard_parent = keyboard(kb)
  61. for _ in range(5):
  62. if keyboard_parent == base_path:
  63. break
  64. spec = keyboard_parent / XAP_SPEC
  65. if spec.exists():
  66. return spec
  67. keyboard_parent = keyboard_parent.parent
  68. # Just return something we know doesn't exist
  69. return keyboard(kb) / XAP_SPEC
  70. def _find_km_spec(kb, km):
  71. keymap_dir = locate_keymap(kb, km).parent
  72. if not keymap_dir.exists():
  73. return None
  74. # Resolve any potential USER_NAME overrides - default back to keymap name
  75. keymap_rules_mk = parse_rules_mk_file(keymap_dir / 'rules.mk')
  76. username = keymap_rules_mk.get('USER_NAME', km)
  77. keymap_spec = keymap_dir / XAP_SPEC
  78. userspace_spec = USERSPACE_DIR / username / XAP_SPEC
  79. # In the case of both userspace and keymap - keymap wins
  80. return keymap_spec if keymap_spec.exists() else userspace_spec
  81. def get_xap_definition_files():
  82. """Get the sorted list of XAP definition files, from <QMK>/data/xap.
  83. """
  84. xap_defs = qmk.constants.QMK_FIRMWARE / "data" / "xap"
  85. return list(sorted(xap_defs.glob('**/xap_*.hjson')))
  86. def update_xap_definitions(original, new):
  87. """Creates a new XAP definition object based on an original and the new supplied object.
  88. Both inputs must be of type OrderedDict.
  89. Later input dicts overrides earlier dicts for plain values.
  90. 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.
  91. Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
  92. """
  93. if original is None:
  94. original = OrderedDict()
  95. return merge_ordered_dicts([original, new])
  96. @lru_cache(timeout=5)
  97. def get_xap_defs(version):
  98. """Gets the required version of the XAP definitions.
  99. """
  100. files = get_xap_definition_files()
  101. # Slice off anything newer than specified version
  102. if version != 'latest':
  103. index = [idx for idx, s in enumerate(files) if version in str(s)][0]
  104. files = files[:(index + 1)]
  105. definitions = [hjson.load(file.open(encoding='utf-8')) for file in files]
  106. return merge_ordered_dicts(definitions)
  107. def latest_xap_defs():
  108. """Gets the latest version of the XAP definitions.
  109. """
  110. return get_xap_defs('latest')
  111. def merge_xap_defs(kb, km):
  112. """Gets the latest version of the XAP definitions and merges in optional keyboard/keymap specs
  113. """
  114. definitions = [get_xap_defs('latest')]
  115. kb_xap = _find_kb_spec(kb)
  116. if kb_xap.exists():
  117. definitions.append({'routes': {'0x02': hjson.load(kb_xap.open(encoding='utf-8'))}})
  118. km_xap = _find_km_spec(kb, km)
  119. if km_xap.exists():
  120. definitions.append({'routes': {'0x03': hjson.load(km_xap.open(encoding='utf-8'))}})
  121. defs = merge_ordered_dicts(definitions)
  122. try:
  123. validate(defs, 'qmk.xap.v1')
  124. except jsonschema.ValidationError as e:
  125. print(f'Invalid XAP spec: {e.message}')
  126. exit(1)
  127. return defs
  128. def route_conditions(route_stack):
  129. """Handles building the C preprocessor conditional based on the current route.
  130. """
  131. conditions = []
  132. for route in route_stack:
  133. if 'enable_if_preprocessor' in route:
  134. conditions.append(route['enable_if_preprocessor'])
  135. if len(conditions) == 0:
  136. return None
  137. return "(" + ' && '.join([f'({c})' for c in conditions]) + ")"