diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 42d377fe4a9c..340a10226059 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,6 +36,7 @@ jobs: - name: Install dependencies run: | + pip3 install -r requirements-dev.txt apt-get update && apt-get install -y rsync doxygen # install nvm touch $HOME/.bashrc diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index a31526084f9e..cfaca65ac8db 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -21,6 +21,10 @@ jobs: - uses: actions/checkout@v4 + - name: Install python reqs + run: | + python3 -m pip install -r requirements.txt + - name: Run qmk generators run: | util/regen.sh diff --git a/.github/workflows/regen_push.yml b/.github/workflows/regen_push.yml index 9a05a9461dfe..2e4d408afa35 100644 --- a/.github/workflows/regen_push.yml +++ b/.github/workflows/regen_push.yml @@ -21,6 +21,10 @@ jobs: - uses: actions/checkout@v4 + - name: Install dependencies + run: | + pip3 install -r requirements-dev.txt + - name: Run qmk generators run: | util/regen.sh diff --git a/.gitignore b/.gitignore index 20e706a2b59b..f74035e17e24 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,7 @@ secrets.tar # Python things __pycache__ .python-version +*.egg-info .venv # Prerequisites for updating ChibiOS diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk index 2b1293088772..b9eeb7dd966a 100644 --- a/builddefs/common_features.mk +++ b/builddefs/common_features.mk @@ -44,6 +44,10 @@ else ifeq ($(strip $(DEBUG_MATRIX_SCAN_RATE_ENABLE)), api) OPT_DEFS += -DDEBUG_MATRIX_SCAN_RATE endif +ifeq ($(strip $(XAP_ENABLE)), yes) + include $(BUILDDEFS_PATH)/xap.mk +endif + AUDIO_ENABLE ?= no ifeq ($(strip $(AUDIO_ENABLE)), yes) ifeq ($(PLATFORM),CHIBIOS) @@ -627,6 +631,22 @@ ifeq ($(strip $(VIA_ENABLE)), yes) TRI_LAYER_ENABLE := yes endif +ifeq ($(strip $(XAP_ENABLE)), yes) + ifeq ($(strip $(VIA_ENABLE)), yes) + $(error 'XAP_ENABLE = $(XAP_ENABLE)' deprecates 'VIA_ENABLE = $(VIA_ENABLE)'. Please set 'VIA_ENABLE = no') + endif + + DYNAMIC_KEYMAP_ENABLE := yes + FNV_ENABLE := yes + SECURE_ENABLE := yes + BOOTMAGIC_ENABLE := yes + + OPT_DEFS += -DXAP_ENABLE + OPT_DEFS += -DBOOTLOADER_JUMP_SUPPORTED + VPATH += $(QUANTUM_DIR)/xap + SRC += $(QUANTUM_DIR)/xap/xap.c $(QUANTUM_DIR)/xap/xap_handlers.c +endif + VALID_CUSTOM_MATRIX_TYPES:= yes lite no CUSTOM_MATRIX ?= no diff --git a/builddefs/xap.mk b/builddefs/xap.mk new file mode 100644 index 000000000000..78e138aca0e6 --- /dev/null +++ b/builddefs/xap.mk @@ -0,0 +1,44 @@ +# Copyright 2022 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later + +XAP_FILES := $(shell ls -1 data/xap/* | sort | xargs echo) +ifneq ("$(wildcard $(KEYBOARD_PATH_1)/xap.hjson)","") + XAP_FILES += $(KEYBOARD_PATH_1)/xap.hjson +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_2)/xap.hjson)","") + XAP_FILES += $(KEYBOARD_PATH_2)/xap.hjson +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_3)/xap.hjson)","") + XAP_FILES += $(KEYBOARD_PATH_3)/xap.hjson +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_4)/xap.hjson)","") + XAP_FILES += $(KEYBOARD_PATH_4)/xap.hjson +endif +ifneq ("$(wildcard $(KEYBOARD_PATH_5)/xap.hjson)","") + XAP_FILES += $(KEYBOARD_PATH_5)/xap.hjson +endif +ifneq ("$(wildcard $(KEYMAP_PATH)/xap.hjson)","") + XAP_FILES += $(KEYMAP_PATH)/xap.hjson +endif +ifneq ("$(wildcard $(USER_NAME)/xap.hjson)","") + XAP_FILES += $(USER_NAME)/xap.hjson +endif + +$(INTERMEDIATE_OUTPUT)/src/config_blob_gz.h: $(INFO_JSON_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) xap-generate-qmk-blob-h -o "$(INTERMEDIATE_OUTPUT)/src/config_blob_gz.h" -kb $(KEYBOARD) -km $(KEYMAP)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/xap_generated.inl: $(XAP_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) xap-generate-qmk-inc -o "$(INTERMEDIATE_OUTPUT)/src/xap_generated.inl" -kb $(KEYBOARD) -km $(KEYMAP)) + @$(BUILD_CMD) + +$(INTERMEDIATE_OUTPUT)/src/xap_generated.h: $(XAP_FILES) + @$(SILENT) || printf "$(MSG_GENERATING) $@" | $(AWK_CMD) + $(eval CMD=$(QMK_BIN) xap-generate-qmk-h -o "$(INTERMEDIATE_OUTPUT)/src/xap_generated.h" -kb $(KEYBOARD) -km $(KEYMAP)) + @$(BUILD_CMD) + +generated-files: $(INTERMEDIATE_OUTPUT)/src/config_blob_gz.h $(INTERMEDIATE_OUTPUT)/src/xap_generated.inl $(INTERMEDIATE_OUTPUT)/src/xap_generated.h + +VPATH += $(INTERMEDIATE_OUTPUT)/src diff --git a/data/constants/lighting/led_matrix_0.0.1.hjson b/data/constants/lighting/led_matrix_0.0.1.hjson new file mode 100644 index 000000000000..769f3d3fbad7 --- /dev/null +++ b/data/constants/lighting/led_matrix_0.0.1.hjson @@ -0,0 +1,82 @@ +{ + "groups": { + "reactive": { + "define": "LED_MATRIX_KEYREACTIVE_ENABLED" + } + }, + "effects": { + "0x00": { + "key": "SOLID" + }, + "0x01": { + "key": "ALPHAS_MODS" + }, + "0x02": { + "key": "BREATHING" + }, + "0x03": { + "key": "BAND" + }, + "0x04": { + "key": "BAND_PINWHEEL" + }, + "0x05": { + "key": "BAND_SPIRAL" + }, + "0x06": { + "key": "CYCLE_LEFT_RIGHT" + }, + "0x07": { + "key": "CYCLE_UP_DOWN" + }, + "0x08": { + "key": "CYCLE_OUT_IN" + }, + "0x09": { + "key": "DUAL_BEACON" + }, + "0x0A": { + "key": "WAVE_LEFT_RIGHT" + }, + "0x0B": { + "key": "WAVE_UP_DOWN" + }, + + "0x0C": { + "key": "SOLID_REACTIVE_SIMPLE", + "group": "reactive" + }, + "0x0D": { + "key": "SOLID_REACTIVE_WIDE", + "group": "reactive" + }, + "0x0E": { + "key": "SOLID_REACTIVE_MULTIWIDE", + "group": "reactive" + }, + "0x0F": { + "key": "SOLID_REACTIVE_CROSS", + "group": "reactive" + }, + "0x10": { + "key": "SOLID_REACTIVE_MULTICROSS", + "group": "reactive" + }, + "0x11": { + "key": "SOLID_REACTIVE_NEXUS", + "group": "reactive" + }, + "0x12": { + "key": "SOLID_REACTIVE_MULTINEXUS", + "group": "reactive" + }, + "0x13": { + "key": "SOLID_SPLASH", + "group": "reactive" + }, + "0x14": { + "key": "SOLID_MULTISPLASH", + "group": "reactive" + } + } +} diff --git a/data/constants/lighting/rgb_matrix_0.0.1.hjson b/data/constants/lighting/rgb_matrix_0.0.1.hjson new file mode 100644 index 000000000000..da8a960e768c --- /dev/null +++ b/data/constants/lighting/rgb_matrix_0.0.1.hjson @@ -0,0 +1,175 @@ +{ + "groups": { + "framebuffer": { + "define": "RGB_MATRIX_FRAMEBUFFER_EFFECTS" + }, + "reactive": { + "define": "RGB_MATRIX_KEYREACTIVE_ENABLED" + } + }, + "effects": { + "0x00": { + "key": "SOLID_COLOR" + }, + "0x01": { + "key": "ALPHAS_MODS" + }, + "0x02": { + "key": "GRADIENT_UP_DOWN" + }, + "0x03": { + "key": "GRADIENT_LEFT_RIGHT" + }, + "0x04": { + "key": "BREATHING" + }, + "0x05": { + "key": "BAND_SAT" + }, + "0x06": { + "key": "BAND_VAL" + }, + "0x07": { + "key": "BAND_PINWHEEL_SAT" + }, + "0x08": { + "key": "BAND_PINWHEEL_VAL" + }, + "0x09": { + "key": "BAND_SPIRAL_SAT" + }, + "0x0A": { + "key": "BAND_SPIRAL_VAL" + }, + "0x0B": { + "key": "CYCLE_ALL" + }, + "0x0C": { + "key": "CYCLE_LEFT_RIGHT" + }, + "0x0D": { + "key": "CYCLE_UP_DOWN" + }, + "0x0E": { + "key": "CYCLE_OUT_IN" + }, + "0x0F": { + "key": "CYCLE_OUT_IN_DUAL" + }, + "0x10": { + "key": "RAINBOW_MOVING_CHEVRON" + }, + "0x11": { + "key": "CYCLE_PINWHEEL" + }, + "0x12": { + "key": "CYCLE_SPIRAL" + }, + "0x13": { + "key": "DUAL_BEACON" + }, + "0x14": { + "key": "RAINBOW_BEACON" + }, + "0x15": { + "key": "RAINBOW_PINWHEELS" + }, + "0x16": { + "key": "RAINDROPS" + }, + "0x17": { + "key": "JELLYBEAN_RAINDROPS" + }, + "0x18": { + "key": "HUE_BREATHING" + }, + "0x19": { + "key": "HUE_PENDULUM" + }, + "0x1A": { + "key": "HUE_WAVE" + }, + "0x1B": { + "key": "PIXEL_FRACTAL" + }, + "0x1C": { + "key": "PIXEL_FLOW" + }, + "0x1D": { + "key": "PIXEL_RAIN" + }, + + "0x1E": { + "key": "TYPING_HEATMAP", + "group": "framebuffer" + }, + "0x1F": { + "key": "DIGITAL_RAIN", + "group": "framebuffer" + }, + + "0x20": { + "key": "SOLID_REACTIVE_SIMPLE", + "group": "reactive" + }, + "0x21": { + "key": "SOLID_REACTIVE", + "group": "reactive" + }, + "0x22": { + "key": "SOLID_REACTIVE_WIDE", + "group": "reactive" + }, + "0x23": { + "key": "SOLID_REACTIVE_MULTIWIDE", + "group": "reactive" + }, + "0x24": { + "key": "SOLID_REACTIVE_CROSS", + "group": "reactive" + }, + "0x25": { + "key": "SOLID_REACTIVE_MULTICROSS", + "group": "reactive" + }, + "0x26": { + "key": "SOLID_REACTIVE_NEXUS", + "group": "reactive" + }, + "0x27": { + "key": "SOLID_REACTIVE_MULTINEXUS", + "group": "reactive" + }, + "0x28": { + "key": "SPLASH", + "group": "reactive" + }, + "0x29": { + "key": "MULTISPLASH", + "group": "reactive" + }, + "0x2A": { + "key": "SOLID_SPLASH", + "group": "reactive" + }, + "0x2B": { + "key": "SOLID_MULTISPLASH", + "group": "reactive" + }, + "0x2C": { + "key": "FLOWER_BLOOMING" + }, + "0x2D": { + "key": "STARLIGHT" + }, + "0x2E": { + "key": "STARLIGHT_DUAL_SAT" + }, + "0x2F": { + "key": "STARLIGHT_DUAL_HUE" + }, + "0x30": { + "key": "RIVERFLOW" + } + } +} diff --git a/data/constants/lighting/rgblight_0.0.1.hjson b/data/constants/lighting/rgblight_0.0.1.hjson new file mode 100644 index 000000000000..e096628ae4da --- /dev/null +++ b/data/constants/lighting/rgblight_0.0.1.hjson @@ -0,0 +1,130 @@ +{ + "effects": { + "0x00": { + "key": "STATIC_LIGHT" + }, + "0x01": { + "key": "BREATHING" + }, + "0x02": { + "key": "BREATHING_2" + }, + "0x03": { + "key": "BREATHING_3" + }, + "0x04": { + "key": "BREATHING_4" + }, + "0x05": { + "key": "RAINBOW_MOOD" + }, + "0x06": { + "key": "RAINBOW_MOOD_2" + }, + "0x07": { + "key": "RAINBOW_MOOD_3" + }, + "0x08": { + "key": "RAINBOW_SWIRL" + }, + "0x09": { + "key": "RAINBOW_SWIRL_2" + }, + "0x0A": { + "key": "RAINBOW_SWIRL_3" + }, + "0x0B": { + "key": "RAINBOW_SWIRL_4" + }, + "0x0C": { + "key": "RAINBOW_SWIRL_5" + }, + "0x0D": { + "key": "RAINBOW_SWIRL_6" + }, + "0x0E": { + "key": "SNAKE" + }, + "0x0F": { + "key": "SNAKE_2" + }, + "0x10": { + "key": "SNAKE_3" + }, + "0x11": { + "key": "SNAKE_4" + }, + "0x12": { + "key": "SNAKE_5" + }, + "0x13": { + "key": "SNAKE_6" + }, + "0x14": { + "key": "KNIGHT" + }, + "0x15": { + "key": "KNIGHT_2" + }, + "0x16": { + "key": "KNIGHT_3" + }, + "0x17": { + "key": "CHRISTMAS" + }, + "0x18": { + "key": "STATIC_GRADIENT" + }, + "0x19": { + "key": "STATIC_GRADIENT_2" + }, + "0x1A": { + "key": "STATIC_GRADIENT_3" + }, + "0x1B": { + "key": "STATIC_GRADIENT_4" + }, + "0x1C": { + "key": "STATIC_GRADIENT_5" + }, + "0x1D": { + "key": "STATIC_GRADIENT_6" + }, + "0x1E": { + "key": "STATIC_GRADIENT_7" + }, + "0x1F": { + "key": "STATIC_GRADIENT_8" + }, + "0x20": { + "key": "STATIC_GRADIENT_9" + }, + "0x21": { + "key": "STATIC_GRADIENT_10" + }, + "0x22": { + "key": "RGB_TEST" + }, + "0x23": { + "key": "ALTERNATING" + }, + "0x24": { + "key": "TWINKLE" + }, + "0x25": { + "key": "TWINKLE_2" + }, + "0x26": { + "key": "TWINKLE_3" + }, + "0x27": { + "key": "TWINKLE_4" + }, + "0x28": { + "key": "TWINKLE_5" + }, + "0x29": { + "key": "TWINKLE_6" + } + } +} diff --git a/data/mappings/xap_defaults.json b/data/mappings/xap_defaults.json new file mode 100644 index 000000000000..9bf3461213df --- /dev/null +++ b/data/mappings/xap_defaults.json @@ -0,0 +1,7 @@ +{ + "secure": { + "unlock_sequence": [ [0,0] ], + "unlock_timeout": 5000, + "idle_timeout": 60000 + } +} \ No newline at end of file diff --git a/data/schemas/definitions.jsonschema b/data/schemas/definitions.jsonschema index 76ea8520ac98..47013be942a6 100644 --- a/data/schemas/definitions.jsonschema +++ b/data/schemas/definitions.jsonschema @@ -16,6 +16,18 @@ "type": "object", "additionalProperties": {"type": "boolean"} }, + "build_target": { + "oneOf": [ + {"$ref": "#/keyboard_keymap_tuple"}, + {"$ref": "#/json_file_path"} + ] + }, + "define": { + "type": "string", + "minLength": 2, + "maxLength": 50, + "pattern": "^[A-Z_]*$" + }, "filename": { "type": "string", "minLength": 1, @@ -187,6 +199,10 @@ "minLength": 1, "maxLength": 250 }, + "text_unsigned_int": { + "type": "string", + "pattern": "^[0-8]+$" + }, "unsigned_decimal": { "type": "number", "minimum": 0 diff --git a/data/schemas/xap.jsonschema b/data/schemas/xap.jsonschema new file mode 100644 index 000000000000..89000bb47f75 --- /dev/null +++ b/data/schemas/xap.jsonschema @@ -0,0 +1,255 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "qmk.xap.v1", + "title": "XAP Spec", + "definitions": { + "data_type": { + "oneOf": [ + { + "enum": [ + "bool", + "u8", + "u16", + "u32", + "u64", + "struct", + "string" + ] + }, + { + "type": "string", + "pattern": "^u\\d{1,2}\\[\\d{1,2}\\]*$" + } + ] + }, + "router_type": { + "enum": [ + "command", + "router" + ] + }, + "permission": { + "enum": [ + "secure" + ] + }, + "struct": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + }, + "route": { + "type": "object", + "propertyNames": { + "$ref": "qmk.definitions.v1#/hex_number_2d" + }, + "additionalProperties": { + "type": "object", + "required": [ + "type", + "define" + ], + "additionalProperties": false, + "properties": { + "type": { + "$ref": "#/definitions/router_type" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "define": { + "$ref": "qmk.definitions.v1#/define" + }, + "permissions": { + "$ref": "#/definitions/permission" + }, + "enable_if_preprocessor": { + "type": "string" + }, + "request_type": { + "$ref": "#/definitions/data_type" + }, + "request_struct_length": { + "type": "number" + }, + "request_purpose": { + "type": "string" + }, + "return_type": { + "$ref": "#/definitions/data_type" + }, + "request_struct_members": { + "$ref": "#definitions/struct" + }, + "return_struct_length": { + "type": "number" + }, + "return_constant": { + "type": [ + "array", + "string" + ] + }, + "return_struct_members": { + "$ref": "#definitions/struct" + }, + "return_purpose": { + "type": "string" + }, + "return_execute": { + "type": "string" + }, + "routes": { + "$ref": "#/definitions/route" + } + } + } + } + }, + "type": "object", + "additionalProperties": false, + "required": [ + "version" + ], + "properties": { + "version": { + "$ref": "qmk.definitions.v1#/bcd_version" + }, + "define": { + "$ref": "qmk.definitions.v1#/define" + }, + "documentation": { + "type": "object", + "properties": { + "order": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": { + "type": "string" + } + }, + "term_definitions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "type_docs": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "type_definitions": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/data_type" + }, + "struct_length": { + "type": "number" + }, + "struct_members": { + "$ref": "#definitions/struct" + } + } + } + }, + "response_flags": { + "type": "object", + "additionalProperties": false, + "properties": { + "define_prefix": { + "$ref": "qmk.definitions.v1#/define" + }, + "bits": { + "type": "object", + "propertyNames": { + "$ref": "qmk.definitions.v1#/text_unsigned_int" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "define": { + "$ref": "qmk.definitions.v1#/define" + }, + "description": { + "type": "string" + } + } + } + } + } + }, + "broadcast_messages": { + "type": "object", + "additionalProperties": false, + "properties": { + "define_prefix": { + "$ref": "qmk.definitions.v1#/define" + }, + "messages": { + "type": "object", + "propertyNames": { + "$ref": "qmk.definitions.v1#/hex_number_2d" + }, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "define": { + "$ref": "qmk.definitions.v1#/define" + }, + "description": { + "type": "string" + }, + "return_type": { + "$ref": "#/definitions/data_type" + } + } + } + } + } + }, + "routes": { + "$ref": "#/definitions/route" + } + } +} \ No newline at end of file diff --git a/data/templates/xap/client/python/constants.py.j2 b/data/templates/xap/client/python/constants.py.j2 new file mode 100644 index 000000000000..dcd653765d00 --- /dev/null +++ b/data/templates/xap/client/python/constants.py.j2 @@ -0,0 +1,19 @@ +{{ constants.GPL2_HEADER_SH_LIKE }} +{{ constants.GENERATED_HEADER_SH_LIKE }} +from enum import IntEnum + + +# version: 0.0.1 +class RgblightModes(IntEnum): +{% for id, effect in specs.rgblight.effects | dictsort %} + {{ effect.key }} = {{ id }} +{% endfor %} + + +# version: 0.0.1 +class RgbMatrixModes(IntEnum): +{% for id, effect in specs.rgb_matrix.effects | dictsort %} + {{ effect.key }} = {{ id }} +{% endfor %} + +# noqa: W391 \ No newline at end of file diff --git a/data/templates/xap/client/python/routes.py.j2 b/data/templates/xap/client/python/routes.py.j2 new file mode 100644 index 000000000000..51ce0d790077 --- /dev/null +++ b/data/templates/xap/client/python/routes.py.j2 @@ -0,0 +1,22 @@ +{{ constants.GPL2_HEADER_SH_LIKE }} +{{ constants.GENERATED_HEADER_SH_LIKE }} +class XAPRouteError(Exception): + pass + + +class XAPRoutes(): +{% for id, route in xap.routes | dictsort %} +{% if route.routes %} + # {{route.define}} +{% for subid, subroute in route.routes | dictsort %} + {{route.define}}_{{subroute.define}} = b'\x{{ '%02d' % id|int(base=16) }}\x{{ '%02d' % subid|int(base=16) }}' +{% if subroute.routes %} +{% for subsubid, subsubroute in subroute.routes | dictsort %} + {{route.define}}_{{subroute.define}}_{{subsubroute.define}} = b'\x{{ '%02d' % id|int(base=16) }}\x{{ '%02d' % subid|int(base=16) }}\x{{ '%02d' % subsubid|int(base=16) }}' +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} + +# noqa: W391 \ No newline at end of file diff --git a/data/templates/xap/client/python/types.py.j2 b/data/templates/xap/client/python/types.py.j2 new file mode 100644 index 000000000000..477121981efc --- /dev/null +++ b/data/templates/xap/client/python/types.py.j2 @@ -0,0 +1,68 @@ +{{ constants.GPL2_HEADER_SH_LIKE }} +{{ constants.GENERATED_HEADER_SH_LIKE }} +from collections import namedtuple +from enum import IntFlag, IntEnum +from struct import Struct + + +{% macro gen_struct(name, members, fmt) -%} +class {{ name }}(namedtuple('{{ name }}', '{{ members }}')): + fmt = Struct('{{ fmt }}') + + def __new__(cls, *args): + return super().__new__(cls, *args) + + @classmethod + def from_bytes(cls, data): + return cls._make(cls.fmt.unpack(data)) + + def to_bytes(self): + return self.fmt.pack(*list(self)) + + +{% endmacro -%} + +{% set type_definitions = [ + {'name':'XAPRequest', 'members': 'token length data', 'fmt':'{{ " "|safe*4 }}* {{ member.name }}: `{{ member.type }}` +{%- endfor -%} +{%- elif purpose -%} +__{{ name }}:__
{{ " "|safe*4 }}* {{ purpose }}: `{{ type }}` +{%- elif type -%} +__{{ name }}:__ `{{ type }}` +{%- endif -%} +{%- endmacro -%} + +{%- macro gen_payloads(route) -%} +{{ gen_payload('Request', route.request_type, route.request_purpose, route.request_struct_members) }}{%- if route.return_type and route.request_type -%}

{% endif %}{{ gen_payload('Response', route.return_type, null, route.return_struct_members) }} +{%- endmacro -%} + +{%- macro gen_tags(route) -%} +{% if 'secure' == route.permissions %}__Secure__{% endif %} +{%- endmacro -%} + +{% for id, route in xap.routes | dictsort %} +### {{ route.name }} - `{{ id }}` +{{ route.description }} + +{% if route.routes %} +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +{% for subid, subroute in route.routes | dictsort %} +{% if not subroute.routes %} +| {{ subroute.name }} | `{{ id }} {{ subid }}` | {{ gen_tags(subroute) }} | {{ gen_payloads(subroute) }} | {{ subroute.description | newline_to_br }}| +{% endif %} +{% endfor %} + +{% for subid, subroute in route.routes | dictsort %} +{%- if subroute.routes %} + +#### {{ subroute.name }} - `{{ id }} {{ subid }}` +{{ subroute.description }} + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +{% for subsubid, subsubroute in subroute.routes | dictsort %} +{% if not subsubroute.routes %} +| {{ subsubroute.name }} | `{{ id }} {{ subid }} {{ subsubid }}` | {{ gen_tags(subsubroute) }} | {{ gen_payloads(subsubroute) }} | {{ subsubroute.description | newline_to_br }}| +{% endif %} +{% endfor %} + +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} diff --git a/data/templates/xap/docs/term_definitions.md.j2 b/data/templates/xap/docs/term_definitions.md.j2 new file mode 100644 index 000000000000..710a9890e975 --- /dev/null +++ b/data/templates/xap/docs/term_definitions.md.j2 @@ -0,0 +1,8 @@ +| Name | Definition | +| -- | -- | +{% for type, definition in xap.term_definitions | dictsort %} +| _{{ type }}_ | {{ definition }} | +{% endfor %} +{% for type, definition in xap.type_definitions | dictsort %} +| _{{ definition.name }}_ | {{ definition.description }}{% if 'struct' == definition.type %} Takes the format:{% for item in definition.struct_members %}
`{{ item.type }}` - {{ item.name }}{%- endfor %}{% endif %} | +{% endfor %} \ No newline at end of file diff --git a/data/templates/xap/docs/type_docs.md.j2 b/data/templates/xap/docs/type_docs.md.j2 new file mode 100644 index 000000000000..4700681274cb --- /dev/null +++ b/data/templates/xap/docs/type_docs.md.j2 @@ -0,0 +1,5 @@ +| Name | Definition | +| -- | -- | +{% for type, definition in xap.type_docs | dictsort %} +| _{{ type }}_ | {{ definition }} | +{% endfor %} \ No newline at end of file diff --git a/data/templates/xap/docs/versions.md.j2 b/data/templates/xap/docs/versions.md.j2 new file mode 100644 index 000000000000..4d91913abe39 --- /dev/null +++ b/data/templates/xap/docs/versions.md.j2 @@ -0,0 +1,6 @@ +{{ constants.GPL2_HEADER_XML_LIKE }} +{{ constants.GENERATED_HEADER_XML_LIKE }} + +{%- for ver in versions | reverse -%} +* [XAP Version {{ ver }}](xap_{{ ver }}.md) +{% endfor %} diff --git a/data/templates/xap/firmware/xap_generated.h.j2 b/data/templates/xap/firmware/xap_generated.h.j2 new file mode 100755 index 000000000000..22f0942106eb --- /dev/null +++ b/data/templates/xap/firmware/xap_generated.h.j2 @@ -0,0 +1,171 @@ +{{ constants.GPL2_HEADER_C_LIKE }} +{{ constants.GENERATED_HEADER_C_LIKE }} +#pragma once + +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +// Versions and identifiers + +#define XAP_BCD_VERSION UINT32_C({{ xap.version | triplet_to_bcd }}) +#define QMK_BCD_VERSION UINT32_C({{ qmk_version | triplet_to_bcd }}) +#define XAP_KEYBOARD_IDENTIFIER UINT32_C({{ keyboard | fnv1a_32 }}) + + +//////////////////////////////////////////////////////////////////////////////// +// Response flag definitions + +{% for bit,data in xap.response_flags.bits | dictsort %} +#define {{ xap.response_flags.define_prefix }}_{{ data.define | to_snake | upper }} (UINT32_C(1) << ({{ bit }})) +{% endfor %} +#define {{ xap.response_flags.define_prefix }}_FAILED 0x00 + + +//////////////////////////////////////////////////////////////////////////////// +// Broadcast message definitions + +{% for message_id,data in xap.broadcast_messages.messages | dictsort %} +#define {{ xap.broadcast_messages.define_prefix }}_{{ data.define | to_snake | upper }} {{ message_id }} +{% if 'return_type' in data %} +void {{ xap.broadcast_messages.define_prefix | lower }}_{{ data.define | to_snake | lower }}({{ data.return_type | type_to_c('value') }}); +{% else %} +void {{ xap.broadcast_messages.define_prefix | lower }}_{{ data.define | to_snake | lower }}(const void *data, size_t length); +{% endif %} +{% endfor %} +#define XAP_BROADCAST_TOKEN 0xFFFF + + +//////////////////////////////////////////////////////////////////////////////// +// Type definitions + +{% for name,data in xap.type_definitions | dictsort %} +{% if data.type != 'struct' %} +typedef {{ data.type | type_to_c('xap_'+(name|to_snake|lower)+'_t') }}; +{% endif %} +{% endfor %} +{% for name,data in xap.type_definitions | dictsort %} +{% if data.type == 'struct' %} +typedef struct { +{% for member in data.struct_members %} + {{ member.type | type_to_c(member.name) }}; +{% endfor %} +} __attribute__((__packed__)) xap_{{ name | to_snake | lower }}_t{{ data.type | type_to_c_after }}; +_Static_assert(sizeof(xap_{{ name | to_snake | lower }}_t) == {{ data.struct_length }}, "xap_{{ name | to_snake | lower }}_t needs to be {{ data.struct_length }} bytes in size"); +{% endif %} +{% endfor %} + +//////////////////////////////////////////////////////////////////////////////// +// Route definitions + +{% macro export_route_types(prefix, container) %} +{% if 'routes' in container %} +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} +{% set this_prefix_lc = this_prefix_uc | lower %} + +{% if 'request_struct_members' in data %} +typedef struct { +{% for member in data.request_struct_members %} + {{ member.type | type_to_c(member.name|lower) }}; +{% endfor %} +} __attribute__((__packed__)) {{ this_prefix_lc | to_snake | lower }}_arg_t; +_Static_assert(sizeof({{ this_prefix_lc | to_snake | lower }}_arg_t) == {{ data.request_struct_length }}, "{{ this_prefix_lc | to_snake | lower }}_arg_t needs to be {{ data.request_struct_length }} bytes in size"); +{% elif 'request_type' in data %} +{% if '[' in data.request_type %} +typedef struct __attribute__((__packed__)) { {{ data.request_type | type_to_c('x') }}; } {{ this_prefix_lc }}_arg_t; +{% else %} +typedef {{ data.request_type | type_to_c(this_prefix_lc+'_arg_t') }}; +{% endif %} +{% endif %} + +{% if 'return_struct_members' in data %} +typedef struct { +{% for member in data.return_struct_members %} + {{ member.type | type_to_c(member.name|lower) }}; +{% endfor %} +} __attribute__((__packed__)) {{ this_prefix_lc | to_snake | lower }}_t; +_Static_assert(sizeof({{ this_prefix_lc | to_snake | lower }}_t) == {{ data.return_struct_length }}, "{{ this_prefix_lc | to_snake | lower }}_t needs to be {{ data.return_struct_length }} bytes in size"); +{% elif 'return_type' in data %} +{% if '[' in data.return_type %} +typedef struct __attribute__((__packed__)) { {{ data.return_type | type_to_c('x') }}; } {{ this_prefix_lc }}_t; +{% else %} +typedef {{ data.return_type | type_to_c(this_prefix_lc+'_t') }}; +{% endif %} +{% endif %} + +{{ export_route_types(this_prefix_lc, data) }} +{% endfor %} +{% endif %} +{% endmacro %} + +{{ export_route_types('xap_route', xap) }} + +//////////////////////////////////////////////////////////////////////////////// +// Capabilities IDs + +{% macro export_route_ids(prefix, container) %} +{% if 'routes' in container %} +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} +{% set this_prefix_lc = this_prefix_uc | lower %} +#define {{ this_prefix_uc }} {{ route }} +{{ export_route_ids(this_prefix_uc, data) }} +{% endfor %} +{% endif %} +{% endmacro %} + +{{ export_route_ids('XAP_ROUTE', xap) }} + +//////////////////////////////////////////////////////////////////////////////// +// Capabilities Masks + +{% macro export_route_masks(prefix, container, preprocessor_condition) %} +{% if 'routes' in container %} +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} +{% set this_prefix_lc = this_prefix_uc | lower %} +{% if 'enable_if_preprocessor' in data %} +{% if preprocessor_condition == 'TRUE' %} +{% set condition = "(" + data.enable_if_preprocessor + ")" %} +{% else %} +{% set condition = "(" + preprocessor_condition + " && (" + data.enable_if_preprocessor + "))" %} +{% endif %} +{% else %} +{% set condition = preprocessor_condition %} +{% endif %} +{% if condition == 'TRUE' %} +#define {{ this_prefix_uc }}_MASK (UINT32_C(1) << ({{ this_prefix_uc }})) +{% else %} +#if ({{ condition }}) +#define {{ this_prefix_uc }}_MASK (UINT32_C(1) << ({{ this_prefix_uc }})) +#else // ({{ condition }}) +#define {{ this_prefix_uc }}_MASK 0 +#endif // ({{ condition }}) +{% endif %} +{{ export_route_masks(this_prefix_uc, data, condition) }} +{% endfor %} +{% endif %} +{% endmacro %} + +{{ export_route_masks('XAP_ROUTE', xap, 'TRUE') }} + +//////////////////////////////////////////////////////////////////////////////// +// Capabilities Values + +{% macro export_route_capabilities(prefix, container) %} +{% if 'routes' in container %} +#define {{ prefix }}_CAPABILITIES (0 \ +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} + | ({{ this_prefix_uc }}_MASK) \ +{% endfor %} + ) +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} +{{ export_route_capabilities(this_prefix_uc, data) }} +{% endfor %} +{% endif %} +{% endmacro %} + +{{ export_route_capabilities('XAP_ROUTE', xap) }} diff --git a/data/templates/xap/firmware/xap_generated.inl.j2 b/data/templates/xap/firmware/xap_generated.inl.j2 new file mode 100755 index 000000000000..208b977a97c7 --- /dev/null +++ b/data/templates/xap/firmware/xap_generated.inl.j2 @@ -0,0 +1,166 @@ +{{ constants.GPL2_HEADER_C_LIKE }} +{{ constants.GENERATED_HEADER_C_LIKE }} + +{% macro route_conditions(route_stack) %} +{% set conditions = [] %} +{% for data in route_stack %} +{% if 'enable_if_preprocessor' in data %} +{{ conditions.append(data.enable_if_preprocessor) or '' }} +{% endif %} +{% endfor %} + +{% if conditions %} +#if ({{ conditions | join(' && ') }}) +{% endif %} +{{ caller() }} +{%- if conditions %} +#endif // ({{ conditions | join(' && ') }}) +{% endif %} +{% endmacro %} + +//////////////////////////////////////////////////////////////////////////////// +// Broadcast + +{% for message_id,data in xap.broadcast_messages.messages | dictsort %} +{% if 'return_type' in data %} +void {{ xap.broadcast_messages.define_prefix | lower }}_{{ data.define | to_snake | lower }}({{ data.return_type | type_to_c('value') }}) { xap_broadcast({{ message_id }}, &value, sizeof(value)); } +{% else %} +void {{ xap.broadcast_messages.define_prefix | lower }}_{{ data.define | to_snake | lower }}(const void *data, size_t length) { xap_broadcast({{ message_id }}, data, length); } +{% endif %} +{% endfor %} + +//////////////////////////////////////////////////////////////////////////////// +// Decl + +{% macro export_route_declaration(prefix, container) %} +{% if 'routes' in container %} +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} +{% set this_prefix_lc = this_prefix_uc | lower %} +{% if 'return_execute' in data %} + +{% if 'request_struct_members' in data %} +{% set arg_type = ( this_prefix_lc | to_snake | lower ) + '_arg_t' %} +{% set arg_var = arg_type + ' arg' %} +{% elif 'request_type' in data %} +{% set arg_type = data.request_type | type_to_c() %} +{% set arg_var = data.request_type | type_to_c('arg') %} +{% endif %} + +__attribute__((weak)) bool xap_execute_{{ data.return_execute }}(xap_token_t token{% if arg_type %}, {{ (arg_type + '* arg') if 'xap_route' in arg_type else arg_var }}{% endif %}) { return false; } +__attribute__((weak)) bool xap_respond_{{ data.return_execute }}(xap_token_t token, const uint8_t *data, size_t data_len) { +{% if arg_type %} + if (data_len != sizeof({{ arg_type }})) { + return false; + } + {{ arg_var }}; + memcpy(&arg, data, sizeof({{ arg_type }})); +{% endif %} + + return xap_execute_{{ data.return_execute }}(token{% if arg_type %}, {{ '&' if 'xap_route' in arg_type else '' }}arg{% endif %}); +} +{% endif %} +{{ export_route_declaration(this_prefix_lc, data) }} +{% endfor %} +{% endif %} +{% endmacro %} + +{{ export_route_declaration('xap_route', xap) }} + +//////////////////////////////////////////////////////////////////////////////// +// Data + +{% macro export_route_data(prefix, container, route_stack) %} +{% set this_route_stack = route_stack.copy() %} +{{ this_route_stack.append(container) or '' }} +{% if 'routes' in container %} + {% for route, data in container.routes | dictsort %} + {% set this_prefix_uc = (prefix + '_' + data.define) | upper %} + {% set this_prefix_lc = this_prefix_uc | lower %} + {% if 'return_constant' in data %} + {% if data.return_type == 'struct' %} +{% call route_conditions(this_route_stack) %} +static const {{ this_prefix_lc }}_t {{ this_prefix_lc }}_data PROGMEM = { +{% for member in data.return_constant %} + {{ member }}, +{% endfor %} +}; +{% endcall %} + {% elif data.return_type == 'string' %} +{% call route_conditions(this_route_stack) %} +static const char {{ this_prefix_lc }}_str[] PROGMEM = {{ data.return_constant }}; +{% endcall %} + {% else %} +{% call route_conditions(this_route_stack) %} +static const {{ data.return_type | type_to_c_before }} {{ this_prefix_lc }}_data PROGMEM = {{ data.return_constant }}; +{% endcall %} + {% endif %} + {% endif %} +{{ export_route_data(this_prefix_lc, data, this_route_stack) }} + {% endfor %} +{% endif %} +{% endmacro %} + +{{ export_route_data('XAP_ROUTE', xap, []) }} + +//////////////////////////////////////////////////////////////////////////////// +// Routes + +{% macro append_routing_table(prefix, container, route_stack) %} +{% set this_route_stack = route_stack.copy() %} +{{ this_route_stack.append(container) or '' }} +{% if 'routes' in container %} +{% for route, data in container.routes | dictsort %} +{% set this_prefix_uc = (prefix + '_' + data.define) | upper %} +{% set this_prefix_lc = this_prefix_uc | lower %} +{{ append_routing_table(this_prefix_lc, data, this_route_stack) }} +{% endfor %} +{% call route_conditions(this_route_stack) %} +static const xap_route_t {{ prefix | lower}}_table[] PROGMEM = { +{% for route, data in container.routes | dictsort %} +{% set inner_route_stack = this_route_stack.copy() %} +{{ inner_route_stack.append(data) or '' }} +{% if 'permissions' in data %} +{% set secure_status = 'ROUTE_PERMISSIONS_SECURE' %} +{% else %} +{% set secure_status = 'ROUTE_PERMISSIONS_INSECURE' %} +{% endif %} +{% call route_conditions(inner_route_stack) %} + [{{ prefix | upper }}_{{ data.define }}] = { +{% if 'routes' in data %} + .flags = { + .type = XAP_ROUTE, + .secure = {{ secure_status }}, + }, + .child_routes = {{ prefix | lower }}_{{ data.define | lower }}_table, + .child_routes_len = sizeof({{ prefix | lower }}_{{ data.define | lower }}_table)/sizeof(xap_route_t), +{% elif 'return_execute' in data %} + .flags = { + .type = XAP_EXECUTE, + .secure = {{ secure_status }}, + }, + .handler = xap_respond_{{ data.return_execute | lower }}, +{% elif 'return_constant' in data and data.return_type == 'string' %} + .flags = { + .type = XAP_CONST_MEM, + .secure = {{ secure_status }}, + }, + .const_data = {{ prefix | lower }}_{{ data.define | lower }}_str, + .const_data_len = sizeof({{ prefix | lower }}_{{ data.define | lower }}_str) - 1, +{% elif 'return_constant' in data %} + .flags = { + .type = XAP_CONST_MEM, + .secure = {{ secure_status }}, + }, + .const_data = &{{ prefix | lower }}_{{ data.define | lower }}_data, + .const_data_len = sizeof({{ prefix | lower }}_{{ data.define | lower }}_data), +{% endif %} + }, +{% endcall %} +{% endfor %} +}; +{% endcall %} +{% endif %} +{% endmacro %} + +{{ append_routing_table("xap_route", xap, []) }} diff --git a/data/xap/xap_0.0.1.hjson b/data/xap/xap_0.0.1.hjson new file mode 100755 index 000000000000..aa34d7828d38 --- /dev/null +++ b/data/xap/xap_0.0.1.hjson @@ -0,0 +1,237 @@ +{ + version: 0.0.1 + + // Needed for table generation + define: XAP_ROUTE + + // Documentation section is used purely for `qmk xap-generate-docs`. + documentation: { + order: [ + page_header + type_docs + !type_docs.md.j2 + term_definitions + !term_definitions.md.j2 + request_response + reserved_tokens + response_flags + !response_flags.md.j2 + example_conversation + routes + !routes.md.j2 + ] + + page_header: + ''' + # QMK Firmware XAP Specs + + This document describes the requirements of the QMK XAP ("extensible application protocol") API. + ''' + + type_docs: + ''' + ## Types + + **All integral types are little-endian.** + ''' + + term_definitions: + ''' + ## Definitions + + This list defines the terms used across the entire set of XAP protocol documentation. + ''' + + request_response: + ''' + ## Requests and Responses + + Communication generally follows a request/response pattern. + + Each request needs to include a _token_ -- this `u16` value prefixes each outbound request from the host application and its corresponding response. + This allows response messages to be correlated with their request, even if multiple host applications are communicating with the firmware simultaneously. + Host applications should randomly generate a token ID for **every** outbound request, unless using a reserved token defined below. + To ensure host interoperability, valid token values are within the range `0x0100`-`0xFFFF`. + + This token is followed by a `u8` signifying the length of data in the request. + ''' + + // This documentation section reserved for next version + reserved_tokens: '' + + response_flags: + ''' + Response messages will always be prefixed by the originating request _token_, directly followed by that request's _response flags_, then the response payload length: + ''' + + example_conversation: + ''' + ### Example "conversation": + + **Request** -- version query: + + | Byte | 0 | 1 | 2 | 3 | 4 | + | --- | --- | --- | --- | --- | --- | + | **Purpose** | Token | Token | Payload Length | Route | Route | + | **Value** | `0x43` | `0x2B` | `0x02` | `0x00` | `0x00` | + + **Response** -- matching token, successful flag, payload of `0x03170192` = 3.17.192: + + | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | + | --- | --- | --- | --- | --- | --- | --- | --- | --- | + | **Purpose** | Token | Token | Response Flags | Payload Length | Payload | Payload | Payload | Payload | + | **Value** | `0x43` | `0x2B` | `0x01` | `0x04` | `0x92` | `0x01` | `0x17` | `0x03` | + ''' + + routes: + ''' + ## Routes + + Subsystem validity should be queried through the “Enabled-in-firmware subsystem query” under the QMK subsystem (route=0x00,0x01). + This is the primary method for determining if a subsystem has been enabled in the running firmware. + ''' + } + + type_docs: { + u8: + ''' + An unsigned 8-bit integral (octet, or byte), commonly seen as `uint8_t` from _stdint.h_. + ''' + u16: + ''' + An unsigned 16-bit integral, commonly seen as `uint16_t` from _stdint.h_. + ''' + u32: + ''' + An unsigned 32-bit integral, commonly seen as `uint32_t` from _stdint.h_. + ''' + "type[n]": + ''' + An array of `type`, with array extent of `N` -- e.g. `u8[2]` signifies two consecutive octets. + ''' + } + + term_definitions: { + Subsystem: + ''' + A high-level area of functionality within XAP. + ''' + Route: + ''' + A sequence of _IDs_ describing the route to invoke a _handler_. + ''' + Handler: + ''' + A piece of code that is executed when a specific _route_ is received. + ''' + Response: + ''' + The data sent back to the host during execution of a _handler_. + ''' + Payload: + ''' + Any received data appended to the _route_, which gets delivered to the _handler_ when received. + ''' + } + + type_definitions: { + identifier: { + name: ID + description: A single octet / 8-bit byte, representing Subsystem or Route index. + type: u8 + } + + response_flags: { + name: Response Flags + description: An `u8` containing the status of the request. + type: u8 + } + + token: { + name: Token + description: A `u16` associated with a specific request as well as its corresponding response. Valid token values are within the range `0x0100`-`0xFFFF`. + type: u16 + } + + request_header: { + name: Request Header + description: Packet format for inbound data. + type: struct + struct_length: 3 + struct_members: [ + { + type: token + name: token + }, + { + type: u8 + name: length + } + ] + } + + response_header: { + name: Response Header + description: Packet format for outbound data. + type: struct + struct_length: 4 + struct_members: [ + { + type: token + name: token + }, + { + type: response_flags + name: flags + }, + { + type: u8 + name: length + } + ] + } + } + + response_flags: { + define_prefix: XAP_RESPONSE_FLAG + bits: { + 0: { + name: Success + define: SUCCESS + description: + ''' + When this bit is set, the request was successfully handled. If not set, all payload data should be disregarded, and the request retried if appropriate (with a new token). + ''' + } + } + } + + routes: { + 0x00: { + type: router + name: XAP + define: XAP + description: + ''' + This subsystem is always present, and provides the ability to query information about the XAP protocol of the connected device. + ''' + routes: { + 0x00: { + type: command + name: Version Query + define: VERSION_QUERY + description: + ''' + XAP protocol version query. + + * Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ` + * e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}. + ''' + return_type: u32 + return_purpose: bcd-version + return_constant: XAP_BCD_VERSION + } + } + } + } +} diff --git a/data/xap/xap_0.1.0.hjson b/data/xap/xap_0.1.0.hjson new file mode 100755 index 000000000000..932d6d09a03a --- /dev/null +++ b/data/xap/xap_0.1.0.hjson @@ -0,0 +1,385 @@ +{ + version: 0.1.0 + + documentation: { + order: [ + broadcast_messages + !broadcast_messages.md.j2 + ] + + reserved_tokens: + ''' + Two token values are reserved: `0xFFFE` and `0xFFFF`: + * `0xFFFE`: A message sent by a host application may use this token if no response is to be sent -- a "fire and forget" message. + * `0xFFFF`: Signifies a "broadcast" message sent by the firmware without prompting from the host application. Broadcast messages are defined later in this document. + + Any request will generate at least one corresponding response, with the exception of messages using reserved tokens. Maximum total message length is 128 bytes due to RAM constraints. + ''' + + broadcast_messages: + ''' + ## Broadcast messages + + Broadcast messages may be sent by the firmware to the host, without a corresponding inbound request. Each broadcast message uses the token `0xFFFF`, and does not expect a response from the host. Tokens are followed by an _ID_ signifying the type of broadcast, then the response _payload_ length, and finally the corresponding _payload_. + ''' + } + + response_flags: { + bits: { + 1: { + name: Secure Failure + define: SECURE_FAILURE + description: + ''' + When this bit is set, the requested _route_ was marked _secure_ but an _unlock sequence_ has not completed. + ''' + } + } + } + + type_docs: { + bool: + ''' + Data type that contains values 0 and 1. Implemented as an alias of `u8`. + ''' + u64: + ''' + An unsigned 64-bit integral, commonly seen as `uint64_t` from _stdint.h_. + ''' + "struct{}": + ''' + A structure of data, packing different objects together. Data is "compacted" -- there are no padding bytes between fields. Equivalent to a packed C-style `struct`. The order in which they're defined matches the order of the data in the response packet. + ''' + } + + term_definitions: { + Capability: + ''' + A way to determine if certain functionality is enabled in the firmware. Any _subsystem_ that provides build-time restriction of functionality must provide a _route_ for a _capabilities query_. + ''' + "Secure Route": + ''' + A _route_ which has potentially destructive consequences, necessitating prior approval by the user before executing. + ''' + "Unlock sequence": + ''' + A physical sequence initiated by the user to enable execution of _secure routes_. + ''' + } + + type_definitions: { + broadcast_header: { + name: Broadcast Header + description: Packet format for broadcast messages. + type: struct + struct_length: 4 + struct_members: [ + { + type: token + name: token + }, + { + type: u8 + name: type + }, + { + type: u8 + name: length + } + ] + } + } + + broadcast_messages: { + define_prefix: XAP_BROADCAST + messages: { + 0x00: { + name: Log message + define: LOG_MESSAGE + description: + ''' + Replicates and replaces the same functionality as if using the standard QMK `CONSOLE_ENABLE = yes` in `rules.mk`. Normal prints within the firmware will manifest as log messages broadcast to the host. `hid_listen` will not be functional with XAP enabled. + + Log message payloads include `u8[Length]` containing the text, where the length of the text is the _broadcast_header.length_ field. + + **Example Log Broadcast** -- log message "Hello QMK!" + + | Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | + | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | + | **Purpose** | Token | Token | Broadcast Type | Length | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | + | **Value** | `0xFF` | `0xFF` | `0x00` | `0x0A`(10) | `0x48`(H) | `0x65`(e) | `0x6C`(l) | `0x6C`(l) | `0x6F`(o) | `0x20`( ) | `0x51`(Q) | `0x4D`(M) | `0x4B`(K) | `0x21`(!) | + ''' + } + 0x01: { + name: Secure Status + define: SECURE_STATUS + description: + ''' + Secure status has changed. Payloads include a `u8` matching a 'Secure Status' request. + + **Example Secure Status Broadcast** -- secure "Unlocking" + + | Byte | 0 | 1 | 2 | 3 | 4 | + | --- | --- | --- | --- | --- | --- | + | **Purpose** | Token | Token | Broadcast Type | Length | Secure Status | + | **Value** | `0xFF` | `0xFF` | `0x01` | `0x01` | `0x01` | + ''' + return_type: u8 + } + 0x02: { + name: Keyboard + define: KB + description: + ''' + Reserved for vendor-specific functionality. No messages are defined by XAP. + ''' + }, + + 0x03: { + name: User + define: USER + description: + ''' + Reserved for user-specific functionality. No messages are defined by XAP. + ''' + } + } + } + + routes: { + 0x00: { + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + XAP subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_XAP_CAPABILITIES + } + 0x02: { + type: command + name: Enabled subsystem query + define: SUBSYSTEM_QUERY + description: + ''' + XAP protocol subsystem query. Each bit should be considered as a "usable" subsystem. For example, checking `(value & (1 << XAP_ROUTE_QMK) != 0)` means the QMK subsystem is enabled and available for querying. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_CAPABILITIES + } + 0x03: { + type: command + name: Secure Status + define: SECURE_STATUS + description: + ''' + Query secure route status + + * 0 means secure routes are disabled + * 1 means unlock sequence initiated but incomplete + * 2 means secure routes are allowed + * any other value should be interpreted as disabled + ''' + return_type: u8 + return_execute: secure_status + } + 0x04: { + type: command + name: Secure Unlock + define: SECURE_UNLOCK + description: Initiate secure route unlock sequence + return_execute: secure_unlock + } + 0x05: { + type: command + name: Secure Lock + define: SECURE_LOCK + description: Disable secure routes + return_execute: secure_lock + } + } + }, + + 0x01: { + type: router + name: QMK + define: QMK + description: + ''' + This subsystem is always present, and provides the ability to address QMK-specific functionality. + ''' + routes: { + 0x00: { + type: command + name: Version Query + define: VERSION_QUERY + description: + ''' + QMK protocol version query. + + * Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ` + * e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}. + ''' + return_type: u32 + return_purpose: bcd-version + return_constant: QMK_BCD_VERSION + } + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + QMK subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_QMK_CAPABILITIES + } + 0x02: { + type: command + name: Board identifiers + define: BOARD_IDENTIFIERS + description: + ''' + Retrieves the set of identifying information for the board. + ''' + return_type: struct + return_struct_length: 10 + return_struct_members: [ + { + type: u16 + name: Vendor ID + }, + { + type: u16 + name: Product ID + }, + { + type: u16 + name: Product Version + }, + { + type: u32 + name: QMK Unique Identifier + } + ] + return_constant: [ + VENDOR_ID + PRODUCT_ID + DEVICE_VER + XAP_KEYBOARD_IDENTIFIER + ] + } + 0x03: { + type: command + name: Board Manufacturer + define: BOARD_MANUFACTURER + description: Retrieves the name of the manufacturer + return_type: string + return_constant: QSTR(MANUFACTURER) + } + 0x04: { + type: command + name: Product Name + define: PRODUCT_NAME + description: Retrieves the product name + return_type: string + return_constant: QSTR(PRODUCT) + } + 0x05: { + type: command + name: Config Blob Length + define: CONFIG_BLOB_LEN + description: Retrieves the length of the configuration data bundled within the firmware + return_type: u16 + return_constant: CONFIG_BLOB_GZ_LEN + } + 0x06: { + type: command + name: Config Blob Chunk + define: CONFIG_BLOB_CHUNK + description: Retrieves a chunk of the configuration data bundled within the firmware + request_type: u16 + request_purpose: offset + return_type: u8[32] + return_execute: get_config_blob_chunk + } + 0x07: { + type: command + name: Jump to bootloader + define: BOOTLOADER_JUMP + permissions: secure + enable_if_preprocessor: defined(BOOTLOADER_JUMP_SUPPORTED) + description: + ''' + Jump to bootloader + + May not be present - if QMK capabilities query returns “true”, then jump to bootloader is supported + + * 0 means secure routes are disabled, and should be considered as a failure + * 1 means successful, board will jump to bootloader + ''' + return_type: u8 + return_execute: request_bootloader_jump + } + 0x08: { + type: command + name: Hardware Identifier + define: HARDWARE_ID + description: Retrieves a unique identifier for the board. + return_type: u32[4] + return_execute: get_hardware_id + } + 0x09: { + type: command + name: Reinitialize EEPROM + define: EEPROM_RESET + permissions: secure + enable_if_preprocessor: !defined(NO_RESET) + description: + ''' + Reinitializes the keyboard's EEPROM (persistent memory) + + May not be present - if QMK capabilities query returns “true”, then reinitialize is supported + + * 0 means secure routes are disabled, and should be considered as a failure + * 1 means successful, board will reinitialize and then reboot + ''' + return_type: u8 + return_execute: request_eeprom_reset + } + } + }, + + 0x02: { + type: router + name: Keyboard + define: KB + description: + ''' + This subsystem is always present, and reserved for vendor-specific functionality. No routes are defined by XAP. + ''' + routes: { + } + }, + + 0x03: { + type: router + name: User + define: USER + description: + ''' + This subsystem is always present, and reserved for user-specific functionality. No routes are defined by XAP. + ''' + routes: { + } + } + } +} diff --git a/data/xap/xap_0.2.0.hjson b/data/xap/xap_0.2.0.hjson new file mode 100755 index 000000000000..5411e35b6213 --- /dev/null +++ b/data/xap/xap_0.2.0.hjson @@ -0,0 +1,200 @@ +{ + version: 0.2.0 + + routes: { + 0x04: { + type: router + name: Keymap + define: KEYMAP + description: + ''' + This subsystem allows for query of currently configured keycodes. + ''' + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + Keymap subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_KEYMAP_CAPABILITIES + } + 0x02: { + type: command + name: Get Layer Count + define: GET_LAYER_COUNT + description: Query maximum number of layers that can be addressed within the keymap. + return_type: u8 + return_execute: keymap_get_layer_count + } + 0x03: { + type: command + name: Get Keycode + define: GET_KEYMAP_KEYCODE + description: Query the Keycode at the requested location. + request_type: struct + request_struct_length: 3 + request_struct_members: [ + { + type: u8 + name: Layer + }, + { + type: u8 + name: Row + }, + { + type: u8 + name: Column + } + ] + return_type: u16 + return_execute: get_keymap_keycode + } + 0x04: { + type: command + name: Get Encoder Keycode + define: GET_ENCODER_KEYCODE + description: Query the Keycode at the requested location. + enable_if_preprocessor: defined(ENCODER_MAP_ENABLE) + request_type: struct + request_struct_length: 3 + request_struct_members: [ + { + type: u8 + name: Layer + }, + { + type: u8 + name: Encoder + }, + { + type: u8 + name: Clockwise + } + ] + return_type: u16 + return_execute: get_encoder_keycode + } + } + } + + 0x05: { + type: router + name: Remapping + define: REMAPPING + description: + ''' + This subsystem allows for live reassignment of keycodes without rebuilding the firmware. + ''' + enable_if_preprocessor: defined(DYNAMIC_KEYMAP_ENABLE) + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + Remapping subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_REMAPPING_CAPABILITIES + } + 0x02: { + type: command + name: Get Layer Count + define: GET_DYNAMIC_LAYER_COUNT + description: Query maximum number of layers that can be addressed within the keymap. + return_type: u8 + return_constant: DYNAMIC_KEYMAP_LAYER_COUNT + } + 0x03: { + type: command + name: Set Keycode + define: SET_KEYMAP_KEYCODE + description: Modify the Keycode at the requested location. + permissions: secure + request_type: struct + request_struct_length: 5 + request_struct_members: [ + { + type: u8 + name: Layer + }, + { + type: u8 + name: Row + }, + { + type: u8 + name: Column + }, + { + type: u16 + name: Keycode + } + ] + return_execute: dynamic_keymap_set_keycode + } + 0x04: { + type: command + name: Set Encoder Keycode + define: SET_ENCODER_KEYCODE + permissions: secure + description: Modify the Keycode at the requested location. + enable_if_preprocessor: defined(ENCODER_MAP_ENABLE) + request_type: struct + request_struct_length: 5 + request_struct_members: [ + { + type: u8 + name: Layer + }, + { + type: u8 + name: Encoder + }, + { + type: u8 + name: Clockwise + }, + { + type: u16 + name: Keycode + } + ] + return_execute: dynamic_encoder_set_keycode + } + } + } + + 0x06: { + type: router + name: Lighting + define: LIGHTING + description: + ''' + This subsystem allows for control over the lighting subsystem. + ''' + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + Lighting subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_LIGHTING_CAPABILITIES + } + } + } + } +} diff --git a/data/xap/xap_0.3.0.hjson b/data/xap/xap_0.3.0.hjson new file mode 100644 index 000000000000..a119e6a42fc2 --- /dev/null +++ b/data/xap/xap_0.3.0.hjson @@ -0,0 +1,396 @@ +{ + version: 0.3.0 + + routes: { + + 0x06: { + routes: { + + 0x02: { + type: router + name: backlight + define: BACKLIGHT + description: + ''' + This subsystem allows for control over the backlight subsystem. + ''' + enable_if_preprocessor: defined(BACKLIGHT_ENABLE) + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + backlight subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_LIGHTING_BACKLIGHT_CAPABILITIES + } + 0x02: { + type: command + name: Get Enabled Effects + define: GET_ENABLED_EFFECTS + description: Each bit should be considered as a "usable" effect id + return_type: u8 + return_constant: ENABLED_BACKLIGHT_EFFECTS + } + 0x03: { + type: command + name: Get Config + define: GET_CONFIG + description: Query the current config. + return_type: struct + return_struct_length: 3 + return_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: mode + }, + { + type: u8 + name: val + }, + ] + return_execute: get_backlight_config + } + 0x04: { + type: command + name: Set Config + define: SET_CONFIG + description: Set the current config. + request_type: struct + request_struct_length: 3 + request_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: mode + }, + { + type: u8 + name: val + }, + ] + return_execute: set_backlight_config + } + 0x05: { + type: command + name: Save Config + define: SAVE_CONFIG + description: Save the current config. + return_execute: save_backlight_config + } + } + } + + 0x03: { + type: router + name: rgblight + define: RGBLIGHT + description: + ''' + This subsystem allows for control over the rgblight subsystem. + ''' + enable_if_preprocessor: defined(RGBLIGHT_ENABLE) + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + rgblight subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_LIGHTING_RGBLIGHT_CAPABILITIES + } + 0x02: { + type: command + name: Get Enabled Effects + define: GET_ENABLED_EFFECTS + description: Each bit should be considered as a "usable" effect id + return_type: u64 + return_constant: ENABLED_RGBLIGHT_EFFECTS + } + 0x03: { + type: command + name: Get Config + define: GET_CONFIG + description: Query the current config. + return_type: struct + return_struct_length: 6 + return_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: mode + }, + { + type: u8 + name: hue + }, + { + type: u8 + name: sat + }, + { + type: u8 + name: val + }, + { + type: u8 + name: speed + }, + ] + return_execute: get_rgblight_config + } + 0x04: { + type: command + name: Set Config + define: SET_CONFIG + description: Set the current config. + request_type: struct + request_struct_length: 6 + request_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: mode + }, + { + type: u8 + name: hue + }, + { + type: u8 + name: sat + }, + { + type: u8 + name: val + }, + { + type: u8 + name: speed + }, + ] + return_execute: set_rgblight_config + } + 0x05: { + type: command + name: Save Config + define: SAVE_CONFIG + description: Save the current config. + return_execute: save_rgblight_config + } + } + } + + 0x04: { + type: router + name: rgbmatrix + define: RGB_MATRIX + description: + ''' + This subsystem allows for control over the rgb matrix subsystem. + ''' + enable_if_preprocessor: defined(RGB_MATRIX_ENABLE) + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + rgb matrix subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_LIGHTING_RGB_MATRIX_CAPABILITIES + } + 0x02: { + type: command + name: Get Enabled Effects + define: GET_ENABLED_EFFECTS + description: Each bit should be considered as a "usable" effect id + return_type: u64 + return_constant: ENABLED_RGB_MATRIX_EFFECTS + } + 0x03: { + type: command + name: Get Config + define: GET_CONFIG + description: Query the current config. + return_type: struct + return_struct_length: 7 + return_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: mode + }, + { + type: u8 + name: hue + }, + { + type: u8 + name: sat + }, + { + type: u8 + name: val + }, + { + type: u8 + name: speed + }, + { + type: u8 + name: flags + }, + ] + return_execute: get_rgb_matrix_config + } + 0x04: { + type: command + name: Set Config + define: SET_CONFIG + description: Set the current config. + request_type: struct + request_struct_length: 7 + request_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: mode + }, + { + type: u8 + name: hue + }, + { + type: u8 + name: sat + }, + { + type: u8 + name: val + }, + { + type: u8 + name: speed + }, + { + type: u8 + name: flags + }, + ] + return_execute: set_rgb_matrix_config + } + 0x05: { + type: command + name: Save Config + define: SAVE_CONFIG + description: Save the current config. + return_execute: save_rgb_matrix_config + } + } + } + } + } + + 0x07: { + type: router + name: Audio + define: AUDIO + description: + ''' + This subsystem allows for control over the audio subsystem. + ''' + enable_if_preprocessor: defined(AUDIO_ENABLE) + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY + description: + ''' + Audio subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_AUDIO_CAPABILITIES + } + + 0x03: { + type: command + name: Get Config + define: GET_CONFIG + description: Query the current config. + return_type: struct + return_struct_length: 2 + return_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: clicky_enable + }, + ] + return_execute: get_audio_config + } + 0x04: { + type: command + name: Set Config + define: SET_CONFIG + description: Set the current config. + request_type: struct + request_struct_length: 2 + request_struct_members: [ + { + type: u8 + name: enable + }, + { + type: u8 + name: clicky_enable + }, + ] + return_execute: set_audio_config + } + 0x05: { + type: command + name: Save Config + define: SAVE_CONFIG + description: Save the current config. + return_execute: save_audio_config + } + } + } + } +} diff --git a/docs/_sidebar.json b/docs/_sidebar.json index d691011d641f..9b5dc1ff7564 100644 --- a/docs/_sidebar.json +++ b/docs/_sidebar.json @@ -135,7 +135,8 @@ { "text": "Tri Layer", "link": "/features/tri_layer" }, { "text": "Unicode", "link": "/features/unicode" }, { "text": "Userspace", "link": "/feature_userspace" }, - { "text": "WPM Calculation", "link": "/features/wpm" } + { "text": "WPM Calculation", "link": "/features/wpm" }, + { "text": "XAP", "link": "/xap" } ] }, { diff --git a/docs/xap.md b/docs/xap.md new file mode 100644 index 000000000000..f83af10618ee --- /dev/null +++ b/docs/xap.md @@ -0,0 +1,95 @@ +# XAP + +XAP (“extensible application protocol”) API intends to provide access to various QMK subsystems. + +## Overview + +```mermaid +%%{init: {'themeVariables': { 'fontSize': '24px'}}}%% +flowchart LR + dev[QMK Device] <-- XAP --> host[Host Computer] +``` + +The intention is to provide access to QMK subsystems through a versioned and documented protocol. + +## Protocol Reference + +[protocol_versions](xap_protocol.md ':include') + +## Clients + +TODO + +## CLI + +The QMK CLI provides a few XAP specific commands for diagnosis purposes. + +### List Connected Devices +Simple +``` +$ qmk xap --list +Ψ Available devices: +Ψ 7844:8450 KPrepublic XD84 Pro [API:0.2.0] LOCKED +``` + +Verbose +``` +$ qmk --verbose xap --list +Ψ Available devices: +Ψ 7844:8450 KPrepublic XD84 Pro [API:0.2.0] LOCKED + _id: 553831323538150A2113000000000000 + backlight.pin: F5 + bootloader: atmel-dfu + community_layouts: 75_ansi, 75_iso + debounce: 5 + diode_direction: COL2ROW + features.audio: False + features.backlight: True + features.bootmagic: True + features.command: False + features.console: False + features.extrakey: True + features.mousekey: False + features.nkro: True + features.rgblight: True + indicators.caps_lock: B2 + keyboard_folder: xiudi/xd84pro + keyboard_name: XD84 Pro + layouts: LAYOUT_75_ansi, LAYOUT_75_iso, LAYOUT_all + maintainer: qmk + manufacturer: KPrepublic + matrix_pins.cols: B1, B3, B4, B5, B6, B7, C6, C7, D4, D6, D7, E6, F0, F1, F7 + matrix_pins.rows: D0, D1, D2, D3, D5, F4 + matrix_pins.unused: B0, E2 + matrix_size.cols: 15 + matrix_size.rows: 6 + mouse_key.enabled: False + platform: unknown + processor: atmega32u4 + processor_type: avr + protocol: LUFA + rgblight.animations.all: False + rgblight.led_count: 12 + rgblight.pin: F6 + rgblight.sleep: False + url: + usb.device_ver: 0x0001 + usb.device_version: 0.0.1 + usb.pid: 0x8450 + usb.vid: 0x7844 +``` + +### Interactive shell +``` +$ qmk xap -i +Ψ Connected to:7844:8450 KPrepublic XD84 Pro +Welcome to the XAP shell. Type help or ? to list commands. + +Ψ> help + +Documented commands (type help ): +======================================== +EOF about exit help keycode keymap layer listen unlock + +Ψ> +``` diff --git a/docs/xap_0.0.1.md b/docs/xap_0.0.1.md new file mode 100644 index 000000000000..e3b7619a5ace --- /dev/null +++ b/docs/xap_0.0.1.md @@ -0,0 +1,108 @@ + + + + +# QMK Firmware XAP Specs + +This document describes the requirements of the QMK XAP ("extensible application protocol") API. + +## Types + +**All integral types are little-endian.** + +| Name | Definition | +| -- | -- | +| _type[n]_ | An array of `type`, with array extent of `N` -- e.g. `u8[2]` signifies two consecutive octets. | +| _u16_ | An unsigned 16-bit integral, commonly seen as `uint16_t` from _stdint.h_. | +| _u32_ | An unsigned 32-bit integral, commonly seen as `uint32_t` from _stdint.h_. | +| _u8_ | An unsigned 8-bit integral (octet, or byte), commonly seen as `uint8_t` from _stdint.h_. | + +## Definitions + +This list defines the terms used across the entire set of XAP protocol documentation. + +| Name | Definition | +| -- | -- | +| _Handler_ | A piece of code that is executed when a specific _route_ is received. | +| _Payload_ | Any received data appended to the _route_, which gets delivered to the _handler_ when received. | +| _Response_ | The data sent back to the host during execution of a _handler_. | +| _Route_ | A sequence of _IDs_ describing the route to invoke a _handler_. | +| _Subsystem_ | A high-level area of functionality within XAP. | +| _ID_ | A single octet / 8-bit byte, representing Subsystem or Route index. | +| _Request Header_ | Packet format for inbound data. Takes the format:
`token` - token
`u8` - length | +| _Response Flags_ | An `u8` containing the status of the request. | +| _Response Header_ | Packet format for outbound data. Takes the format:
`token` - token
`response_flags` - flags
`u8` - length | +| _Token_ | A `u16` associated with a specific request as well as its corresponding response. Valid token values are within the range `0x0100`-`0xFFFF`. | + +## Requests and Responses + +Communication generally follows a request/response pattern. + +Each request needs to include a _token_ -- this `u16` value prefixes each outbound request from the host application and its corresponding response. +This allows response messages to be correlated with their request, even if multiple host applications are communicating with the firmware simultaneously. +Host applications should randomly generate a token ID for **every** outbound request, unless using a reserved token defined below. +To ensure host interoperability, valid token values are within the range `0x0100`-`0xFFFF`. + +This token is followed by a `u8` signifying the length of data in the request. + + + +Response messages will always be prefixed by the originating request _token_, directly followed by that request's _response flags_, then the response payload length: + +| Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | +| -- | -- | -- | -- | -- | -- | -- | -- | +| `-` | `-` | `-` | `-` | `-` | `-` | `-` | `SUCCESS` | + +* Bit 0 (`SUCCESS`): When this bit is set, the request was successfully handled. If not set, all payload data should be disregarded, and the request retried if appropriate (with a new token). + +### Example "conversation": + +**Request** -- version query: + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Payload Length | Route | Route | +| **Value** | `0x43` | `0x2B` | `0x02` | `0x00` | `0x00` | + +**Response** -- matching token, successful flag, payload of `0x03170192` = 3.17.192: + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Response Flags | Payload Length | Payload | Payload | Payload | Payload | +| **Value** | `0x43` | `0x2B` | `0x01` | `0x04` | `0x92` | `0x01` | `0x17` | `0x03` | + +## Routes + +Subsystem validity should be queried through the “Enabled-in-firmware subsystem query” under the QMK subsystem (route=0x00,0x01). +This is the primary method for determining if a subsystem has been enabled in the running firmware. + +### XAP - `0x00` +This subsystem is always present, and provides the ability to query information about the XAP protocol of the connected device. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x00 0x00` | | __Response:__ `u32` | XAP protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| + + diff --git a/docs/xap_0.1.0.md b/docs/xap_0.1.0.md new file mode 100644 index 000000000000..d69cea542d33 --- /dev/null +++ b/docs/xap_0.1.0.md @@ -0,0 +1,176 @@ + + + + +# QMK Firmware XAP Specs + +This document describes the requirements of the QMK XAP ("extensible application protocol") API. + +## Types + +**All integral types are little-endian.** + +| Name | Definition | +| -- | -- | +| _bool_ | Data type that contains values 0 and 1. Implemented as an alias of `u8`. | +| _struct{}_ | A structure of data, packing different objects together. Data is "compacted" -- there are no padding bytes between fields. Equivalent to a packed C-style `struct`. The order in which they're defined matches the order of the data in the response packet. | +| _type[n]_ | An array of `type`, with array extent of `N` -- e.g. `u8[2]` signifies two consecutive octets. | +| _u16_ | An unsigned 16-bit integral, commonly seen as `uint16_t` from _stdint.h_. | +| _u32_ | An unsigned 32-bit integral, commonly seen as `uint32_t` from _stdint.h_. | +| _u64_ | An unsigned 64-bit integral, commonly seen as `uint64_t` from _stdint.h_. | +| _u8_ | An unsigned 8-bit integral (octet, or byte), commonly seen as `uint8_t` from _stdint.h_. | + +## Definitions + +This list defines the terms used across the entire set of XAP protocol documentation. + +| Name | Definition | +| -- | -- | +| _Capability_ | A way to determine if certain functionality is enabled in the firmware. Any _subsystem_ that provides build-time restriction of functionality must provide a _route_ for a _capabilities query_. | +| _Handler_ | A piece of code that is executed when a specific _route_ is received. | +| _Payload_ | Any received data appended to the _route_, which gets delivered to the _handler_ when received. | +| _Response_ | The data sent back to the host during execution of a _handler_. | +| _Route_ | A sequence of _IDs_ describing the route to invoke a _handler_. | +| _Secure Route_ | A _route_ which has potentially destructive consequences, necessitating prior approval by the user before executing. | +| _Subsystem_ | A high-level area of functionality within XAP. | +| _Unlock sequence_ | A physical sequence initiated by the user to enable execution of _secure routes_. | +| _Broadcast Header_ | Packet format for broadcast messages. Takes the format:
`token` - token
`u8` - type
`u8` - length | +| _ID_ | A single octet / 8-bit byte, representing Subsystem or Route index. | +| _Request Header_ | Packet format for inbound data. Takes the format:
`token` - token
`u8` - length | +| _Response Flags_ | An `u8` containing the status of the request. | +| _Response Header_ | Packet format for outbound data. Takes the format:
`token` - token
`response_flags` - flags
`u8` - length | +| _Token_ | A `u16` associated with a specific request as well as its corresponding response. Valid token values are within the range `0x0100`-`0xFFFF`. | + +## Requests and Responses + +Communication generally follows a request/response pattern. + +Each request needs to include a _token_ -- this `u16` value prefixes each outbound request from the host application and its corresponding response. +This allows response messages to be correlated with their request, even if multiple host applications are communicating with the firmware simultaneously. +Host applications should randomly generate a token ID for **every** outbound request, unless using a reserved token defined below. +To ensure host interoperability, valid token values are within the range `0x0100`-`0xFFFF`. + +This token is followed by a `u8` signifying the length of data in the request. + +Two token values are reserved: `0xFFFE` and `0xFFFF`: +* `0xFFFE`: A message sent by a host application may use this token if no response is to be sent -- a "fire and forget" message. +* `0xFFFF`: Signifies a "broadcast" message sent by the firmware without prompting from the host application. Broadcast messages are defined later in this document. + +Any request will generate at least one corresponding response, with the exception of messages using reserved tokens. Maximum total message length is 128 bytes due to RAM constraints. + +Response messages will always be prefixed by the originating request _token_, directly followed by that request's _response flags_, then the response payload length: + +| Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | +| -- | -- | -- | -- | -- | -- | -- | -- | +| `-` | `-` | `-` | `-` | `-` | `-` | `SECURE_FAILURE` | `SUCCESS` | + +* Bit 1 (`SECURE_FAILURE`): When this bit is set, the requested _route_ was marked _secure_ but an _unlock sequence_ has not completed. +* Bit 0 (`SUCCESS`): When this bit is set, the request was successfully handled. If not set, all payload data should be disregarded, and the request retried if appropriate (with a new token). + +### Example "conversation": + +**Request** -- version query: + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Payload Length | Route | Route | +| **Value** | `0x43` | `0x2B` | `0x02` | `0x00` | `0x00` | + +**Response** -- matching token, successful flag, payload of `0x03170192` = 3.17.192: + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Response Flags | Payload Length | Payload | Payload | Payload | Payload | +| **Value** | `0x43` | `0x2B` | `0x01` | `0x04` | `0x92` | `0x01` | `0x17` | `0x03` | + +## Routes + +Subsystem validity should be queried through the “Enabled-in-firmware subsystem query” under the QMK subsystem (route=0x00,0x01). +This is the primary method for determining if a subsystem has been enabled in the running firmware. + +### XAP - `0x00` +This subsystem is always present, and provides the ability to query information about the XAP protocol of the connected device. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x00 0x00` | | __Response:__ `u32` | XAP protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| +| Capabilities Query | `0x00 0x01` | | __Response:__ `u32` | XAP subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Enabled subsystem query | `0x00 0x02` | | __Response:__ `u32` | XAP protocol subsystem query. Each bit should be considered as a "usable" subsystem. For example, checking `(value & (1 << XAP_ROUTE_QMK) != 0)` means the QMK subsystem is enabled and available for querying.| +| Secure Status | `0x00 0x03` | | __Response:__ `u8` | Query secure route status

* 0 means secure routes are disabled
* 1 means unlock sequence initiated but incomplete
* 2 means secure routes are allowed
* any other value should be interpreted as disabled| +| Secure Unlock | `0x00 0x04` | | | Initiate secure route unlock sequence| +| Secure Lock | `0x00 0x05` | | | Disable secure routes| + +### QMK - `0x01` +This subsystem is always present, and provides the ability to address QMK-specific functionality. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x01 0x00` | | __Response:__ `u32` | QMK protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| +| Capabilities Query | `0x01 0x01` | | __Response:__ `u32` | QMK subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Board identifiers | `0x01 0x02` | | __Response:__
    * Vendor ID: `u16`
    * Product ID: `u16`
    * Product Version: `u16`
    * QMK Unique Identifier: `u32` | Retrieves the set of identifying information for the board.| +| Board Manufacturer | `0x01 0x03` | | __Response:__ `string` | Retrieves the name of the manufacturer| +| Product Name | `0x01 0x04` | | __Response:__ `string` | Retrieves the product name| +| Config Blob Length | `0x01 0x05` | | __Response:__ `u16` | Retrieves the length of the configuration data bundled within the firmware| +| Config Blob Chunk | `0x01 0x06` | | __Request:__ `u16`

__Response:__ `u8[32]` | Retrieves a chunk of the configuration data bundled within the firmware| +| Jump to bootloader | `0x01 0x07` | __Secure__ | __Response:__ `u8` | Jump to bootloader

May not be present - if QMK capabilities query returns “true”, then jump to bootloader is supported

* 0 means secure routes are disabled, and should be considered as a failure
* 1 means successful, board will jump to bootloader| +| Hardware Identifier | `0x01 0x08` | | __Response:__ `u32[4]` | Retrieves a unique identifier for the board.| +| Reinitialize EEPROM | `0x01 0x09` | __Secure__ | __Response:__ `u8` | Reinitializes the keyboard's EEPROM (persistent memory)

May not be present - if QMK capabilities query returns “true”, then reinitialize is supported

* 0 means secure routes are disabled, and should be considered as a failure
* 1 means successful, board will reinitialize and then reboot| + +### Keyboard - `0x02` +This subsystem is always present, and reserved for vendor-specific functionality. No routes are defined by XAP. + +### User - `0x03` +This subsystem is always present, and reserved for user-specific functionality. No routes are defined by XAP. + + +## Broadcast messages + +Broadcast messages may be sent by the firmware to the host, without a corresponding inbound request. Each broadcast message uses the token `0xFFFF`, and does not expect a response from the host. Tokens are followed by an _ID_ signifying the type of broadcast, then the response _payload_ length, and finally the corresponding _payload_. + +### Log message - `0x00` +Replicates and replaces the same functionality as if using the standard QMK `CONSOLE_ENABLE = yes` in `rules.mk`. Normal prints within the firmware will manifest as log messages broadcast to the host. `hid_listen` will not be functional with XAP enabled. + +Log message payloads include `u8[Length]` containing the text, where the length of the text is the _broadcast_header.length_ field. + +**Example Log Broadcast** -- log message "Hello QMK!" + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Broadcast Type | Length | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | +| **Value** | `0xFF` | `0xFF` | `0x00` | `0x0A`(10) | `0x48`(H) | `0x65`(e) | `0x6C`(l) | `0x6C`(l) | `0x6F`(o) | `0x20`( ) | `0x51`(Q) | `0x4D`(M) | `0x4B`(K) | `0x21`(!) | +### Secure Status - `0x01` +Secure status has changed. Payloads include a `u8` matching a 'Secure Status' request. + +**Example Secure Status Broadcast** -- secure "Unlocking" + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Broadcast Type | Length | Secure Status | +| **Value** | `0xFF` | `0xFF` | `0x01` | `0x01` | `0x01` | +### Keyboard - `0x02` +Reserved for vendor-specific functionality. No messages are defined by XAP. +### User - `0x03` +Reserved for user-specific functionality. No messages are defined by XAP. + diff --git a/docs/xap_0.2.0.md b/docs/xap_0.2.0.md new file mode 100644 index 000000000000..0411283f16ee --- /dev/null +++ b/docs/xap_0.2.0.md @@ -0,0 +1,203 @@ + + + + +# QMK Firmware XAP Specs + +This document describes the requirements of the QMK XAP ("extensible application protocol") API. + +## Types + +**All integral types are little-endian.** + +| Name | Definition | +| -- | -- | +| _bool_ | Data type that contains values 0 and 1. Implemented as an alias of `u8`. | +| _struct{}_ | A structure of data, packing different objects together. Data is "compacted" -- there are no padding bytes between fields. Equivalent to a packed C-style `struct`. The order in which they're defined matches the order of the data in the response packet. | +| _type[n]_ | An array of `type`, with array extent of `N` -- e.g. `u8[2]` signifies two consecutive octets. | +| _u16_ | An unsigned 16-bit integral, commonly seen as `uint16_t` from _stdint.h_. | +| _u32_ | An unsigned 32-bit integral, commonly seen as `uint32_t` from _stdint.h_. | +| _u64_ | An unsigned 64-bit integral, commonly seen as `uint64_t` from _stdint.h_. | +| _u8_ | An unsigned 8-bit integral (octet, or byte), commonly seen as `uint8_t` from _stdint.h_. | + +## Definitions + +This list defines the terms used across the entire set of XAP protocol documentation. + +| Name | Definition | +| -- | -- | +| _Capability_ | A way to determine if certain functionality is enabled in the firmware. Any _subsystem_ that provides build-time restriction of functionality must provide a _route_ for a _capabilities query_. | +| _Handler_ | A piece of code that is executed when a specific _route_ is received. | +| _Payload_ | Any received data appended to the _route_, which gets delivered to the _handler_ when received. | +| _Response_ | The data sent back to the host during execution of a _handler_. | +| _Route_ | A sequence of _IDs_ describing the route to invoke a _handler_. | +| _Secure Route_ | A _route_ which has potentially destructive consequences, necessitating prior approval by the user before executing. | +| _Subsystem_ | A high-level area of functionality within XAP. | +| _Unlock sequence_ | A physical sequence initiated by the user to enable execution of _secure routes_. | +| _Broadcast Header_ | Packet format for broadcast messages. Takes the format:
`token` - token
`u8` - type
`u8` - length | +| _ID_ | A single octet / 8-bit byte, representing Subsystem or Route index. | +| _Request Header_ | Packet format for inbound data. Takes the format:
`token` - token
`u8` - length | +| _Response Flags_ | An `u8` containing the status of the request. | +| _Response Header_ | Packet format for outbound data. Takes the format:
`token` - token
`response_flags` - flags
`u8` - length | +| _Token_ | A `u16` associated with a specific request as well as its corresponding response. Valid token values are within the range `0x0100`-`0xFFFF`. | + +## Requests and Responses + +Communication generally follows a request/response pattern. + +Each request needs to include a _token_ -- this `u16` value prefixes each outbound request from the host application and its corresponding response. +This allows response messages to be correlated with their request, even if multiple host applications are communicating with the firmware simultaneously. +Host applications should randomly generate a token ID for **every** outbound request, unless using a reserved token defined below. +To ensure host interoperability, valid token values are within the range `0x0100`-`0xFFFF`. + +This token is followed by a `u8` signifying the length of data in the request. + +Two token values are reserved: `0xFFFE` and `0xFFFF`: +* `0xFFFE`: A message sent by a host application may use this token if no response is to be sent -- a "fire and forget" message. +* `0xFFFF`: Signifies a "broadcast" message sent by the firmware without prompting from the host application. Broadcast messages are defined later in this document. + +Any request will generate at least one corresponding response, with the exception of messages using reserved tokens. Maximum total message length is 128 bytes due to RAM constraints. + +Response messages will always be prefixed by the originating request _token_, directly followed by that request's _response flags_, then the response payload length: + +| Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | +| -- | -- | -- | -- | -- | -- | -- | -- | +| `-` | `-` | `-` | `-` | `-` | `-` | `SECURE_FAILURE` | `SUCCESS` | + +* Bit 1 (`SECURE_FAILURE`): When this bit is set, the requested _route_ was marked _secure_ but an _unlock sequence_ has not completed. +* Bit 0 (`SUCCESS`): When this bit is set, the request was successfully handled. If not set, all payload data should be disregarded, and the request retried if appropriate (with a new token). + +### Example "conversation": + +**Request** -- version query: + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Payload Length | Route | Route | +| **Value** | `0x43` | `0x2B` | `0x02` | `0x00` | `0x00` | + +**Response** -- matching token, successful flag, payload of `0x03170192` = 3.17.192: + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Response Flags | Payload Length | Payload | Payload | Payload | Payload | +| **Value** | `0x43` | `0x2B` | `0x01` | `0x04` | `0x92` | `0x01` | `0x17` | `0x03` | + +## Routes + +Subsystem validity should be queried through the “Enabled-in-firmware subsystem query” under the QMK subsystem (route=0x00,0x01). +This is the primary method for determining if a subsystem has been enabled in the running firmware. + +### XAP - `0x00` +This subsystem is always present, and provides the ability to query information about the XAP protocol of the connected device. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x00 0x00` | | __Response:__ `u32` | XAP protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| +| Capabilities Query | `0x00 0x01` | | __Response:__ `u32` | XAP subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Enabled subsystem query | `0x00 0x02` | | __Response:__ `u32` | XAP protocol subsystem query. Each bit should be considered as a "usable" subsystem. For example, checking `(value & (1 << XAP_ROUTE_QMK) != 0)` means the QMK subsystem is enabled and available for querying.| +| Secure Status | `0x00 0x03` | | __Response:__ `u8` | Query secure route status

* 0 means secure routes are disabled
* 1 means unlock sequence initiated but incomplete
* 2 means secure routes are allowed
* any other value should be interpreted as disabled| +| Secure Unlock | `0x00 0x04` | | | Initiate secure route unlock sequence| +| Secure Lock | `0x00 0x05` | | | Disable secure routes| + +### QMK - `0x01` +This subsystem is always present, and provides the ability to address QMK-specific functionality. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x01 0x00` | | __Response:__ `u32` | QMK protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| +| Capabilities Query | `0x01 0x01` | | __Response:__ `u32` | QMK subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Board identifiers | `0x01 0x02` | | __Response:__
    * Vendor ID: `u16`
    * Product ID: `u16`
    * Product Version: `u16`
    * QMK Unique Identifier: `u32` | Retrieves the set of identifying information for the board.| +| Board Manufacturer | `0x01 0x03` | | __Response:__ `string` | Retrieves the name of the manufacturer| +| Product Name | `0x01 0x04` | | __Response:__ `string` | Retrieves the product name| +| Config Blob Length | `0x01 0x05` | | __Response:__ `u16` | Retrieves the length of the configuration data bundled within the firmware| +| Config Blob Chunk | `0x01 0x06` | | __Request:__ `u16`

__Response:__ `u8[32]` | Retrieves a chunk of the configuration data bundled within the firmware| +| Jump to bootloader | `0x01 0x07` | __Secure__ | __Response:__ `u8` | Jump to bootloader

May not be present - if QMK capabilities query returns “true”, then jump to bootloader is supported

* 0 means secure routes are disabled, and should be considered as a failure
* 1 means successful, board will jump to bootloader| +| Hardware Identifier | `0x01 0x08` | | __Response:__ `u32[4]` | Retrieves a unique identifier for the board.| +| Reinitialize EEPROM | `0x01 0x09` | __Secure__ | __Response:__ `u8` | Reinitializes the keyboard's EEPROM (persistent memory)

May not be present - if QMK capabilities query returns “true”, then reinitialize is supported

* 0 means secure routes are disabled, and should be considered as a failure
* 1 means successful, board will reinitialize and then reboot| + +### Keyboard - `0x02` +This subsystem is always present, and reserved for vendor-specific functionality. No routes are defined by XAP. + +### User - `0x03` +This subsystem is always present, and reserved for user-specific functionality. No routes are defined by XAP. + +### Keymap - `0x04` +This subsystem allows for query of currently configured keycodes. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x04 0x01` | | __Response:__ `u32` | Keymap subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Layer Count | `0x04 0x02` | | __Response:__ `u8` | Query maximum number of layers that can be addressed within the keymap.| +| Get Keycode | `0x04 0x03` | | __Request:__
    * Layer: `u8`
    * Row: `u8`
    * Column: `u8`

__Response:__ `u16` | Query the Keycode at the requested location.| +| Get Encoder Keycode | `0x04 0x04` | | __Request:__
    * Layer: `u8`
    * Encoder: `u8`
    * Clockwise: `u8`

__Response:__ `u16` | Query the Keycode at the requested location.| + +### Remapping - `0x05` +This subsystem allows for live reassignment of keycodes without rebuilding the firmware. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x05 0x01` | | __Response:__ `u32` | Remapping subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Layer Count | `0x05 0x02` | | __Response:__ `u8` | Query maximum number of layers that can be addressed within the keymap.| +| Set Keycode | `0x05 0x03` | __Secure__ | __Request:__
    * Layer: `u8`
    * Row: `u8`
    * Column: `u8`
    * Keycode: `u16` | Modify the Keycode at the requested location.| +| Set Encoder Keycode | `0x05 0x04` | __Secure__ | __Request:__
    * Layer: `u8`
    * Encoder: `u8`
    * Clockwise: `u8`
    * Keycode: `u16` | Modify the Keycode at the requested location.| + +### Lighting - `0x06` +This subsystem allows for control over the lighting subsystem. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x06 0x01` | | __Response:__ `u32` | Lighting subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| + + +## Broadcast messages + +Broadcast messages may be sent by the firmware to the host, without a corresponding inbound request. Each broadcast message uses the token `0xFFFF`, and does not expect a response from the host. Tokens are followed by an _ID_ signifying the type of broadcast, then the response _payload_ length, and finally the corresponding _payload_. + +### Log message - `0x00` +Replicates and replaces the same functionality as if using the standard QMK `CONSOLE_ENABLE = yes` in `rules.mk`. Normal prints within the firmware will manifest as log messages broadcast to the host. `hid_listen` will not be functional with XAP enabled. + +Log message payloads include `u8[Length]` containing the text, where the length of the text is the _broadcast_header.length_ field. + +**Example Log Broadcast** -- log message "Hello QMK!" + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Broadcast Type | Length | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | +| **Value** | `0xFF` | `0xFF` | `0x00` | `0x0A`(10) | `0x48`(H) | `0x65`(e) | `0x6C`(l) | `0x6C`(l) | `0x6F`(o) | `0x20`( ) | `0x51`(Q) | `0x4D`(M) | `0x4B`(K) | `0x21`(!) | +### Secure Status - `0x01` +Secure status has changed. Payloads include a `u8` matching a 'Secure Status' request. + +**Example Secure Status Broadcast** -- secure "Unlocking" + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Broadcast Type | Length | Secure Status | +| **Value** | `0xFF` | `0xFF` | `0x01` | `0x01` | `0x01` | +### Keyboard - `0x02` +Reserved for vendor-specific functionality. No messages are defined by XAP. +### User - `0x03` +Reserved for user-specific functionality. No messages are defined by XAP. + diff --git a/docs/xap_0.3.0.md b/docs/xap_0.3.0.md new file mode 100644 index 000000000000..6ceaa59ed89f --- /dev/null +++ b/docs/xap_0.3.0.md @@ -0,0 +1,249 @@ + + + + +# QMK Firmware XAP Specs + +This document describes the requirements of the QMK XAP ("extensible application protocol") API. + +## Types + +**All integral types are little-endian.** + +| Name | Definition | +| -- | -- | +| _bool_ | Data type that contains values 0 and 1. Implemented as an alias of `u8`. | +| _struct{}_ | A structure of data, packing different objects together. Data is "compacted" -- there are no padding bytes between fields. Equivalent to a packed C-style `struct`. The order in which they're defined matches the order of the data in the response packet. | +| _type[n]_ | An array of `type`, with array extent of `N` -- e.g. `u8[2]` signifies two consecutive octets. | +| _u16_ | An unsigned 16-bit integral, commonly seen as `uint16_t` from _stdint.h_. | +| _u32_ | An unsigned 32-bit integral, commonly seen as `uint32_t` from _stdint.h_. | +| _u64_ | An unsigned 64-bit integral, commonly seen as `uint64_t` from _stdint.h_. | +| _u8_ | An unsigned 8-bit integral (octet, or byte), commonly seen as `uint8_t` from _stdint.h_. | + +## Definitions + +This list defines the terms used across the entire set of XAP protocol documentation. + +| Name | Definition | +| -- | -- | +| _Capability_ | A way to determine if certain functionality is enabled in the firmware. Any _subsystem_ that provides build-time restriction of functionality must provide a _route_ for a _capabilities query_. | +| _Handler_ | A piece of code that is executed when a specific _route_ is received. | +| _Payload_ | Any received data appended to the _route_, which gets delivered to the _handler_ when received. | +| _Response_ | The data sent back to the host during execution of a _handler_. | +| _Route_ | A sequence of _IDs_ describing the route to invoke a _handler_. | +| _Secure Route_ | A _route_ which has potentially destructive consequences, necessitating prior approval by the user before executing. | +| _Subsystem_ | A high-level area of functionality within XAP. | +| _Unlock sequence_ | A physical sequence initiated by the user to enable execution of _secure routes_. | +| _Broadcast Header_ | Packet format for broadcast messages. Takes the format:
`token` - token
`u8` - type
`u8` - length | +| _ID_ | A single octet / 8-bit byte, representing Subsystem or Route index. | +| _Request Header_ | Packet format for inbound data. Takes the format:
`token` - token
`u8` - length | +| _Response Flags_ | An `u8` containing the status of the request. | +| _Response Header_ | Packet format for outbound data. Takes the format:
`token` - token
`response_flags` - flags
`u8` - length | +| _Token_ | A `u16` associated with a specific request as well as its corresponding response. Valid token values are within the range `0x0100`-`0xFFFF`. | + +## Requests and Responses + +Communication generally follows a request/response pattern. + +Each request needs to include a _token_ -- this `u16` value prefixes each outbound request from the host application and its corresponding response. +This allows response messages to be correlated with their request, even if multiple host applications are communicating with the firmware simultaneously. +Host applications should randomly generate a token ID for **every** outbound request, unless using a reserved token defined below. +To ensure host interoperability, valid token values are within the range `0x0100`-`0xFFFF`. + +This token is followed by a `u8` signifying the length of data in the request. + +Two token values are reserved: `0xFFFE` and `0xFFFF`: +* `0xFFFE`: A message sent by a host application may use this token if no response is to be sent -- a "fire and forget" message. +* `0xFFFF`: Signifies a "broadcast" message sent by the firmware without prompting from the host application. Broadcast messages are defined later in this document. + +Any request will generate at least one corresponding response, with the exception of messages using reserved tokens. Maximum total message length is 128 bytes due to RAM constraints. + +Response messages will always be prefixed by the originating request _token_, directly followed by that request's _response flags_, then the response payload length: + +| Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | +| -- | -- | -- | -- | -- | -- | -- | -- | +| `-` | `-` | `-` | `-` | `-` | `-` | `SECURE_FAILURE` | `SUCCESS` | + +* Bit 1 (`SECURE_FAILURE`): When this bit is set, the requested _route_ was marked _secure_ but an _unlock sequence_ has not completed. +* Bit 0 (`SUCCESS`): When this bit is set, the request was successfully handled. If not set, all payload data should be disregarded, and the request retried if appropriate (with a new token). + +### Example "conversation": + +**Request** -- version query: + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Payload Length | Route | Route | +| **Value** | `0x43` | `0x2B` | `0x02` | `0x00` | `0x00` | + +**Response** -- matching token, successful flag, payload of `0x03170192` = 3.17.192: + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Response Flags | Payload Length | Payload | Payload | Payload | Payload | +| **Value** | `0x43` | `0x2B` | `0x01` | `0x04` | `0x92` | `0x01` | `0x17` | `0x03` | + +## Routes + +Subsystem validity should be queried through the “Enabled-in-firmware subsystem query” under the QMK subsystem (route=0x00,0x01). +This is the primary method for determining if a subsystem has been enabled in the running firmware. + +### XAP - `0x00` +This subsystem is always present, and provides the ability to query information about the XAP protocol of the connected device. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x00 0x00` | | __Response:__ `u32` | XAP protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| +| Capabilities Query | `0x00 0x01` | | __Response:__ `u32` | XAP subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Enabled subsystem query | `0x00 0x02` | | __Response:__ `u32` | XAP protocol subsystem query. Each bit should be considered as a "usable" subsystem. For example, checking `(value & (1 << XAP_ROUTE_QMK) != 0)` means the QMK subsystem is enabled and available for querying.| +| Secure Status | `0x00 0x03` | | __Response:__ `u8` | Query secure route status

* 0 means secure routes are disabled
* 1 means unlock sequence initiated but incomplete
* 2 means secure routes are allowed
* any other value should be interpreted as disabled| +| Secure Unlock | `0x00 0x04` | | | Initiate secure route unlock sequence| +| Secure Lock | `0x00 0x05` | | | Disable secure routes| + +### QMK - `0x01` +This subsystem is always present, and provides the ability to address QMK-specific functionality. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Version Query | `0x01 0x00` | | __Response:__ `u32` | QMK protocol version query.

* Returns the BCD-encoded version in the format of XX.YY.ZZZZ => `0xXXYYZZZZ`
* e.g. 3.2.115 will match `0x03020115`, or bytes {0x15,0x01,0x02,0x03}.| +| Capabilities Query | `0x01 0x01` | | __Response:__ `u32` | QMK subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Board identifiers | `0x01 0x02` | | __Response:__
    * Vendor ID: `u16`
    * Product ID: `u16`
    * Product Version: `u16`
    * QMK Unique Identifier: `u32` | Retrieves the set of identifying information for the board.| +| Board Manufacturer | `0x01 0x03` | | __Response:__ `string` | Retrieves the name of the manufacturer| +| Product Name | `0x01 0x04` | | __Response:__ `string` | Retrieves the product name| +| Config Blob Length | `0x01 0x05` | | __Response:__ `u16` | Retrieves the length of the configuration data bundled within the firmware| +| Config Blob Chunk | `0x01 0x06` | | __Request:__ `u16`

__Response:__ `u8[32]` | Retrieves a chunk of the configuration data bundled within the firmware| +| Jump to bootloader | `0x01 0x07` | __Secure__ | __Response:__ `u8` | Jump to bootloader

May not be present - if QMK capabilities query returns “true”, then jump to bootloader is supported

* 0 means secure routes are disabled, and should be considered as a failure
* 1 means successful, board will jump to bootloader| +| Hardware Identifier | `0x01 0x08` | | __Response:__ `u32[4]` | Retrieves a unique identifier for the board.| +| Reinitialize EEPROM | `0x01 0x09` | __Secure__ | __Response:__ `u8` | Reinitializes the keyboard's EEPROM (persistent memory)

May not be present - if QMK capabilities query returns “true”, then reinitialize is supported

* 0 means secure routes are disabled, and should be considered as a failure
* 1 means successful, board will reinitialize and then reboot| + +### Keyboard - `0x02` +This subsystem is always present, and reserved for vendor-specific functionality. No routes are defined by XAP. + +### User - `0x03` +This subsystem is always present, and reserved for user-specific functionality. No routes are defined by XAP. + +### Keymap - `0x04` +This subsystem allows for query of currently configured keycodes. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x04 0x01` | | __Response:__ `u32` | Keymap subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Layer Count | `0x04 0x02` | | __Response:__ `u8` | Query maximum number of layers that can be addressed within the keymap.| +| Get Keycode | `0x04 0x03` | | __Request:__
    * Layer: `u8`
    * Row: `u8`
    * Column: `u8`

__Response:__ `u16` | Query the Keycode at the requested location.| +| Get Encoder Keycode | `0x04 0x04` | | __Request:__
    * Layer: `u8`
    * Encoder: `u8`
    * Clockwise: `u8`

__Response:__ `u16` | Query the Keycode at the requested location.| + +### Remapping - `0x05` +This subsystem allows for live reassignment of keycodes without rebuilding the firmware. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x05 0x01` | | __Response:__ `u32` | Remapping subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Layer Count | `0x05 0x02` | | __Response:__ `u8` | Query maximum number of layers that can be addressed within the keymap.| +| Set Keycode | `0x05 0x03` | __Secure__ | __Request:__
    * Layer: `u8`
    * Row: `u8`
    * Column: `u8`
    * Keycode: `u16` | Modify the Keycode at the requested location.| +| Set Encoder Keycode | `0x05 0x04` | __Secure__ | __Request:__
    * Layer: `u8`
    * Encoder: `u8`
    * Clockwise: `u8`
    * Keycode: `u16` | Modify the Keycode at the requested location.| + +### Lighting - `0x06` +This subsystem allows for control over the lighting subsystem. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x06 0x01` | | __Response:__ `u32` | Lighting subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| + + +#### backlight - `0x06 0x02` +This subsystem allows for control over the backlight subsystem. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x06 0x02 0x01` | | __Response:__ `u32` | backlight subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Enabled Effects | `0x06 0x02 0x02` | | __Response:__ `u8` | Each bit should be considered as a "usable" effect id| +| Get Config | `0x06 0x02 0x03` | | __Response:__
    * enable: `u8`
    * mode: `u8`
    * val: `u8` | Query the current config.| +| Set Config | `0x06 0x02 0x04` | | __Request:__
    * enable: `u8`
    * mode: `u8`
    * val: `u8` | Set the current config.| +| Save Config | `0x06 0x02 0x05` | | | Save the current config.| + + +#### rgblight - `0x06 0x03` +This subsystem allows for control over the rgblight subsystem. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x06 0x03 0x01` | | __Response:__ `u32` | rgblight subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Enabled Effects | `0x06 0x03 0x02` | | __Response:__ `u64` | Each bit should be considered as a "usable" effect id| +| Get Config | `0x06 0x03 0x03` | | __Response:__
    * enable: `u8`
    * mode: `u8`
    * hue: `u8`
    * sat: `u8`
    * val: `u8`
    * speed: `u8` | Query the current config.| +| Set Config | `0x06 0x03 0x04` | | __Request:__
    * enable: `u8`
    * mode: `u8`
    * hue: `u8`
    * sat: `u8`
    * val: `u8`
    * speed: `u8` | Set the current config.| +| Save Config | `0x06 0x03 0x05` | | | Save the current config.| + + +#### rgbmatrix - `0x06 0x04` +This subsystem allows for control over the rgb matrix subsystem. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x06 0x04 0x01` | | __Response:__ `u32` | rgb matrix subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Enabled Effects | `0x06 0x04 0x02` | | __Response:__ `u64` | Each bit should be considered as a "usable" effect id| +| Get Config | `0x06 0x04 0x03` | | __Response:__
    * enable: `u8`
    * mode: `u8`
    * hue: `u8`
    * sat: `u8`
    * val: `u8`
    * speed: `u8`
    * flags: `u8` | Query the current config.| +| Set Config | `0x06 0x04 0x04` | | __Request:__
    * enable: `u8`
    * mode: `u8`
    * hue: `u8`
    * sat: `u8`
    * val: `u8`
    * speed: `u8`
    * flags: `u8` | Set the current config.| +| Save Config | `0x06 0x04 0x05` | | | Save the current config.| + +### Audio - `0x07` + This subsystem allows for control over the audio subsystem. + +| Name | Route | Tags | Payloads | Description | +| -- | -- | -- | -- | -- | +| Capabilities Query | `0x07 0x01` | | __Response:__ `u32` | Audio subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem.| +| Get Config | `0x07 0x03` | | __Response:__
    * enable: `u8`
    * clicky_enable: `u8` | Query the current config.| +| Set Config | `0x07 0x04` | | __Request:__
    * enable: `u8`
    * clicky_enable: `u8` | Set the current config.| +| Save Config | `0x07 0x05` | | | Save the current config.| + + +## Broadcast messages + +Broadcast messages may be sent by the firmware to the host, without a corresponding inbound request. Each broadcast message uses the token `0xFFFF`, and does not expect a response from the host. Tokens are followed by an _ID_ signifying the type of broadcast, then the response _payload_ length, and finally the corresponding _payload_. + +### Log message - `0x00` +Replicates and replaces the same functionality as if using the standard QMK `CONSOLE_ENABLE = yes` in `rules.mk`. Normal prints within the firmware will manifest as log messages broadcast to the host. `hid_listen` will not be functional with XAP enabled. + +Log message payloads include `u8[Length]` containing the text, where the length of the text is the _broadcast_header.length_ field. + +**Example Log Broadcast** -- log message "Hello QMK!" + +| Byte | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Broadcast Type | Length | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | Payload | +| **Value** | `0xFF` | `0xFF` | `0x00` | `0x0A`(10) | `0x48`(H) | `0x65`(e) | `0x6C`(l) | `0x6C`(l) | `0x6F`(o) | `0x20`( ) | `0x51`(Q) | `0x4D`(M) | `0x4B`(K) | `0x21`(!) | +### Secure Status - `0x01` +Secure status has changed. Payloads include a `u8` matching a 'Secure Status' request. + +**Example Secure Status Broadcast** -- secure "Unlocking" + +| Byte | 0 | 1 | 2 | 3 | 4 | +| --- | --- | --- | --- | --- | --- | +| **Purpose** | Token | Token | Broadcast Type | Length | Secure Status | +| **Value** | `0xFF` | `0xFF` | `0x01` | `0x01` | `0x01` | +### Keyboard - `0x02` +Reserved for vendor-specific functionality. No messages are defined by XAP. +### User - `0x03` +Reserved for user-specific functionality. No messages are defined by XAP. + diff --git a/docs/xap_protocol.md b/docs/xap_protocol.md new file mode 100644 index 000000000000..f35f72907518 --- /dev/null +++ b/docs/xap_protocol.md @@ -0,0 +1,30 @@ + + + + +* [XAP Version 0.3.0](xap_0.3.0.md) +* [XAP Version 0.2.0](xap_0.2.0.md) +* [XAP Version 0.1.0](xap_0.1.0.md) +* [XAP Version 0.0.1](xap_0.0.1.md) diff --git a/keyboards/zvecr/zv48/keymaps/xap/keymap.c b/keyboards/zvecr/zv48/keymaps/xap/keymap.c new file mode 100644 index 000000000000..b9d3ceec6eee --- /dev/null +++ b/keyboards/zvecr/zv48/keymaps/xap/keymap.c @@ -0,0 +1,113 @@ +// Copyright 2020 zvecr +// SPDX-License-Identifier: GPL-2.0-or-later +#include QMK_KEYBOARD_H + +// Defines names for use in layer keycodes and the keymap +enum layer_names { + _QWERTY, + _LOWER, + _RAISE, + _ADJUST, +}; + +#define LOWER MO(_LOWER) +#define RAISE MO(_RAISE) + +const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = { +/* Qwerty + * ,-----------------------------------------------------------------------------------. + * | Esc | Q | W | E | R | T | Y | U | I | O | P | Bksp | + * |------+------+------+------+------+-------------+------+------+------+------+------| + * | Tab | A | S | D | F | G | H | J | K | L | ; | " | + * |------+------+------+------+------+------|------+------+------+------+------+------| + * | Shift| Z | X | C | V | B | N | M | , | . | / |Enter | + * |------+------+------+------+------+------+------+------+------+------+------+------| + * | Ctrl | GUI | Alt | App |Lower | Space |Raise | Left | Down | Up |Right | + * `-----------------------------------------------------------------------------------' + */ +[_QWERTY] = LAYOUT_ortho_4x12( + KC_ESC, KC_Q, KC_W, KC_E, KC_R, KC_T, KC_Y, KC_U, KC_I, KC_O, KC_P, KC_BSPC, + KC_TAB, KC_A, KC_S, KC_D, KC_F, KC_G, KC_H, KC_J, KC_K, KC_L, KC_SCLN, KC_QUOT, + KC_LSFT, KC_Z, KC_X, KC_C, KC_V, KC_B, KC_N, KC_M, KC_COMM, KC_DOT, KC_SLSH, KC_ENT , + KC_LCTL, KC_LGUI, KC_LALT, KC_APP, LOWER, KC_SPC, KC_SPC, RAISE, KC_LEFT, KC_DOWN, KC_UP, KC_RGHT +), + +/* Lower + * ,-----------------------------------------------------------------------------------. + * | ~ | ! | @ | # | $ | % | ^ | & | * | ( | ) | Del | + * |------+------+------+------+------+-------------+------+------+------+------+------| + * | Del | F1 | F2 | F3 | F4 | F5 | F6 | _ | + | { | } | | | + * |------+------+------+------+------+------|------+------+------+------+------+------| + * | | F7 | F8 | F9 | F10 | F11 | F12 |ISO ~ |ISO | | | | | + * |------+------+------+------+------+------+------+------+------+------+------+------| + * | | | | | | | | | Next | Vol- | Vol+ | Play | + * `-----------------------------------------------------------------------------------' + */ +[_LOWER] = LAYOUT_ortho_4x12( + KC_TILD, KC_EXLM, KC_AT, KC_HASH, KC_DLR, KC_PERC, KC_CIRC, KC_AMPR, KC_ASTR, KC_LPRN, KC_RPRN, KC_DEL, + KC_DEL, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_UNDS, KC_PLUS, KC_LCBR, KC_RCBR, KC_PIPE, + _______, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12,S(KC_NUHS),S(KC_NUBS),_______, _______, _______, + _______, _______, _______, _______, _______, _______, _______, _______, KC_MNXT, KC_VOLD, KC_VOLU, KC_MPLY +), + +/* Raise + * ,-----------------------------------------------------------------------------------. + * | ` | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | Del | + * |------+------+------+------+------+-------------+------+------+------+------+------| + * | Del | F1 | F2 | F3 | F4 | F5 | F6 | - | = | [ | ] | \ | + * |------+------+------+------+------+------|------+------+------+------+------+------| + * | | F7 | F8 | F9 | F10 | F11 | F12 |ISO # |ISO / | | | | + * |------+------+------+------+------+------+------+------+------+------+------+------| + * | | | | | | | | | Next | Vol- | Vol+ | Play | + * `-----------------------------------------------------------------------------------' + */ +[_RAISE] = LAYOUT_ortho_4x12( + KC_GRV, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_0, KC_DEL, + KC_DEL, KC_F1, KC_F2, KC_F3, KC_F4, KC_F5, KC_F6, KC_MINS, KC_EQL, KC_LBRC, KC_RBRC, KC_BSLS, + _______, KC_F7, KC_F8, KC_F9, KC_F10, KC_F11, KC_F12, KC_NUHS, KC_NUBS, _______, _______, _______, + _______, _______, _______, _______, _______, _______, _______, _______, KC_MNXT, KC_VOLD, KC_VOLU, KC_MPLY +), + +/* Adjust (Lower + Raise) + * ,-----------------------------------------------------------------------------------. + * | | Reset| | | | |R Tog |R Mode|R Rev |R Grad| Reset| | + * |------+------+------+------+------+-------------+------+------+------+------+------| + * | | | | | | |R HUI|R SAI|R VAI| | | | + * |------+------+------+------+------+------|------+------+------+------+------+------| + * | | | | | | |R HUD|R SAD|R VAD| | | | + * |------+------+------+------+------+------+------+------+------+------+------+------| + * | | | | | | | | | | | | | + * `-----------------------------------------------------------------------------------' + */ +[_ADJUST] = LAYOUT_ortho_4x12( + _______, QK_BOOT, _______, _______, _______, _______, RGB_TOG, RGB_MOD, RGB_RMOD,RGB_M_G, QK_BOOT, _______, + _______, _______, _______, _______, _______, _______, RGB_HUI, RGB_SAI, RGB_VAI, _______, _______, _______, + _______, _______, _______, _______, _______, _______, RGB_HUD, RGB_SAD, RGB_VAD, _______, _______, _______, + _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, _______ +) + +}; + +layer_state_t layer_state_set_user(layer_state_t state) { + return update_tri_layer_state(state, _LOWER, _RAISE, _ADJUST); +} + +#if defined(ENCODER_MAP_ENABLE) +const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][2] = { + [_QWERTY] = { ENCODER_CCW_CW(KC_MS_WH_UP, KC_MS_WH_DOWN), ENCODER_CCW_CW(KC_VOLD, KC_VOLU) }, + [_LOWER] = { ENCODER_CCW_CW(RGB_HUD, RGB_HUI), ENCODER_CCW_CW(RGB_SAD, RGB_SAI) }, + [_RAISE] = { ENCODER_CCW_CW(RGB_VAD, RGB_VAI), ENCODER_CCW_CW(RGB_SPD, RGB_SPI) }, + [_ADJUST] = { ENCODER_CCW_CW(RGB_RMOD, RGB_MOD), ENCODER_CCW_CW(KC_RIGHT, KC_LEFT) }, +}; +#endif + +void housekeeping_task_user(void) { + static uint32_t timer = 0; + static uint8_t count = 0; + if (timer_elapsed32(timer) > 1000) { + timer = timer_read32(); + count++; + + xap_broadcast(0x03, &count, 1); + } +} diff --git a/keyboards/zvecr/zv48/keymaps/xap/rules.mk b/keyboards/zvecr/zv48/keymaps/xap/rules.mk new file mode 100644 index 000000000000..1eb54897cdfd --- /dev/null +++ b/keyboards/zvecr/zv48/keymaps/xap/rules.mk @@ -0,0 +1,2 @@ +ENCODER_MAP_ENABLE = yes +XAP_ENABLE = yes diff --git a/keyboards/zvecr/zv48/keymaps/xap/xap.hjson b/keyboards/zvecr/zv48/keymaps/xap/xap.hjson new file mode 100644 index 000000000000..c406625ccd9c --- /dev/null +++ b/keyboards/zvecr/zv48/keymaps/xap/xap.hjson @@ -0,0 +1,16 @@ +{ + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY_USER + description: + ''' + USER subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_USER_CAPABILITIES + } + } +} \ No newline at end of file diff --git a/keyboards/zvecr/zv48/xap.hjson b/keyboards/zvecr/zv48/xap.hjson new file mode 100644 index 000000000000..5da9efc3ecce --- /dev/null +++ b/keyboards/zvecr/zv48/xap.hjson @@ -0,0 +1,16 @@ +{ + routes: { + 0x01: { + type: command + name: Capabilities Query + define: CAPABILITIES_QUERY_KB + description: + ''' + KB subsystem capabilities query. Each bit should be considered as a "usable" route within this subsystem. + ''' + return_type: u32 + return_purpose: capabilities + return_constant: XAP_ROUTE_KB_CAPABILITIES + } + } +} \ No newline at end of file diff --git a/lib/python/qmk/casing.py b/lib/python/qmk/casing.py new file mode 100755 index 000000000000..60b14cc5401d --- /dev/null +++ b/lib/python/qmk/casing.py @@ -0,0 +1,65 @@ +"""This script handles conversion between snake and camel casing. +""" +import re + +_words_expr = re.compile(r"([a-zA-Z][^A-Z0-9]*|[0-9]+)") +_lower_snake_case_expr = re.compile(r'^[a-z][a-z0-9_]*$') +_upper_snake_case_expr = re.compile(r'^[A-Z][A-Z0-9_]*$') + + +def _is_snake_case(str): + """Checks if the supplied string is already in snake case. + """ + match = _lower_snake_case_expr.match(str) + if match: + return True + match = _upper_snake_case_expr.match(str) + if match: + return True + return False + + +def _split_snake_case(str): + """Splits up a string based on underscores, if it's in snake casing. + """ + if _is_snake_case(str): + return [s.lower() for s in str.split("_")] + return str + + +def _split_camel_case(str): + """Splits up a string based on capitalised camel casing. + """ + return _words_expr.findall(str) + + +def _split_cased_words(str): + return _split_snake_case(str) if _is_snake_case(str) else _split_camel_case(str) + + +def to_snake(str): + str = "_".join([word.strip().lower() for word in _split_cased_words(str)]) + + # Fix acronyms + str = str.replace('i_d', 'id') + str = str.replace('x_a_p', 'xap') + str = str.replace('q_m_k', 'qmk') + + return str + + +def to_upper_snake(str): + return to_snake(str).upper() + + +def to_camel(str): + def _acronym(w): + if w.strip().lower() == 'qmk': + return 'QMK' + elif w.strip().lower() == 'xap': + return 'XAP' + elif w.strip().lower() == 'id': + return 'ID' + return w.title() + + return "".join([_acronym(word) for word in _split_cased_words(str)]) diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 0baf19a629df..2bfd53a06965 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -18,7 +18,8 @@ 'pyserial': 'serial', 'pyusb': 'usb.core', 'qmk-dotty-dict': 'dotty_dict', - 'pillow': 'PIL' + 'pillow': 'PIL', + 'Jinja2': 'jinja2' } safe_commands = [ @@ -58,6 +59,7 @@ 'qmk.cli.generate.keyboard_h', 'qmk.cli.generate.keycodes', 'qmk.cli.generate.keycodes_tests', + 'qmk.cli.generate.lighting_map', 'qmk.cli.generate.keymap_h', 'qmk.cli.generate.make_dependencies', 'qmk.cli.generate.rgb_breathe_table', @@ -90,6 +92,11 @@ 'qmk.cli.userspace.path', 'qmk.cli.userspace.remove', 'qmk.cli.via2json', + 'qmk.cli.xap', + 'qmk.cli.xap.generate_docs', + 'qmk.cli.xap.generate_json', + 'qmk.cli.xap.generate_python', + 'qmk.cli.xap.generate_qmk', ] diff --git a/lib/python/qmk/cli/format/python.py b/lib/python/qmk/cli/format/python.py index e7b545109f20..0c9f4cf28dbe 100755 --- a/lib/python/qmk/cli/format/python.py +++ b/lib/python/qmk/cli/format/python.py @@ -12,7 +12,7 @@ def yapf_run(files): edit = '--diff' if cli.args.dry_run else '--in-place' - yapf_cmd = ['yapf', '-vv', '--recursive', edit, *files] + yapf_cmd = ['yapf', '-vv', '--exclude', '**/xap_client/*', '--recursive', edit, *files] try: cli.run(yapf_cmd, check=True, capture_output=False, stdin=DEVNULL) cli.log.info('Successfully formatted the python code.') diff --git a/lib/python/qmk/cli/format/text.py b/lib/python/qmk/cli/format/text.py index 6dd451189652..ff44cf2cd981 100644 --- a/lib/python/qmk/cli/format/text.py +++ b/lib/python/qmk/cli/format/text.py @@ -1,24 +1,17 @@ """Ensure text files have the proper line endings. """ -from itertools import islice from subprocess import DEVNULL from milc import cli from qmk.path import normpath - - -def _get_chunks(it, size): - """Break down a collection into smaller parts - """ - it = iter(it) - return iter(lambda: tuple(islice(it, size)), ()) +from qmk.commands import get_chunks def dos2unix_run(files): """Spawn multiple dos2unix subprocess avoiding too long commands on formatting everything """ - for chunk in _get_chunks(files, 10): + for chunk in get_chunks(files, 10): dos2unix = cli.run(['dos2unix', *chunk]) if dos2unix.returncode: diff --git a/lib/python/qmk/cli/generate/api.py b/lib/python/qmk/cli/generate/api.py index 83118184764f..10654f73861e 100755 --- a/lib/python/qmk/cli/generate/api.py +++ b/lib/python/qmk/cli/generate/api.py @@ -2,6 +2,7 @@ """ from pathlib import Path import shutil +import hjson import json from milc import cli @@ -13,6 +14,7 @@ from qmk.keymap import list_keymaps from qmk.keyboard import find_readme, list_keyboards, keyboard_alias_definitions from qmk.keycodes import load_spec, list_versions, list_languages +from qmk.xap.common import get_xap_definition_files, update_xap_definitions DATA_PATH = Path('data') TEMPLATE_PATH = DATA_PATH / 'templates/api/' @@ -89,6 +91,22 @@ def _filtered_keyboard_list(): return keyboard_list +def _resolve_xap_specs(output_folder): + """To make it easier for consumers, publish pre-merged spec files + """ + overall = None + for file in get_xap_definition_files(): + overall = update_xap_definitions(overall, hjson.load(file.open(encoding='utf-8'))) + + # Inject dummy bits for unspecified response flags + for n in range(0, 8): + if str(n) not in overall['response_flags']['bits']: + overall['response_flags']['bits'][str(n)] = {'name': '', 'description': '', 'define': '-'} + + output_file = output_folder / (file.stem + ".json") + output_file.write_text(json.dumps(overall, indent=4), encoding='utf-8') + + @cli.argument('-n', '--dry-run', arg_only=True, action='store_true', help="Don't write the data to disk.") @cli.argument('-f', '--filter', arg_only=True, action='append', default=[], help="Filter the list of keyboards based on partial name matches the supplied value. May be passed multiple times.") @cli.subcommand('Generate QMK API data', hidden=False if cli.config.user.developer else True) @@ -181,6 +199,7 @@ def generate_api(cli): # Feature specific handling _resolve_keycode_specs(v1_dir) + _resolve_xap_specs(v1_dir / 'xap') # Write the global JSON files keyboard_all_json = json.dumps({'last_updated': current_datetime(), 'keyboards': kb_all}, separators=(',', ':')) diff --git a/lib/python/qmk/cli/generate/lighting_map.py b/lib/python/qmk/cli/generate/lighting_map.py new file mode 100644 index 000000000000..98606348709b --- /dev/null +++ b/lib/python/qmk/cli/generate/lighting_map.py @@ -0,0 +1,126 @@ +from milc import cli + +from qmk.path import normpath +from qmk.commands import dump_lines +from qmk.lighting import load_lighting_spec +from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE + +PREFIX_MAP = { + 'rgblight': { + 'ifdef': 'RGBLIGHT_EFFECT', + 'def': 'RGBLIGHT_MODE', + }, + 'rgb_matrix': { + 'ifdef': 'ENABLE_RGB_MATRIX', + 'def': 'RGB_MATRIX', + }, + 'led_matrix': { + 'ifdef': 'ENABLE_LED_MATRIX', + 'def': 'LED_MATRIX', + }, +} + + +def _always_enabled(id): + """Assumption that first effect is always enabled + """ + return id == '0x00' + + +def _wrap_ifdef(line, define): + return f''' +#ifdef {define} +{line} +#endif''' + + +def _append_lighting_map(lines, feature, spec): + """Translate effect to 'constant id'->'firmware id' lookup table + """ + groups = spec.get('groups', {}) + ifdef_prefix = PREFIX_MAP[feature]['ifdef'] + def_prefix = PREFIX_MAP[feature]['def'] + + lines.append(f'static const uint8_t {feature}_effect_map[][2] PROGMEM = {{') + for id, obj in spec.get('effects', {}).items(): + define = obj['define'] + offset = f' + {obj["offset"]}' if obj['offset'] else '' + + line = f'{{ {id}, {def_prefix}_{define}{offset}}},' + + if not _always_enabled(id): + line = _wrap_ifdef(line, f'{ifdef_prefix}_{define}') + + group = groups.get(obj.get('group', None), {}).get('define', None) + if group: + line = _wrap_ifdef(line, group) + + lines.append(line) + + lines.append('};') + + # add helper funcs + lines.append( + f''' +uint8_t {feature}_effect_to_id(uint8_t val) {{ + for(uint8_t i = 0; i < ARRAY_SIZE({feature}_effect_map); i++) {{ + if (pgm_read_byte(&{feature}_effect_map[i][1]) == val) + return pgm_read_byte(&{feature}_effect_map[i][0]); + }} + return 0xFF; +}} + +uint8_t {feature}_id_to_effect(uint8_t val) {{ + for(uint8_t i = 0; i < ARRAY_SIZE({feature}_effect_map); i++) {{ + if (pgm_read_byte(&{feature}_effect_map[i][0]) == val) + return pgm_read_byte(&{feature}_effect_map[i][1]); + }} + return 0xFF; +}}''' + ) + + +def _append_lighting_bit_field(lines, feature, spec): + """Translate effect to bit of bit-field + """ + groups = spec.get('groups', {}) + ifdef_prefix = PREFIX_MAP[feature]['ifdef'] + + lines.append(f'enum {{ ENABLED_{feature.upper()}_EFFECTS = 0') + for id, obj in spec.get('effects', {}).items(): + define = obj['define'] + + line = f' | (1ULL << {id})' + + if not _always_enabled(id): + line = _wrap_ifdef(line, f'{ifdef_prefix}_{define}') + + group = groups.get(obj.get('group', None), {}).get('define', None) + if group: + line = _wrap_ifdef(line, group) + + lines.append(line) + + lines.append('};') + + +def _append_lighting_mapping(lines, feature): + """Generate lookup table and bit-field of effect + """ + spec = load_lighting_spec(feature) + + _append_lighting_bit_field(lines, feature, spec) + _append_lighting_map(lines, feature, spec) + + +@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('-f', '--feature', required=True, help='Feature to generate map', choices=PREFIX_MAP.keys()) +@cli.subcommand('Generates effect header.') +def generate_lighting_map(cli): + # Preamble + lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '// clang-format off'] + + _append_lighting_mapping(lines, cli.args.feature) + + dump_lines(cli.args.output, lines, cli.args.quiet) diff --git a/lib/python/qmk/cli/xap/__init__.py b/lib/python/qmk/cli/xap/__init__.py new file mode 100755 index 000000000000..190f3607ecec --- /dev/null +++ b/lib/python/qmk/cli/xap/__init__.py @@ -0,0 +1 @@ +from .xap import xap diff --git a/lib/python/qmk/cli/xap/generate_docs.py b/lib/python/qmk/cli/xap/generate_docs.py new file mode 100755 index 000000000000..778da5a5492d --- /dev/null +++ b/lib/python/qmk/cli/xap/generate_docs.py @@ -0,0 +1,58 @@ +"""This script generates the XAP protocol documentation. +""" +import hjson + +from milc import cli + +from qmk.constants import QMK_FIRMWARE +from qmk.path import normpath +from qmk.commands import dump_lines +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.xap.common import get_xap_definition_files, update_xap_definitions, merge_xap_defs, render_xap_output + + +def _patch_spec_for_docs(spec): + # Inject dummy bits for unspecified response flags + for n in range(0, 8): + if str(n) not in spec['response_flags']['bits']: + spec['response_flags']['bits'][str(n)] = {'name': '', 'description': '', 'define': '-'} + + +@cli.subcommand('Generates the XAP protocol documentation.', hidden=False if cli.config.user.developer else True) +def xap_generate_docs(cli): + """Generates the XAP protocol documentation by merging the definitions files, and producing the corresponding Markdown document under `/docs/`. + """ + versions = [] + + overall = None + for file in get_xap_definition_files(): + overall = update_xap_definitions(overall, hjson.load(file.open(encoding='utf-8'))) + + _patch_spec_for_docs(overall) + + output_doc = QMK_FIRMWARE / "docs" / f"{file.stem}.md" + versions.append(overall['version']) + output = render_xap_output('docs', 'docs.md.j2', overall) + with open(output_doc, "w", encoding='utf-8') as out_file: + out_file.write(output) + + output_doc = QMK_FIRMWARE / "docs" / "xap_protocol.md" + output = render_xap_output('docs', 'versions.md.j2', overall, versions=versions) + with open(output_doc, "w", encoding='utf-8') as out_file: + out_file.write(output) + + +@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('-km', '--keymap', help='The keymap\'s name - "default" if not specified') +@cli.argument('-kb', '--keyboard', required=True, type=keyboard_folder, completer=keyboard_completer, help='Name of the keyboard') +@cli.subcommand('Generates the XAP protocol documentation for a given keyboard/keymap.', hidden=False if cli.config.user.developer else True) +def xap_generate_keyboard_docs(cli): + """Generates the XAP protocol documentation for a given keyboard/keymap and producing the corresponding Markdown. + """ + spec = merge_xap_defs(cli.args.keyboard, cli.args.keymap or 'default') + + _patch_spec_for_docs(spec) + + output = render_xap_output('docs', 'docs.md.j2', spec) + dump_lines(cli.args.output, output.split('\n'), cli.args.quiet) diff --git a/lib/python/qmk/cli/xap/generate_json.py b/lib/python/qmk/cli/xap/generate_json.py new file mode 100755 index 000000000000..e999f6ce87dd --- /dev/null +++ b/lib/python/qmk/cli/xap/generate_json.py @@ -0,0 +1,13 @@ +"""This script generates the consolidated XAP protocol definitions. +""" +import hjson +from milc import cli +from qmk.xap.common import latest_xap_defs + + +@cli.subcommand('Generates the consolidated XAP protocol definitions.', hidden=False if cli.config.user.developer else True) +def xap_generate_json(cli): + """Generates the consolidated XAP protocol definitions. + """ + defs = latest_xap_defs() + print(hjson.dumps(defs)) diff --git a/lib/python/qmk/cli/xap/generate_python.py b/lib/python/qmk/cli/xap/generate_python.py new file mode 100644 index 000000000000..23fb7a81046b --- /dev/null +++ b/lib/python/qmk/cli/xap/generate_python.py @@ -0,0 +1,19 @@ +"""This script generates the python XAP client. +""" +from milc import cli + +from qmk.commands import dump_lines +from qmk.constants import QMK_FIRMWARE +from qmk.xap.common import latest_xap_defs, render_xap_output + + +@cli.subcommand('Generates the python XAP client.', hidden=False if cli.config.user.developer else True) +def xap_generate_python(cli): + defs = latest_xap_defs() + + parent = QMK_FIRMWARE / 'lib' / 'python' / 'xap_client' + for name in ['types.py', 'routes.py', 'constants.py']: + output = render_xap_output('client/python', f'{name}.j2', defs) + lines = output.split('\n') + + dump_lines(parent / name, lines) diff --git a/lib/python/qmk/cli/xap/generate_qmk.py b/lib/python/qmk/cli/xap/generate_qmk.py new file mode 100755 index 000000000000..67ec93227477 --- /dev/null +++ b/lib/python/qmk/cli/xap/generate_qmk.py @@ -0,0 +1,78 @@ +"""This script generates the XAP protocol generated sources to be compiled into QMK firmware. +""" +from milc import cli + +from qmk.path import normpath +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.xap.common import render_xap_output, merge_xap_defs +from qmk.xap.gen_firmware.blob_generator import generate_blob + + +@cli.argument('-o', '--output', type=normpath, help='File to write to') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Name of the keyboard') +@cli.argument('-km', '--keymap', help='The keymap\'s name') +@cli.subcommand('Generates the XAP protocol include.', hidden=False if cli.config.user.developer else True) +def xap_generate_qmk_inc(cli): + """Generates the XAP protocol inline codegen file, generated during normal build. + """ + # Determine our keyboard/keymap + if not cli.args.keyboard: + cli.log.error('Missing parameter: --keyboard') + cli.subcommands['xap-generate-qmk-inc'].print_help() + return False + if not cli.args.keymap: + cli.log.error('Missing parameter: --keymap') + cli.subcommands['xap-generate-qmk-inc'].print_help() + return False + + defs = merge_xap_defs(cli.args.keyboard, cli.args.keymap) + with open(cli.args.output, 'w', encoding='utf-8') as out_file: + r = render_xap_output('firmware', 'xap_generated.inl.j2', defs, keyboard=cli.args.keyboard, keymap=cli.args.keymap) + while r.find('\n\n\n') != -1: + r = r.replace('\n\n\n', '\n\n') + out_file.write(r) + + +@cli.argument('-o', '--output', type=normpath, help='File to write to') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Name of the keyboard') +@cli.argument('-km', '--keymap', help='The keymap\'s name') +@cli.subcommand('Generates the XAP protocol include.', hidden=False if cli.config.user.developer else True) +def xap_generate_qmk_h(cli): + """Generates the XAP protocol header file, generated during normal build. + """ + # Determine our keyboard/keymap + if not cli.args.keyboard: + cli.log.error('Missing parameter: --keyboard') + cli.subcommands['xap-generate-qmk-h'].print_help() + return False + if not cli.args.keymap: + cli.log.error('Missing parameter: --keymap') + cli.subcommands['xap-generate-qmk-h'].print_help() + return False + + defs = merge_xap_defs(cli.args.keyboard, cli.args.keymap) + with open(cli.args.output, 'w', encoding='utf-8') as out_file: + r = render_xap_output('firmware', 'xap_generated.h.j2', defs, keyboard=cli.args.keyboard, keymap=cli.args.keymap) + while r.find('\n\n\n') != -1: + r = r.replace('\n\n\n', '\n\n') + out_file.write(r) + + +@cli.argument('-o', '--output', type=normpath, help='File to write to') +@cli.argument('-kb', '--keyboard', type=keyboard_folder, completer=keyboard_completer, help='Name of the keyboard') +@cli.argument('-km', '--keymap', help='The keymap\'s name') +@cli.subcommand('Generates the XAP config payload include.', hidden=False if cli.config.user.developer else True) +def xap_generate_qmk_blob_h(cli): + """Generates the XAP config payload header file, generated during normal build. + """ + # Determine our keyboard/keymap + if not cli.args.keyboard: + cli.log.error('Missing parameter: --keyboard') + cli.subcommands['xap-generate-qmk-blob-h'].print_help() + return False + if not cli.args.keymap: + cli.log.error('Missing parameter: --keymap') + cli.subcommands['xap-generate-qmk-blob-h'].print_help() + return False + + generate_blob(cli.args.output, cli.args.keyboard, cli.args.keymap) diff --git a/lib/python/qmk/cli/xap/xap.py b/lib/python/qmk/cli/xap/xap.py new file mode 100644 index 000000000000..61189962179a --- /dev/null +++ b/lib/python/qmk/cli/xap/xap.py @@ -0,0 +1,283 @@ +"""Interactions with compatible XAP devices +""" +import cmd + +from milc import cli + +from qmk.keycodes import load_spec +from qmk.decorators import lru_cache +from qmk.keyboard import render_layout + +from xap_client import XAPClient, XAPEventType, XAPSecureStatus, XAPConfigRgblight, XAPConfigBacklight, XAPConfigRgbMatrix, XAPRoutes + + +def print_dotted_output(kb_info_json, prefix=''): + """Print the info.json in a plain text format with dot-joined keys. + """ + for key in sorted(kb_info_json): + new_prefix = f'{prefix}.{key}' if prefix else key + + if key in ['parse_errors', 'parse_warnings']: + continue + elif key == 'layouts' and prefix == '': + cli.echo(' {fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys()))) + elif isinstance(kb_info_json[key], bytes): + conv = "".join(["{:02X}".format(b) for b in kb_info_json[key]]) + cli.echo(' {fg_blue}%s{fg_reset}: %s', new_prefix, conv) + elif isinstance(kb_info_json[key], dict): + print_dotted_output(kb_info_json[key], new_prefix) + elif isinstance(kb_info_json[key], list): + data = kb_info_json[key] + if len(data) and isinstance(data[0], dict): + for index, item in enumerate(data, start=0): + cli.echo(' {fg_blue}%s.%s{fg_reset}: %s', new_prefix, index, str(item)) + else: + cli.echo(' {fg_blue}%s{fg_reset}: %s', new_prefix, ', '.join(map(str, data))) + else: + cli.echo(' {fg_blue}%s{fg_reset}: %s', new_prefix, kb_info_json[key]) + + +@lru_cache(timeout=5) +def _load_keycodes(keycode_version): + """Gets keycode data for the required version of the XAP definitions. + """ + spec = load_spec(keycode_version) + + # Transform into something more usable - { raw_value : first alias || keycode } + ret = {int(k, 16): v.get('aliases', [v.get('key')])[0] for k, v in spec['keycodes'].items()} + + # TODO: handle non static keycodes + for k, v in spec['ranges'].items(): + lo, mask = map(lambda x: int(x, 16), k.split('/')) + hi = lo + mask + define = v.get("define") + for i in range(lo, hi): + if i not in ret: + if define == 'QK_TO': + layer = i & 0x1F + ret[i] = f'TO({layer})' + elif define == 'QK_MOMENTARY': + layer = i & 0x1F + ret[i] = f'MO({layer})' + elif define == 'QK_LAYER_TAP': + layer = (((i) >> 8) & 0xF) + keycode = ((i) & 0xFF) + ret[i] = f'LT({layer}, {ret.get(keycode, "???")})' + + return ret + + +def _list_devices(): + """Dump out available devices + """ + cli.log.info('Available devices:') + for dev in XAPClient.devices(): + device = XAPClient().connect(dev) + ver = device.version() + + cli.log.info(' %04x:%04x %s %s [API:%s]', dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string'], ver['xap']) + + if cli.args.verbose: + data = device.info() + # TODO: better formatting like 'lsusb -v'? + print_dotted_output(data) + + +class XAPShell(cmd.Cmd): + intro = 'Welcome to the XAP shell. Type help or ? to list commands.\n' + prompt = 'Ψ> ' + + def __init__(self, device): + cmd.Cmd.__init__(self) + self.device = device + # cache keycodes for this device + self.keycodes = _load_keycodes(device.version().get('keycodes', 'latest')) + + # TODO: dummy code is only to PoC kb/user keycodes + kb_keycodes = self.device.info().get('keycodes', []) + for index, item in enumerate(kb_keycodes): + self.keycodes[0x7E00 + index] = item['key'] + + user_keycodes = self.device.info().get('user_keycodes', []) + for index, item in enumerate(user_keycodes): + self.keycodes[0x7E40 + index] = item['key'] + + def do_about(self, arg): + """Prints out the version info of QMK + """ + data = self.device.version() + print_dotted_output(data) + + def do_status(self, arg): + """Prints out the current device state + """ + status = self.device.status() + print('Secure:%s' % status.get('lock', '???')) + + def do_unlock(self, arg): + """Initiate secure unlock + """ + self.device.unlock() + print('Unlock Requested...') + + def do_lock(self, arg): + """Disable secure routes + """ + self.device.lock() + + def do_reset(self, arg): + """Jump to bootloader if unlocked + """ + if not self.device.reset(): + print("Reboot to bootloader failed") + return True + + def do_listen(self, arg): + """Log out XAP broadcast messages + """ + try: + cli.log.info('Listening for XAP broadcasts...') + while 1: + (event, data) = self.device.listen() + + if event == XAPEventType.SECURE_STATUS: + secure_status = XAPSecureStatus(data[0]).name + + cli.log.info(' Secure[%s]', secure_status) + else: + cli.log.info(' Broadcast: type[%02x] data:[%s]', event, data.hex()) + + except KeyboardInterrupt: + cli.log.info('Stopping...') + + def do_keycode(self, arg): + """Prints out the keycode value of a certain layer, row, and column + """ + data = bytes(map(int, arg.split())) + if len(data) != 3: + cli.log.error('Invalid args') + return + + keycode = self.device.transaction(b'\x04\x03', data) + keycode = int.from_bytes(keycode, 'little') + print(f'keycode:{self.keycodes.get(keycode, "unknown")}[{keycode}]') + + def do_keymap(self, arg): + """Prints out the keycode values of a certain layer + """ + data = bytes(map(int, arg.split())) + if len(data) != 1: + cli.log.error('Invalid args') + return + + info = self.device.info() + rows = info['matrix_size']['rows'] + cols = info['matrix_size']['cols'] + + for r in range(rows): + for c in range(cols): + q = data + r.to_bytes(1, byteorder='little') + c.to_bytes(1, byteorder='little') + keycode = self.device.transaction(b'\x04\x03', q) + keycode = int.from_bytes(keycode, 'little') + print(f'| {self.keycodes.get(keycode, "unknown").ljust(7)} ', end='', flush=True) + print('|') + + def do_layer(self, arg): + """Renders keycode values of a certain layer + """ + data = bytes(map(int, arg.split())) + if len(data) != 1: + cli.log.error('Invalid args') + return + + info = self.device.info() + + # Assumptions on selected layout rather than prompt + first_layout = next(iter(info['layouts'])) + layout = info['layouts'][first_layout]['layout'] + + keycodes = [] + for item in layout: + q = data + bytes(item['matrix']) + keycode = self.device.transaction(b'\x04\x03', q) + keycode = int.from_bytes(keycode, 'little') + keycodes.append(self.keycodes.get(keycode, '???')) + + print(render_layout(layout, False, keycodes)) + + def do_exit(self, line): + """Quit shell + """ + return True + + def do_EOF(self, line): # noqa: N802 + """Quit shell (ctrl+D) + """ + return True + + def loop(self): + """Wrapper for cmdloop that handles ctrl+C + """ + try: + self.cmdloop() + print('') + except KeyboardInterrupt: + print('^C') + return False + + def do_dump(self, line): + caps = self.device.int_transaction(XAPRoutes.LIGHTING_CAPABILITIES_QUERY) + + if caps & (1 << XAPRoutes.LIGHTING_BACKLIGHT[-1]): + ret = self.device.transaction(XAPRoutes.LIGHTING_BACKLIGHT_GET_CONFIG) + ret = XAPConfigBacklight.from_bytes(ret) + print(ret) + + ret = self.device.int_transaction(XAPRoutes.LIGHTING_BACKLIGHT_GET_ENABLED_EFFECTS) + print(f'XAPEffectBacklight(enabled={bin(ret)})') + + if caps & (1 << XAPRoutes.LIGHTING_RGBLIGHT[-1]): + ret = self.device.transaction(XAPRoutes.LIGHTING_RGBLIGHT_GET_CONFIG) + ret = XAPConfigRgblight.from_bytes(ret) + print(ret) + + ret = self.device.int_transaction(XAPRoutes.LIGHTING_RGBLIGHT_GET_ENABLED_EFFECTS) + print(f'XAPEffectRgblight(enabled={bin(ret)})') + + if caps & (1 << XAPRoutes.LIGHTING_RGB_MATRIX[-1]): + ret = self.device.transaction(XAPRoutes.LIGHTING_RGB_MATRIX_GET_CONFIG) + ret = XAPConfigRgbMatrix.from_bytes(ret) + print(ret) + + ret = self.device.int_transaction(XAPRoutes.LIGHTING_RGB_MATRIX_GET_ENABLED_EFFECTS) + print(f'XAPEffectRgbMatrix(enabled={bin(ret)})') + + +@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.') +@cli.argument('-d', '--device', help='device to select - uses format :.') +@cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available devices.') +@cli.argument('-i', '--interactive', arg_only=True, action='store_true', help='Start interactive shell.') +@cli.argument('action', nargs='*', arg_only=True, default=['listen'], help='Shell command and any arguments to run standalone') +@cli.subcommand('Acquire debugging information from usb XAP devices.', hidden=False if cli.config.user.developer else True) +def xap(cli): + """Acquire debugging information from XAP devices + """ + if cli.args.list: + return _list_devices() + + # Connect to first available device + devices = XAPClient.devices() + if not devices: + cli.log.error('No devices found!') + return False + + dev = devices[0] + cli.log.info('Connecting to: %04x:%04x %s %s', dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) + device = XAPClient().connect(dev) + + # shell? + if cli.args.interactive: + XAPShell(device).loop() + return True + + XAPShell(device).onecmd(' '.join(cli.args.action)) diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py index 97d9c5032c8f..3b9210930a08 100644 --- a/lib/python/qmk/commands.py +++ b/lib/python/qmk/commands.py @@ -3,6 +3,7 @@ import os import sys import shutil +from itertools import islice from pathlib import Path from milc import cli @@ -98,6 +99,13 @@ def in_virtualenv(): return active_prefix != sys.prefix +def get_chunks(it, size): + """Break down a collection into smaller parts + """ + it = iter(it) + return iter(lambda: tuple(islice(it, size)), ()) + + def dump_lines(output_file, lines, quiet=True): """Handle dumping to stdout or file Creates parent folders if required diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index b6f46180b298..589f942dbc8e 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -147,6 +147,11 @@ # SPDX-License-Identifier: GPL-2.0-or-later ''' +GPL2_HEADER_XML_LIKE = f'''\ + + +''' + GENERATED_HEADER_C_LIKE = '''\ /******************************************************************************* 88888888888 888 d8b .d888 d8b 888 d8b @@ -198,6 +203,32 @@ ################################################################################ ''' +GENERATED_HEADER_XML_LIKE = '''\ + +''' + LICENSE_TEXTS = [ ( 'GPL-2.0-or-later', [ diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index 0c6dd181d9ba..a985ba5db2a8 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -1019,7 +1019,13 @@ def keymap_json_config(keyboard, keymap, force_layout=None): keymap_folder = locate_keymap(keyboard, keymap, force_layout=force_layout).parent km_info_json = parse_configurator_json(keymap_folder / 'keymap.json') - return km_info_json.get('config', {}) + ret = km_info_json.get('config', {}) + + # TODO: dummy code is only to PoC kb/user keycodes + if 'keycodes' in km_info_json: + ret['user_keycodes'] = km_info_json['keycodes'] + + return ret def keymap_json(keyboard, keymap, force_layout=None): diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py index fcf5b5b15888..d82ce5261fef 100644 --- a/lib/python/qmk/keyboard.py +++ b/lib/python/qmk/keyboard.py @@ -5,6 +5,7 @@ from math import ceil from pathlib import Path import os +import shutil from glob import glob import qmk.path @@ -12,6 +13,7 @@ from qmk.json_schema import json_load from qmk.makefile import parse_rules_mk_file +KEY_WIDTH = 4 if shutil.get_terminal_size().columns < 160 else 6 BOX_DRAWING_CHARACTERS = { "unicode": { "tl": "┌", @@ -295,9 +297,9 @@ def render_layouts(info_json, render_ascii): def render_key_rect(textpad, x, y, w, h, label, style): box_chars = BOX_DRAWING_CHARACTERS[style] - x = ceil(x * 4) + x = ceil(x * KEY_WIDTH) y = ceil(y * 3) - w = ceil(w * 4) + w = ceil(w * KEY_WIDTH) h = ceil(h * 3) label_len = w - 2 @@ -324,9 +326,9 @@ def render_key_rect(textpad, x, y, w, h, label, style): def render_key_isoenter(textpad, x, y, w, h, label, style): box_chars = BOX_DRAWING_CHARACTERS[style] - x = ceil(x * 4) + x = ceil(x * KEY_WIDTH) y = ceil(y * 3) - w = ceil(w * 4) + w = ceil(w * KEY_WIDTH) h = ceil(h * 3) label_len = w - 1 @@ -356,9 +358,9 @@ def render_key_isoenter(textpad, x, y, w, h, label, style): def render_key_baenter(textpad, x, y, w, h, label, style): box_chars = BOX_DRAWING_CHARACTERS[style] - x = ceil(x * 4) + x = ceil(x * KEY_WIDTH) y = ceil(y * 3) - w = ceil(w * 4) + w = ceil(w * KEY_WIDTH) h = ceil(h * 3) label_len = w + 1 diff --git a/lib/python/qmk/lighting.py b/lib/python/qmk/lighting.py new file mode 100644 index 000000000000..2a81cf9e8d79 --- /dev/null +++ b/lib/python/qmk/lighting.py @@ -0,0 +1,37 @@ +import re +from pathlib import Path + +from qmk.json_schema import json_load + + +def list_lighting_versions(feature): + """Return available versions - sorted newest first + """ + ret = [] + for file in Path('data/constants/lighting/').glob(f'{feature}_[0-9].[0-9].[0-9].hjson'): + 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/lighting/{feature}_{version}.hjson')) + + # 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 diff --git a/lib/python/qmk/xap/__init__.py b/lib/python/qmk/xap/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lib/python/qmk/xap/common.py b/lib/python/qmk/xap/common.py new file mode 100755 index 000000000000..03d188804c42 --- /dev/null +++ b/lib/python/qmk/xap/common.py @@ -0,0 +1,140 @@ +"""This script handles the XAP protocol data files. +""" +import hjson +import jsonschema +from pathlib import Path +from typing import OrderedDict +from jinja2 import Environment, FileSystemLoader, select_autoescape + +import qmk.constants +from qmk.git import git_get_version +from qmk.lighting import load_lighting_spec +from qmk.json_schema import validate, merge_ordered_dicts +from qmk.makefile import parse_rules_mk_file +from qmk.decorators import lru_cache +from qmk.keymap import locate_keymap +from qmk.path import keyboard +from qmk.xap.jinja2_filters import attach_filters + +USERSPACE_DIR = Path('users') +XAP_SPEC = 'xap.hjson' + + +def _get_jinja2_env(data_templates_xap_subdir: str): + templates_dir = qmk.constants.QMK_FIRMWARE / 'data/templates/xap' / data_templates_xap_subdir + j2 = Environment(loader=FileSystemLoader(templates_dir), autoescape=select_autoescape(), lstrip_blocks=True, trim_blocks=True) + return j2 + + +def render_xap_output(data_templates_xap_subdir, file_to_render, defs=None, **kwargs): + if defs is None: + defs = latest_xap_defs() + j2 = _get_jinja2_env(data_templates_xap_subdir) + + attach_filters(j2) + + specs = {} + for feature in ['rgblight', 'rgb_matrix', 'led_matrix']: + specs[feature] = load_lighting_spec(feature) + + 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) + + +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): + 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 + + +def get_xap_definition_files(): + """Get the sorted list of XAP definition files, from /data/xap. + """ + xap_defs = qmk.constants.QMK_FIRMWARE / "data" / "xap" + 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() + return merge_ordered_dicts([original, new]) + + +@lru_cache(timeout=5) +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 + if version != 'latest': + index = [idx for idx, s in enumerate(files) if version in str(s)][0] + files = files[:(index + 1)] + + definitions = [hjson.load(file.open(encoding='utf-8')) for file in files] + return merge_ordered_dicts(definitions) + + +def latest_xap_defs(): + """Gets the latest version of the XAP definitions. + """ + return get_xap_defs('latest') + + +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'))}}) + + defs = merge_ordered_dicts(definitions) + + try: + validate(defs, 'qmk.xap.v1') + + except jsonschema.ValidationError as e: + print(f'Invalid XAP spec: {e.message}') + exit(1) + + return defs diff --git a/lib/python/qmk/xap/gen_client_js/__init__.py b/lib/python/qmk/xap/gen_client_js/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lib/python/qmk/xap/gen_firmware/__init__.py b/lib/python/qmk/xap/gen_firmware/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lib/python/qmk/xap/gen_firmware/blob_generator.py b/lib/python/qmk/xap/gen_firmware/blob_generator.py new file mode 100644 index 000000000000..319b1f22ed78 --- /dev/null +++ b/lib/python/qmk/xap/gen_firmware/blob_generator.py @@ -0,0 +1,72 @@ +"""This script generates the XAP info.json payload header to be compiled into QMK. +""" +import json +import gzip +from pathlib import Path + +from qmk.info import keymap_json +from qmk.commands import get_chunks, dump_lines +from qmk.json_schema import deep_update, json_load +from qmk.json_encoders import InfoJSONEncoder +from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE + + +def _build_info(keyboard, keymap): + """Build the xap version of info.json + """ + defaults_json = json_load(Path('data/mappings/xap_defaults.json')) + km_info_json = keymap_json(keyboard, keymap) + + info_json = {} + deep_update(info_json, defaults_json) + deep_update(info_json, km_info_json) + + # TODO: Munge to XAP requirements + info_json.pop('config_h_features', None) + info_json.pop('keymaps', None) + info_json.pop('parse_errors', None) + info_json.pop('parse_warnings', None) + info_json.get('usb', {}).pop('device_ver', None) + for layout in info_json.get('layouts', {}).values(): + layout.pop('filename', None) + layout.pop('c_macro', None) + for item in layout.get('layout', []): + item.pop('label', None) + + return info_json + + +def generate_blob(output_file, keyboard, keymap): + """Generate XAP payload + """ + info_json = _build_info(keyboard, keymap) + + # Minify + str_data = json.dumps(info_json, separators=(',', ':')) + + # Compress + compressed = gzip.compress(str_data.encode("utf-8"), compresslevel=9) + + # split into lines to match xxd output + hex_array = ["0x{:02X}".format(b) for b in compressed] + data_len = len(hex_array) + + data = "" + for chunk in get_chunks(hex_array, 12): + data += f' {", ".join(chunk)},\n' + + lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', ''] + + lines.append('#if 0') + lines.append('// Blob contains a minified+gzipped version of the following:') + lines.append(json.dumps(info_json, cls=InfoJSONEncoder)) + lines.append('#endif') + lines.append('') + + # Gen output file + lines.append('static const unsigned char config_blob_gz[] PROGMEM = {') + lines.append(data) + lines.append('};') + lines.append(f'#define CONFIG_BLOB_GZ_LEN {data_len}') + + dump_lines(output_file, lines) diff --git a/lib/python/qmk/xap/jinja2_filters.py b/lib/python/qmk/xap/jinja2_filters.py new file mode 100644 index 000000000000..043d20f8c3ac --- /dev/null +++ b/lib/python/qmk/xap/jinja2_filters.py @@ -0,0 +1,77 @@ +"""This script enables attachment of XAP-specific filters to Jinja2 +""" +import re +from fnvhash import fnv1a_32 +from jinja2 import Environment + +from qmk.casing import to_snake + +TRIPLET_PATTERN = re.compile(r'^(\d+)\.(\d+)\.(\d+)') +TYPE_ARRAY_PATTERN = re.compile(r'^([^\[]+)\[([^[\]]+)\]$') + + +def _fnv1a_32(s: str): + res = fnv1a_32(bytes(s, 'utf-8')) + return f'0x{res:08X}' + + +def _xap_type_to_c_before(xt: str): + m = TYPE_ARRAY_PATTERN.match(xt) + if m: + return _xap_type_to_c(m.group(1)) + if xt == 'u8': + return 'uint8_t' + elif xt == 'u16': + return 'uint16_t' + elif xt == 'u32': + return 'uint32_t' + elif xt == 'u64': + return 'uint64_t' + elif xt == 'bool': + return 'uint8_t' + elif xt == 'string': + return 'const char*' + elif xt == 'token': + return 'xap_token_t' + elif xt == 'response_flags': + return 'xap_response_flags_t' + raise TypeError(f'Unknown XAP type: {xt}') + + +def _xap_type_to_c_after(xt: str): + m = TYPE_ARRAY_PATTERN.match(xt) + if m: + try: + extent = int(m.group(2)) + return f'[{extent}]' + except ValueError: + return '[]' + return '' + + +def _xap_type_to_c(xt: str, name: str = None): + if name is not None: + name = re.sub(' ', '_', name) + return f'{(_xap_type_to_c_before(xt))} {name}{(_xap_type_to_c_after(xt))}' + return f'{(_xap_type_to_c_before(xt))}{(_xap_type_to_c_after(xt))}' + + +def _triplet_to_bcd(value: str): + m = TRIPLET_PATTERN.match(value) + if not m: + return '0' + return f'0x{int(m.group(1)):02d}{int(m.group(2)):02d}{int(m.group(3)):04d}' + + +def _newline_to_br(value: str): + return value.replace('\n', '
') + + +def attach_filters(j2: Environment): + j2.filters['to_snake'] = to_snake + j2.filters['newline_to_br'] = _newline_to_br + j2.filters['triplet_to_bcd'] = _triplet_to_bcd + j2.filters['fnv1a_32'] = _fnv1a_32 + j2.filters['type_to_c'] = _xap_type_to_c + j2.filters['type_to_c_before'] = _xap_type_to_c_before + j2.filters['type_to_c_after'] = _xap_type_to_c_after diff --git a/lib/python/xap_client/__init__.py b/lib/python/xap_client/__init__.py new file mode 100644 index 000000000000..301f1c46fffe --- /dev/null +++ b/lib/python/xap_client/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2022 QMK +# SPDX-License-Identifier: GPL-2.0-or-later +from .types import * # noqa: F403 +from .client import * # noqa: F403 +from .routes import * # noqa: F403 +from .constants import * # noqa: F403 diff --git a/lib/python/xap_client/client.py b/lib/python/xap_client/client.py new file mode 100644 index 000000000000..f4631b871228 --- /dev/null +++ b/lib/python/xap_client/client.py @@ -0,0 +1,39 @@ +# Copyright 2022 QMK +# SPDX-License-Identifier: GPL-2.0-or-later +from typing import List + + +class XAPClient: + """XAP device discovery + """ + @staticmethod + def devices(search: str = None) -> List[dict]: + """Find compatible XAP devices + + Args: + search: optional search string to filter results by + """ + def _is_xap_usage(x): + return x['usage_page'] == 0xFF51 and x['usage'] == 0x0058 + + def _is_filtered_device(x): + name = '%04x:%04x' % (x['vendor_id'], x['product_id']) + return name.lower().startswith(search.lower()) + + # lazy import to avoid compile issues + import hid + + devices = filter(_is_xap_usage, hid.enumerate()) + if search: + devices = filter(_is_filtered_device, devices) + + return list(devices) + + def connect(self, device: dict): + """Connect to a given XAP device + Args: + device: item from a previous `XAPClient.devices()` call + """ + from .device import XAPDevice + + return XAPDevice(device) diff --git a/lib/python/xap_client/constants.py b/lib/python/xap_client/constants.py new file mode 100644 index 000000000000..cfde7e153b31 --- /dev/null +++ b/lib/python/xap_client/constants.py @@ -0,0 +1,125 @@ +# Copyright 2023 QMK +# SPDX-License-Identifier: GPL-2.0-or-later + +################################################################################ +# +# 88888888888 888 d8b .d888 d8b 888 d8b +# 888 888 Y8P d88P" Y8P 888 Y8P +# 888 888 888 888 +# 888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b +# 888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K +# 888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b. +# 888 888 888 888 X88 888 888 888 Y8b. 888 X88 +# 888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P' +# +# 888 888 +# 888 888 +# 888 888 +# .d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888 +# d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888 +# 888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888 +# Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888 +# "Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888 +# 888 +# Y8b d88P +# "Y88P" +# +################################################################################ + +from enum import IntEnum + + +# version: 0.0.1 +class RgblightModes(IntEnum): + STATIC_LIGHT = 0x00 + BREATHING = 0x01 + BREATHING_2 = 0x02 + BREATHING_3 = 0x03 + BREATHING_4 = 0x04 + RAINBOW_MOOD = 0x05 + RAINBOW_MOOD_2 = 0x06 + RAINBOW_MOOD_3 = 0x07 + RAINBOW_SWIRL = 0x08 + RAINBOW_SWIRL_2 = 0x09 + RAINBOW_SWIRL_3 = 0x0A + RAINBOW_SWIRL_4 = 0x0B + RAINBOW_SWIRL_5 = 0x0C + RAINBOW_SWIRL_6 = 0x0D + SNAKE = 0x0E + SNAKE_2 = 0x0F + SNAKE_3 = 0x10 + SNAKE_4 = 0x11 + SNAKE_5 = 0x12 + SNAKE_6 = 0x13 + KNIGHT = 0x14 + KNIGHT_2 = 0x15 + KNIGHT_3 = 0x16 + CHRISTMAS = 0x17 + STATIC_GRADIENT = 0x18 + STATIC_GRADIENT_2 = 0x19 + STATIC_GRADIENT_3 = 0x1A + STATIC_GRADIENT_4 = 0x1B + STATIC_GRADIENT_5 = 0x1C + STATIC_GRADIENT_6 = 0x1D + STATIC_GRADIENT_7 = 0x1E + STATIC_GRADIENT_8 = 0x1F + STATIC_GRADIENT_9 = 0x20 + STATIC_GRADIENT_10 = 0x21 + RGB_TEST = 0x22 + ALTERNATING = 0x23 + TWINKLE = 0x24 + TWINKLE_2 = 0x25 + TWINKLE_3 = 0x26 + TWINKLE_4 = 0x27 + TWINKLE_5 = 0x28 + TWINKLE_6 = 0x29 + + +# version: 0.0.1 +class RgbMatrixModes(IntEnum): + SOLID_COLOR = 0x00 + ALPHAS_MODS = 0x01 + GRADIENT_UP_DOWN = 0x02 + GRADIENT_LEFT_RIGHT = 0x03 + BREATHING = 0x04 + BAND_SAT = 0x05 + BAND_VAL = 0x06 + BAND_PINWHEEL_SAT = 0x07 + BAND_PINWHEEL_VAL = 0x08 + BAND_SPIRAL_SAT = 0x09 + BAND_SPIRAL_VAL = 0x0A + CYCLE_ALL = 0x0B + CYCLE_LEFT_RIGHT = 0x0C + CYCLE_UP_DOWN = 0x0D + CYCLE_OUT_IN = 0x0E + CYCLE_OUT_IN_DUAL = 0x0F + RAINBOW_MOVING_CHEVRON = 0x10 + CYCLE_PINWHEEL = 0x11 + CYCLE_SPIRAL = 0x12 + DUAL_BEACON = 0x13 + RAINBOW_BEACON = 0x14 + RAINBOW_PINWHEELS = 0x15 + RAINDROPS = 0x16 + JELLYBEAN_RAINDROPS = 0x17 + HUE_BREATHING = 0x18 + HUE_PENDULUM = 0x19 + HUE_WAVE = 0x1A + PIXEL_FRACTAL = 0x1B + PIXEL_FLOW = 0x1C + PIXEL_RAIN = 0x1D + TYPING_HEATMAP = 0x1E + DIGITAL_RAIN = 0x1F + SOLID_REACTIVE_SIMPLE = 0x20 + SOLID_REACTIVE = 0x21 + SOLID_REACTIVE_WIDE = 0x22 + SOLID_REACTIVE_MULTIWIDE = 0x23 + SOLID_REACTIVE_CROSS = 0x24 + SOLID_REACTIVE_MULTICROSS = 0x25 + SOLID_REACTIVE_NEXUS = 0x26 + SOLID_REACTIVE_MULTINEXUS = 0x27 + SPLASH = 0x28 + MULTISPLASH = 0x29 + SOLID_SPLASH = 0x2A + SOLID_MULTISPLASH = 0x2B + +# noqa: W391 diff --git a/lib/python/xap_client/device.py b/lib/python/xap_client/device.py new file mode 100644 index 000000000000..c1ce9f477ca1 --- /dev/null +++ b/lib/python/xap_client/device.py @@ -0,0 +1,238 @@ +# Copyright 2022 QMK +# SPDX-License-Identifier: GPL-2.0-or-later +import json +import time +import gzip +import random +import threading +import functools +from typing import Optional +from struct import pack, unpack +from platform import platform + +from .types import XAPSecureStatus, XAPFlags, XAPRequest, XAPResponse, XAPBroadcast +from .routes import XAPRoutes, XAPRouteError + + +def _u32_to_bcd(val: bytes) -> str: # noqa: N802 + """Create BCD string + """ + tmp = "{:08x}".format(val) + major = int(tmp[0:2]) + minor = int(tmp[2:4]) + patch = int(tmp[4:8]) + + return f'{major}.{minor}.{patch}' + + +def _gen_token() -> bytes: + """Generate XAP token - cannot start with 00xx or 'reserved' (FFFE|FFFF) + """ + token = random.randrange(0x0100, 0xFFFD) + + # swap endianness + return unpack('H', token))[0] + + +class XAPDeviceBase: + """Raw XAP interactions + """ + def __init__(self, dev: dict, timeout: int = 1.0): + """Constructor opens hid device and starts dependent services + """ + self.responses = {} + self.timeout = timeout + self.running = True + + # lazy import to avoid compile issues + import hid + + self.dev = hid.Device(path=dev['path']) + + self.bg = threading.Thread(target=self._read_loop, daemon=True) + self.bg.start() + + def close(self): + """Close device and stop dependent services + """ + self.running = False + time.sleep(1) + self.dev.close() + + def _read_loop(self): + """Background thread to signal waiting transactions + """ + while self.running: + data = self.dev.read(XAPResponse.fmt.size, 100) + if data: + r = XAPResponse.from_bytes(data) + event = self.responses.get(r.token) + if event: + event._ret = data + event.set() + + def transaction(self, *args) -> Optional[bytes]: + """Request/Receive Helper + """ + # convert args to array of bytes + data = bytes() + for arg in args: + if isinstance(arg, (bytes, bytearray)): + data += arg + if isinstance(arg, int): # TODO: remove terrible assumption of u16 + data += arg.to_bytes(2, byteorder='little') + + token = _gen_token() + + buffer = XAPRequest(token, len(data), data).to_bytes() + + event = threading.Event() + self.responses[token] = event + + # prepend 0 on windows because reasons... + if 'windows' in platform().lower(): + buffer = b'\x00' + buffer + self.dev.write(buffer) + + event.wait(timeout=self.timeout) + self.responses.pop(token, None) + if not hasattr(event, '_ret'): + return None + + r = XAPResponse.from_bytes(event._ret) + if r.flags & XAPFlags.SUCCESS == 0: + return None + + return r.data[:r.length] + + def listen(self) -> dict: + """Receive a single 'broadcast' message + """ + token = 0xFFFF + event = threading.Event() + self.responses[token] = event + + # emulate a blocking read while allowing `ctrl+c` on windows + while not hasattr(event, '_ret'): + event.wait(timeout=0.25) + + r = XAPBroadcast.from_bytes(event._ret) + return (r.event, r.data[:r.length]) + + +class XAPDevice(XAPDeviceBase): + """XAP device interaction + """ + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.close() + + def _query_device_info(self) -> dict: + """Helper to reconstruct info.json from requested chunks + """ + datalen = self.int_transaction(XAPRoutes.QMK_CONFIG_BLOB_LEN) + if not datalen: + return {} + + data = [] + offset = 0 + while offset < datalen: + chunk = self.transaction(XAPRoutes.QMK_CONFIG_BLOB_CHUNK, offset) + data += chunk + offset += len(chunk) + str_data = gzip.decompress(bytearray(data[:datalen])) + return json.loads(str_data) + + def _ensure_route(self, route: bytes): + """Check a route can be accessed + + Raises: + XAPRouteError: Access to invalid route attempted + """ + # TODO: Remove assumption that capability is always xx01 + (remain, sub, rt) = (route[:-2], route[-2], route[-1]) + cap = remain + bytes([sub, 1]) + + # recurse for nested routes + if remain: + self._ensure_route(remain + bytes([sub])) + + if self.subsystems() & (1 << sub) == 0: + raise XAPRouteError("subsystem not available") + if self.capability(cap) & (1 << rt) == 0: + raise XAPRouteError("route not available") + + def transaction(self, route: bytes, *args): + """Request/Receive to XAP device + + Raises: + XAPRouteError: Access to invalid route attempted + """ + self._ensure_route(route) + + return super().transaction(route, *args) + + def int_transaction(self, route: bytes, *args): + """transaction with int parsing + """ + return int.from_bytes(self.transaction(route, *args) or bytes(0), 'little') + + @functools.lru_cache + def capability(self, route: bytes): + # use parent transaction as we want to ignore capability checks + return int.from_bytes(super().transaction(route) or bytes(0), 'little') + + @functools.lru_cache + def subsystems(self): + # use parent transaction as we want to ignore capability checks + return int.from_bytes(super().transaction(XAPRoutes.XAP_SUBSYSTEM_QUERY) or bytes(0), 'little') + + @functools.lru_cache + def version(self) -> dict: + """Query version data from device + """ + xap = self.int_transaction(XAPRoutes.XAP_VERSION_QUERY) + qmk = self.int_transaction(XAPRoutes.QMK_VERSION_QUERY) + return {'xap': _u32_to_bcd(xap), 'qmk': _u32_to_bcd(qmk)} + + @functools.lru_cache + def info(self) -> dict: + """Query config data from device + """ + data = self._query_device_info() + data['_id'] = self.transaction(XAPRoutes.QMK_HARDWARE_ID) + data['_version'] = self.version() + return data + + def status(self) -> dict: + """Query current device state + """ + lock = self.int_transaction(XAPRoutes.XAP_SECURE_STATUS) + + data = {} + data['lock'] = XAPSecureStatus(lock).name + return data + + def unlock(self): + """Initiate unlock procedure + """ + self.transaction(XAPRoutes.XAP_SECURE_UNLOCK) + + def lock(self): + """Lock device + """ + self.transaction(XAPRoutes.XAP_SECURE_LOCK) + + def reset(self): + """Request device reboot to bootloader - Requires previous unlock + """ + status = self.int_transaction(XAPRoutes.QMK_BOOTLOADER_JUMP) + return status == 1 + + def reinit(self): + """Request device reset EEPROM - Requires previous unlock + """ + status = self.int_transaction(XAPRoutes.QMK_EEPROM_RESET) + return status == 1 diff --git a/lib/python/xap_client/pyproject.toml b/lib/python/xap_client/pyproject.toml new file mode 100644 index 000000000000..94b53db5562d --- /dev/null +++ b/lib/python/xap_client/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "xap_client" +description = "XAP Client" +version = "0.0.0" +dependencies = [ + "hid ~= 1.0.5", +] + +[tool.setuptools] +packages = ['xap_client'] +package-dir = { 'xap_client' = '.' } diff --git a/lib/python/xap_client/readme.md b/lib/python/xap_client/readme.md new file mode 100644 index 000000000000..9dce04721a1f --- /dev/null +++ b/lib/python/xap_client/readme.md @@ -0,0 +1,18 @@ +# XAP python3 bindings + +## Example +```python +from xap_client import XAPClient + +# List Available Devices +devices = XAPClient.devices() +selected = devices[0] + +# Connect then run commands +with XAPClient().connect(selected) as dev: + print(dev.version()) +``` + +## API + +TODO diff --git a/lib/python/xap_client/routes.py b/lib/python/xap_client/routes.py new file mode 100644 index 000000000000..b30138b48df5 --- /dev/null +++ b/lib/python/xap_client/routes.py @@ -0,0 +1,88 @@ +# Copyright 2023 QMK +# SPDX-License-Identifier: GPL-2.0-or-later + +################################################################################ +# +# 88888888888 888 d8b .d888 d8b 888 d8b +# 888 888 Y8P d88P" Y8P 888 Y8P +# 888 888 888 888 +# 888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b +# 888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K +# 888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b. +# 888 888 888 888 X88 888 888 888 Y8b. 888 X88 +# 888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P' +# +# 888 888 +# 888 888 +# 888 888 +# .d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888 +# d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888 +# 888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888 +# Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888 +# "Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888 +# 888 +# Y8b d88P +# "Y88P" +# +################################################################################ + +class XAPRouteError(Exception): + pass + + +class XAPRoutes(): + # XAP + XAP_VERSION_QUERY = b'\x00\x00' + XAP_CAPABILITIES_QUERY = b'\x00\x01' + XAP_SUBSYSTEM_QUERY = b'\x00\x02' + XAP_SECURE_STATUS = b'\x00\x03' + XAP_SECURE_UNLOCK = b'\x00\x04' + XAP_SECURE_LOCK = b'\x00\x05' + # QMK + QMK_VERSION_QUERY = b'\x01\x00' + QMK_CAPABILITIES_QUERY = b'\x01\x01' + QMK_BOARD_IDENTIFIERS = b'\x01\x02' + QMK_BOARD_MANUFACTURER = b'\x01\x03' + QMK_PRODUCT_NAME = b'\x01\x04' + QMK_CONFIG_BLOB_LEN = b'\x01\x05' + QMK_CONFIG_BLOB_CHUNK = b'\x01\x06' + QMK_BOOTLOADER_JUMP = b'\x01\x07' + QMK_HARDWARE_ID = b'\x01\x08' + QMK_EEPROM_RESET = b'\x01\x09' + # KEYMAP + KEYMAP_CAPABILITIES_QUERY = b'\x04\x01' + KEYMAP_GET_LAYER_COUNT = b'\x04\x02' + KEYMAP_GET_KEYMAP_KEYCODE = b'\x04\x03' + KEYMAP_GET_ENCODER_KEYCODE = b'\x04\x04' + # REMAPPING + REMAPPING_CAPABILITIES_QUERY = b'\x05\x01' + REMAPPING_GET_DYNAMIC_LAYER_COUNT = b'\x05\x02' + REMAPPING_SET_KEYMAP_KEYCODE = b'\x05\x03' + REMAPPING_SET_ENCODER_KEYCODE = b'\x05\x04' + # LIGHTING + LIGHTING_CAPABILITIES_QUERY = b'\x06\x01' + LIGHTING_BACKLIGHT = b'\x06\x02' + LIGHTING_BACKLIGHT_CAPABILITIES_QUERY = b'\x06\x02\x01' + LIGHTING_BACKLIGHT_GET_ENABLED_EFFECTS = b'\x06\x02\x02' + LIGHTING_BACKLIGHT_GET_CONFIG = b'\x06\x02\x03' + LIGHTING_BACKLIGHT_SET_CONFIG = b'\x06\x02\x04' + LIGHTING_BACKLIGHT_SAVE_CONFIG = b'\x06\x02\x05' + LIGHTING_RGBLIGHT = b'\x06\x03' + LIGHTING_RGBLIGHT_CAPABILITIES_QUERY = b'\x06\x03\x01' + LIGHTING_RGBLIGHT_GET_ENABLED_EFFECTS = b'\x06\x03\x02' + LIGHTING_RGBLIGHT_GET_CONFIG = b'\x06\x03\x03' + LIGHTING_RGBLIGHT_SET_CONFIG = b'\x06\x03\x04' + LIGHTING_RGBLIGHT_SAVE_CONFIG = b'\x06\x03\x05' + LIGHTING_RGB_MATRIX = b'\x06\x04' + LIGHTING_RGB_MATRIX_CAPABILITIES_QUERY = b'\x06\x04\x01' + LIGHTING_RGB_MATRIX_GET_ENABLED_EFFECTS = b'\x06\x04\x02' + LIGHTING_RGB_MATRIX_GET_CONFIG = b'\x06\x04\x03' + LIGHTING_RGB_MATRIX_SET_CONFIG = b'\x06\x04\x04' + LIGHTING_RGB_MATRIX_SAVE_CONFIG = b'\x06\x04\x05' + # AUDIO + AUDIO_CAPABILITIES_QUERY = b'\x07\x01' + AUDIO_GET_CONFIG = b'\x07\x03' + AUDIO_SET_CONFIG = b'\x07\x04' + AUDIO_SAVE_CONFIG = b'\x07\x05' + +# noqa: W391 diff --git a/lib/python/xap_client/types.py b/lib/python/xap_client/types.py new file mode 100644 index 000000000000..43b80e38a8ab --- /dev/null +++ b/lib/python/xap_client/types.py @@ -0,0 +1,151 @@ +# Copyright 2023 QMK +# SPDX-License-Identifier: GPL-2.0-or-later + +################################################################################ +# +# 88888888888 888 d8b .d888 d8b 888 d8b +# 888 888 Y8P d88P" Y8P 888 Y8P +# 888 888 888 888 +# 888 88888b. 888 .d8888b 888888 888 888 .d88b. 888 .d8888b +# 888 888 "88b 888 88K 888 888 888 d8P Y8b 888 88K +# 888 888 888 888 "Y8888b. 888 888 888 88888888 888 "Y8888b. +# 888 888 888 888 X88 888 888 888 Y8b. 888 X88 +# 888 888 888 888 88888P' 888 888 888 "Y8888 888 88888P' +# +# 888 888 +# 888 888 +# 888 888 +# .d88b. .d88b. 88888b. .d88b. 888d888 8888b. 888888 .d88b. .d88888 +# d88P"88b d8P Y8b 888 "88b d8P Y8b 888P" "88b 888 d8P Y8b d88" 888 +# 888 888 88888888 888 888 88888888 888 .d888888 888 88888888 888 888 +# Y88b 888 Y8b. 888 888 Y8b. 888 888 888 Y88b. Y8b. Y88b 888 +# "Y88888 "Y8888 888 888 "Y8888 888 "Y888888 "Y888 "Y8888 "Y88888 +# 888 +# Y8b d88P +# "Y88P" +# +################################################################################ + +from collections import namedtuple +from enum import IntFlag, IntEnum +from struct import Struct + + +class XAPRequest(namedtuple('XAPRequest', 'token length data')): + fmt = Struct('enable; + audio_config.clicky_enable = arg->clicky_enable; + + return xap_respond_success(token); +} + +bool xap_execute_save_audio_config(xap_token_t token) { + eeconfig_update_audio_current(); + + return xap_respond_success(token); +} +#endif diff --git a/quantum/xap/handlers/core.c b/quantum/xap/handlers/core.c new file mode 100644 index 000000000000..47fbaec1d2ab --- /dev/null +++ b/quantum/xap/handlers/core.c @@ -0,0 +1,60 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "quantum.h" +#include "xap.h" +#include "hardware_id.h" + +bool xap_execute_get_config_blob_chunk(xap_token_t token, uint16_t offset) { + xap_route_qmk_config_blob_chunk_t ret = {0}; + + bool get_config_blob_chunk(uint16_t offset, uint8_t * data, uint8_t data_len); + if (!get_config_blob_chunk(offset, (uint8_t *)&ret, sizeof(ret))) { + return false; + } + + return xap_respond_data(token, &ret, sizeof(ret)); +} + +bool xap_execute_secure_status(xap_token_t token) { + uint8_t ret = secure_get_status(); + return xap_respond_data(token, &ret, sizeof(ret)); +} + +bool xap_execute_secure_unlock(xap_token_t token) { + secure_request_unlock(); + return xap_respond_success(token); +} + +bool xap_execute_secure_lock(xap_token_t token) { + secure_lock(); + return xap_respond_success(token); +} + +#ifdef BOOTLOADER_JUMP_SUPPORTED +bool xap_execute_request_bootloader(xap_token_t token) { + uint8_t ret = secure_is_unlocked(); + + // TODO: post to deferred queue so this request can return? + bool res = xap_respond_data(token, &ret, sizeof(ret)); + reset_keyboard(); + return res; +} +#endif + +#ifndef NO_RESET +bool xap_execute_request_eeprom_reset(xap_token_t token) { + uint8_t ret = secure_is_unlocked(); + + // TODO: post to deferred queue so this request can return? + bool res = xap_respond_data(token, &ret, sizeof(ret)); + eeconfig_disable(); + soft_reset_keyboard(); + return res; +} +#endif + +bool xap_execute_get_hardware_id(xap_token_t token) { + hardware_id_t ret = get_hardware_id(); + return xap_respond_data(token, &ret, sizeof(ret)); +} \ No newline at end of file diff --git a/quantum/xap/handlers/lighting.c b/quantum/xap/handlers/lighting.c new file mode 100644 index 000000000000..03d8aa0802c0 --- /dev/null +++ b/quantum/xap/handlers/lighting.c @@ -0,0 +1,137 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "quantum.h" +#include "xap.h" + +#define INVALID_EFFECT 0xFF + +#if ((defined(BACKLIGHT_ENABLE))) +# include "backlight.h" + +extern backlight_config_t backlight_config; + +bool xap_execute_get_backlight_config(xap_token_t token) { + xap_route_lighting_backlight_get_config_t ret; + + ret.enable = backlight_config.enable; + ret.mode = backlight_config.breathing; + ret.val = backlight_config.level; + + return xap_respond_data(token, &ret, sizeof(ret)); +} + +bool xap_execute_set_backlight_config(xap_token_t token, xap_route_lighting_backlight_set_config_arg_t* arg) { + if (arg->enable) { + backlight_level_noeeprom(arg->val); + } else { + backlight_level_noeeprom(0); + } + +# ifdef BACKLIGHT_BREATHING + if (arg->mode) { + backlight_enable_breathing(); + } else { + backlight_disable_breathing(); + } +# endif + + return xap_respond_success(token); +} + +bool xap_execute_save_backlight_config(xap_token_t token) { + eeconfig_update_backlight_current(); + + return xap_respond_success(token); +} +#endif + +#if ((defined(RGBLIGHT_ENABLE))) +# include "rgblight.h" + +extern rgblight_config_t rgblight_config; + +uint8_t rgblight_effect_to_id(uint8_t val); +uint8_t rgblight_id_to_effect(uint8_t val); + +bool xap_execute_get_rgblight_config(xap_token_t token) { + xap_route_lighting_rgblight_get_config_t ret; + + ret.enable = rgblight_config.enable; + ret.mode = rgblight_effect_to_id(rgblight_config.mode); + ret.hue = rgblight_config.hue; + ret.sat = rgblight_config.sat; + ret.val = rgblight_config.val; + ret.speed = rgblight_config.speed; + + return xap_respond_data(token, &ret, sizeof(ret)); +} + +bool xap_execute_set_rgblight_config(xap_token_t token, xap_route_lighting_rgblight_set_config_arg_t* arg) { + uint8_t mode = rgblight_id_to_effect(arg->mode); + if (mode == INVALID_EFFECT) { + return false; + } + + rgblight_enabled_noeeprom(arg->enable); + rgblight_mode_noeeprom(mode); + rgblight_sethsv_noeeprom(arg->hue, arg->sat, arg->val); + rgblight_set_speed_noeeprom(arg->speed); + + return xap_respond_success(token); +} + +bool xap_execute_save_rgblight_config(xap_token_t token) { + eeconfig_update_rgblight_current(); + + return xap_respond_success(token); +} +#endif + +#if ((defined(RGB_MATRIX_ENABLE))) +# include "rgb_matrix.h" + +extern rgb_config_t rgb_matrix_config; + +uint8_t rgb_matrix_effect_to_id(uint8_t val); +uint8_t rgb_matrix_id_to_effect(uint8_t val); + +void rgb_matrix_enabled_noeeprom(bool val) { + val ? rgb_matrix_enable_noeeprom() : rgb_matrix_disable_noeeprom(); +} + +bool xap_execute_get_rgb_matrix_config(xap_token_t token) { + xap_route_lighting_rgb_matrix_get_config_t ret; + + ret.enable = rgb_matrix_config.enable; + ret.mode = rgb_matrix_effect_to_id(rgb_matrix_config.mode); + ret.hue = rgb_matrix_config.hsv.h; + ret.sat = rgb_matrix_config.hsv.s; + ret.val = rgb_matrix_config.hsv.v; + ret.speed = rgb_matrix_config.speed; + ret.flags = rgb_matrix_config.flags; + + return xap_respond_data(token, &ret, sizeof(ret)); +} + +bool xap_execute_set_rgb_matrix_config(xap_token_t token, xap_route_lighting_rgb_matrix_set_config_arg_t* arg) { + uint8_t mode = rgb_matrix_id_to_effect(arg->mode); + if (mode == INVALID_EFFECT) { + return false; + } + + rgb_matrix_enabled_noeeprom(arg->enable); + rgb_matrix_mode_noeeprom(mode); + rgb_matrix_sethsv_noeeprom(arg->hue, arg->sat, arg->val); + rgb_matrix_set_speed_noeeprom(arg->speed); + rgb_matrix_set_flags_noeeprom(arg->flags); + + return xap_respond_success(token); +} + +bool xap_execute_save_rgb_matrix_config(xap_token_t token) { + eeconfig_update_rgb_matrix(); + + return xap_respond_success(token); +} +#endif diff --git a/quantum/xap/handlers/remapping.c b/quantum/xap/handlers/remapping.c new file mode 100644 index 000000000000..5887a1abcaf1 --- /dev/null +++ b/quantum/xap/handlers/remapping.c @@ -0,0 +1,60 @@ +// Copyright 2022 QMK +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "quantum.h" +#include "xap.h" +#include "keymap_introspection.h" + +#ifdef DYNAMIC_KEYMAP_ENABLE +# include "dynamic_keymap.h" +# define keymap_max_layer_count() DYNAMIC_KEYMAP_LAYER_COUNT +#else +# define keymap_max_layer_count() keymap_layer_count() +#endif + +bool xap_execute_keymap_get_layer_count(xap_token_t token) { + uint8_t ret = keymap_max_layer_count(); + return xap_respond_data(token, &ret, sizeof(ret)); +} + +bool xap_execute_get_keymap_keycode(xap_token_t token, const xap_route_keymap_get_keymap_keycode_arg_t *arg) { + if (arg->layer >= keymap_max_layer_count()) { + return false; + } + + uint16_t keycode = keycode_at_keymap_location(arg->layer, arg->row, arg->column); + return xap_respond_data(token, &keycode, sizeof(keycode)); +} + +#if ((defined(ENCODER_MAP_ENABLE))) +bool xap_execute_get_encoder_keycode(xap_token_t token, const xap_route_keymap_get_encoder_keycode_arg_t *arg) { + if (arg->layer >= keymap_max_layer_count()) { + return false; + } + + uint16_t keycode = keycode_at_encodermap_location(arg->layer, arg->encoder, arg->clockwise); + return xap_respond_data(token, &keycode, sizeof(keycode)); +} +#endif + +#if ((defined(DYNAMIC_KEYMAP_ENABLE))) +bool xap_execute_dynamic_keymap_set_keycode(xap_token_t token, const xap_route_remapping_set_keymap_keycode_arg_t *arg) { + if (arg->layer >= keymap_max_layer_count()) { + return false; + } + + dynamic_keymap_set_keycode(arg->layer, arg->row, arg->column, arg->keycode); + return xap_respond_success(token); +} +#endif + +#if ((defined(DYNAMIC_KEYMAP_ENABLE) && defined(ENCODER_MAP_ENABLE))) +bool xap_execute_dynamic_encoder_set_keycode(xap_token_t token, const xap_route_remapping_set_encoder_keycode_arg_t *arg) { + if (arg->layer >= keymap_max_layer_count()) { + return false; + } + + dynamic_keymap_set_encoder(arg->layer, arg->encoder, arg->clockwise, arg->keycode); + return xap_respond_success(token); +} +#endif diff --git a/quantum/xap/xap.c b/quantum/xap/xap.c new file mode 100644 index 000000000000..491f27c8d771 --- /dev/null +++ b/quantum/xap/xap.c @@ -0,0 +1,163 @@ +/* Copyright 2021 Nick Brassel (@tzarc) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include "secure.h" + +#ifdef RGBLIGHT_ENABLE +# include "rgblight/lighting_map.h" +#endif +#ifdef RGB_MATRIX_ENABLE +# include "rgb_matrix/lighting_map.h" +#endif +#ifdef LED_MATRIX_ENABLE +# include "led_matrix/lighting_map.h" +#endif + +#include "config_blob_gz.h" +bool get_config_blob_chunk(uint16_t offset, uint8_t *data, uint8_t data_len) { + if (offset >= CONFIG_BLOB_GZ_LEN) { + return false; + } + + if (offset + data_len >= CONFIG_BLOB_GZ_LEN) { + data_len = (CONFIG_BLOB_GZ_LEN - 1) - offset; + } + + memcpy_P(data, &config_blob_gz[offset], data_len); + return true; +} + +// TODO: move to better location? +#ifdef BACKLIGHT_BREATHING +# define ENABLED_BACKLIGHT_EFFECTS 0b00000001 +#else +# define ENABLED_BACKLIGHT_EFFECTS 0b00000000 +#endif + +#define QSTR2(z) #z +#define QSTR(z) QSTR2(z) + +typedef enum xap_route_type_t { + XAP_UNUSED = 0, // "Unused" needs to be zero -- undefined routes (through preprocessor) will be skipped + XAP_ROUTE, + XAP_EXECUTE, + XAP_VALUE, + XAP_CONST_MEM, + TOTAL_XAP_ROUTE_TYPES +} xap_route_type_t; + +#define XAP_ROUTE_TYPE_BIT_COUNT 3 + +typedef enum xap_route_secure_t { + ROUTE_PERMISSIONS_INSECURE, + ROUTE_PERMISSIONS_SECURE, +} xap_route_secure_t; + +#define XAP_ROUTE_SECURE_BIT_COUNT 2 + +typedef struct __attribute__((packed)) xap_route_flags_t { + xap_route_type_t type : XAP_ROUTE_TYPE_BIT_COUNT; + xap_route_secure_t secure : XAP_ROUTE_SECURE_BIT_COUNT; +} xap_route_flags_t; + +_Static_assert(TOTAL_XAP_ROUTE_TYPES <= (1 << (XAP_ROUTE_TYPE_BIT_COUNT)), "Number of XAP route types is too large for XAP_ROUTE_TYPE_BITS."); +_Static_assert(sizeof(xap_route_flags_t) == 1, "xap_route_flags_t is not length of 1"); + +typedef struct xap_route_t xap_route_t; +struct __attribute__((packed)) xap_route_t { + const xap_route_flags_t flags; + union { + // XAP_ROUTE + struct { + const xap_route_t *child_routes; + const uint8_t child_routes_len; + }; + + // XAP_EXECUTE + bool (*handler)(xap_token_t token, const uint8_t *data, size_t data_len); + + // XAP_VALUE / XAP_CONST_MEM + struct { + const void * const_data; + const uint8_t const_data_len; + }; + }; +}; + +#include + +bool xap_pre_execute_route(xap_token_t token, const xap_route_t *route) { +#ifdef SECURE_ENABLE + if (!secure_is_unlocked() && (route->flags.secure == ROUTE_PERMISSIONS_SECURE)) { + xap_respond_failure(token, XAP_RESPONSE_FLAG_SECURE_FAILURE); + return true; + } + + // TODO: XAP messages extend unlocked timeout? + secure_activity_event(); +#endif + return false; +} + +void xap_execute_route(xap_token_t token, const xap_route_t *routes, size_t max_routes, const uint8_t *data, size_t data_len) { + if (data_len == 0) return; + xap_identifier_t id = data[0]; + + if (id < max_routes) { + xap_route_t route; + memcpy_P(&route, &routes[id], sizeof(xap_route_t)); + + if (xap_pre_execute_route(token, &route)) { + return; + } + + switch (route.flags.type) { + case XAP_ROUTE: + if (route.child_routes != NULL && route.child_routes_len > 0 && data_len > 0) { + xap_execute_route(token, route.child_routes, route.child_routes_len, &data[1], data_len - 1); + return; + } + break; + + case XAP_EXECUTE: + if (route.handler != NULL) { + bool ok = (route.handler)(token, data_len == 1 ? NULL : &data[1], data_len - 1); + if (ok) return; + } + break; + + case XAP_VALUE: + xap_respond_data(token, route.const_data, route.const_data_len); + return; + + case XAP_CONST_MEM: + xap_respond_data_P(token, route.const_data, route.const_data_len); + return; + + default: + break; + } + } + + // Nothing got handled, so we respond with failure. + xap_respond_failure(token, XAP_RESPONSE_FLAG_FAILED); +} + +void xap_receive(xap_token_t token, const uint8_t *data, size_t length) { + xap_execute_route(token, xap_route_table, sizeof(xap_route_table) / sizeof(xap_route_t), data, length); +} diff --git a/quantum/xap/xap.h b/quantum/xap/xap.h new file mode 100644 index 000000000000..122b7055be6a --- /dev/null +++ b/quantum/xap/xap.h @@ -0,0 +1,39 @@ +/* Copyright 2021 Nick Brassel (@tzarc) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include + +#ifndef XAP_SUBSYSTEM_VERSION_KB +# define XAP_SUBSYSTEM_VERSION_KB 0 +#endif + +#ifndef XAP_SUBSYSTEM_VERSION_USER +# define XAP_SUBSYSTEM_VERSION_USER 0 +#endif + +bool xap_respond_success(xap_token_t token); +void xap_respond_failure(xap_token_t token, xap_response_flags_t response_flags); +bool xap_respond_u32(xap_token_t token, uint32_t value); +bool xap_respond_data(xap_token_t token, const void *data, size_t length); +bool xap_respond_data_P(xap_token_t token, const void *data, size_t length); + +void xap_send(xap_token_t token, xap_response_flags_t response_flags, const void *data, size_t length); +void xap_broadcast(uint8_t type, const void *data, size_t length); diff --git a/quantum/xap/xap_handlers.c b/quantum/xap/xap_handlers.c new file mode 100644 index 000000000000..109c15d54983 --- /dev/null +++ b/quantum/xap/xap_handlers.c @@ -0,0 +1,56 @@ +/* Copyright 2021 Nick Brassel (@tzarc) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include "quantum.h" +#include "xap.h" + +#include "secure.h" +#ifndef SECURE_ENABLE +# define secure_get_status() SECURE_UNLOCKED +# define secure_request_unlock() +# define secure_lock() +#endif + +bool xap_respond_success(xap_token_t token) { + xap_send(token, XAP_RESPONSE_FLAG_SUCCESS, NULL, 0); + return true; +} + +void xap_respond_failure(xap_token_t token, xap_response_flags_t response_flags) { + xap_send(token, response_flags, NULL, 0); +} + +bool xap_respond_data(xap_token_t token, const void *data, size_t length) { + xap_send(token, XAP_RESPONSE_FLAG_SUCCESS, data, length); + return true; +} + +bool xap_respond_data_P(xap_token_t token, const void *data, size_t length) { + uint8_t blob[length]; + memcpy_P(blob, data, length); + return xap_respond_data(token, blob, length); +} + +bool xap_respond_u32(xap_token_t token, uint32_t value) { + xap_send(token, XAP_RESPONSE_FLAG_SUCCESS, &value, sizeof(value)); + return true; +} + +#include "handlers/core.c" +#include "handlers/remapping.c" +#include "handlers/lighting.c" +#include "handlers/audio.c" diff --git a/requirements.txt b/requirements.txt index 6bee74632431..b74e1730c2a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,10 @@ appdirs argcomplete colorama dotty-dict +fnvhash hid hjson +Jinja2 jsonschema>=4 milc>=1.4.2 pygments diff --git a/tmk_core/protocol/chibios/usb_endpoints.c b/tmk_core/protocol/chibios/usb_endpoints.c index 856df6242603..693d2500b185 100644 --- a/tmk_core/protocol/chibios/usb_endpoints.c +++ b/tmk_core/protocol/chibios/usb_endpoints.c @@ -80,6 +80,14 @@ usb_endpoint_in_t usb_endpoints_in[USB_ENDPOINT_IN_COUNT] = { # endif #endif +#if defined(XAP_ENABLE) +# if defined(USB_ENDPOINTS_ARE_REORDERABLE) + [USB_ENDPOINT_IN_XAP] = QMK_USB_ENDPOINT_IN_SHARED(USB_EP_MODE_TYPE_INTR, XAP_EPSIZE, XAP_IN_EPNUM, XAP_IN_CAPACITY, NULL, QMK_USB_REPORT_STORAGE_DEFAULT(XAP_EPSIZE)), +# else + [USB_ENDPOINT_IN_XAP] = QMK_USB_ENDPOINT_IN(USB_EP_MODE_TYPE_INTR, XAP_EPSIZE, XAP_IN_EPNUM, XAP_IN_CAPACITY, NULL, QMK_USB_REPORT_STORAGE_DEFAULT(XAP_EPSIZE)), +# endif +#endif + #if defined(MIDI_ENABLE) # if defined(USB_ENDPOINTS_ARE_REORDERABLE) [USB_ENDPOINT_IN_MIDI] = QMK_USB_ENDPOINT_IN_SHARED(USB_EP_MODE_TYPE_BULK, MIDI_STREAM_EPSIZE, MIDI_STREAM_IN_EPNUM, MIDI_STREAM_IN_CAPACITY, NULL, NULL), @@ -107,6 +115,10 @@ usb_endpoint_in_lut_t usb_endpoint_interface_lut[TOTAL_INTERFACES] = { [RAW_INTERFACE] = USB_ENDPOINT_IN_RAW, #endif +#if defined(XAP_ENABLE) + [XAP_INTERFACE] = USB_ENDPOINT_IN_XAP, +#endif + #if defined(MOUSE_ENABLE) && !defined(MOUSE_SHARED_EP) [MOUSE_INTERFACE] = USB_ENDPOINT_IN_MOUSE, #endif @@ -142,6 +154,10 @@ usb_endpoint_out_t usb_endpoints_out[USB_ENDPOINT_OUT_COUNT] = { [USB_ENDPOINT_OUT_RAW] = QMK_USB_ENDPOINT_OUT(USB_EP_MODE_TYPE_INTR, RAW_EPSIZE, RAW_OUT_EPNUM, RAW_OUT_CAPACITY), #endif +#if defined(XAP_ENABLE) + [USB_ENDPOINT_OUT_XAP] = QMK_USB_ENDPOINT_OUT(USB_EP_MODE_TYPE_INTR, XAP_EPSIZE, XAP_OUT_EPNUM, XAP_OUT_CAPACITY), +#endif + #if defined(MIDI_ENABLE) [USB_ENDPOINT_OUT_MIDI] = QMK_USB_ENDPOINT_OUT(USB_EP_MODE_TYPE_BULK, MIDI_STREAM_EPSIZE, MIDI_STREAM_OUT_EPNUM, MIDI_STREAM_OUT_CAPACITY), #endif diff --git a/tmk_core/protocol/chibios/usb_endpoints.h b/tmk_core/protocol/chibios/usb_endpoints.h index a4e5ed88fce3..7869e2f25e0a 100644 --- a/tmk_core/protocol/chibios/usb_endpoints.h +++ b/tmk_core/protocol/chibios/usb_endpoints.h @@ -43,6 +43,14 @@ # define RAW_OUT_CAPACITY USB_DEFAULT_BUFFER_CAPACITY #endif +#if !defined(XAP_IN_CAPACITY) +# define XAP_IN_CAPACITY USB_DEFAULT_BUFFER_CAPACITY +#endif + +#if !defined(XAP_OUT_CAPACITY) +# define XAP_OUT_CAPACITY USB_DEFAULT_BUFFER_CAPACITY +#endif + #if !defined(MIDI_STREAM_IN_CAPACITY) # define MIDI_STREAM_IN_CAPACITY USB_DEFAULT_BUFFER_CAPACITY #endif @@ -90,6 +98,10 @@ typedef enum { USB_ENDPOINT_IN_RAW, #endif +#if defined(XAP_ENABLE) + USB_ENDPOINT_IN_XAP, +#endif + #if defined(MIDI_ENABLE) USB_ENDPOINT_IN_MIDI, #endif @@ -127,6 +139,9 @@ typedef enum { #if defined(RAW_ENABLE) USB_ENDPOINT_OUT_RAW, #endif +#if defined(XAP_ENABLE) + USB_ENDPOINT_OUT_XAP, +#endif #if defined(MIDI_ENABLE) USB_ENDPOINT_OUT_MIDI, #endif diff --git a/tmk_core/protocol/chibios/usb_main.c b/tmk_core/protocol/chibios/usb_main.c index 2024a3bc7f24..5f9f87ea626b 100644 --- a/tmk_core/protocol/chibios/usb_main.c +++ b/tmk_core/protocol/chibios/usb_main.c @@ -38,6 +38,10 @@ extern keymap_config_t keymap_config; #endif +#ifdef XAP_ENABLE +# include "xap.h" +#endif + /* --------------------------------------------------------- * Global interface variables and declarations * --------------------------------------------------------- @@ -551,6 +555,65 @@ void raw_hid_task(void) { #endif +#ifdef XAP_ENABLE +extern void xap_receive(xap_token_t token, const uint8_t *data, size_t length); + +void xap_send_base(uint8_t *data, uint8_t length) { + if (length != XAP_EPSIZE) { + return; + } + + send_report(USB_ENDPOINT_IN_XAP, data, length); +} + +void xap_send(xap_token_t token, xap_response_flags_t response_flags, const void *data, size_t length) { + uint8_t rdata[XAP_EPSIZE] = {0}; + xap_response_header_t *header = (xap_response_header_t *)&rdata[0]; + header->token = token; + + if (length > (XAP_EPSIZE - sizeof(xap_response_header_t))) response_flags &= ~(XAP_RESPONSE_FLAG_SUCCESS); + header->flags = response_flags; + + if (response_flags & (XAP_RESPONSE_FLAG_SUCCESS)) { + header->length = (uint8_t)length; + if (data != NULL) { + memcpy(&rdata[sizeof(xap_response_header_t)], data, length); + } + } + xap_send_base(rdata, sizeof(rdata)); +} + +void xap_broadcast(uint8_t type, const void *data, size_t length) { + uint8_t rdata[XAP_EPSIZE] = {0}; + xap_broadcast_header_t *header = (xap_broadcast_header_t *)&rdata[0]; + header->token = XAP_BROADCAST_TOKEN; + header->type = type; + + if (length > (XAP_EPSIZE - sizeof(xap_broadcast_header_t))) return; + + header->length = (uint8_t)length; + if (data != NULL) { + memcpy(&rdata[sizeof(xap_broadcast_header_t)], data, length); + } + xap_send_base(rdata, sizeof(rdata)); +} + +void xap_receive_base(const void *data) { + const uint8_t * u8data = (const uint8_t *)data; + xap_request_header_t *header = (xap_request_header_t *)&u8data[0]; + if (header->length <= (XAP_EPSIZE - sizeof(xap_request_header_t))) { + xap_receive(header->token, &u8data[sizeof(xap_request_header_t)], header->length); + } +} + +void xap_task(void) { + uint8_t buffer[XAP_EPSIZE]; + while (receive_report(USB_ENDPOINT_OUT_XAP, buffer, sizeof(buffer))) { + xap_receive_base(buffer); + } +} +#endif // XAP_ENABLE + #ifdef MIDI_ENABLE void send_midi_packet(MIDI_EventPacket_t *event) { diff --git a/tmk_core/protocol/lufa/lufa.c b/tmk_core/protocol/lufa/lufa.c index b0c9758d2fd5..45450bfd6fbe 100644 --- a/tmk_core/protocol/lufa/lufa.c +++ b/tmk_core/protocol/lufa/lufa.c @@ -67,6 +67,10 @@ # include "raw_hid.h" #endif +#ifdef XAP_ENABLE +# include "xap.h" +#endif + #ifdef WAIT_FOR_USB // TODO: Remove backwards compatibility with old define # define USB_WAIT_FOR_ENUMERATION @@ -185,6 +189,105 @@ void raw_hid_task(void) { } #endif +#ifdef XAP_ENABLE +extern void xap_receive(xap_token_t token, const uint8_t *data, size_t length); + +void xap_send_base(uint8_t *data, uint8_t length) { + // TODO: implement variable size packet + if (length != XAP_EPSIZE) { + return; + } + + if (USB_DeviceState != DEVICE_STATE_Configured) { + return; + } + + // TODO: decide if we allow calls to raw_hid_send() in the middle + // of other endpoint usage. + uint8_t ep = Endpoint_GetCurrentEndpoint(); + + Endpoint_SelectEndpoint(XAP_IN_EPNUM); + + // Check to see if the host is ready to accept another packet + if (Endpoint_IsINReady()) { + // Write data + Endpoint_Write_Stream_LE(data, XAP_EPSIZE, NULL); + // Finalize the stream transfer to send the last packet + Endpoint_ClearIN(); + } + + Endpoint_SelectEndpoint(ep); +} + +void xap_send(xap_token_t token, xap_response_flags_t response_flags, const void *data, size_t length) { + uint8_t rdata[XAP_EPSIZE] = {0}; + xap_response_header_t *header = (xap_response_header_t *)&rdata[0]; + header->token = token; + + if (length > (XAP_EPSIZE - sizeof(xap_response_header_t))) response_flags &= ~(XAP_RESPONSE_FLAG_SUCCESS); + header->flags = response_flags; + + if (response_flags & (XAP_RESPONSE_FLAG_SUCCESS)) { + header->length = (uint8_t)length; + if (data != NULL) { + memcpy(&rdata[sizeof(xap_response_header_t)], data, length); + } + } + xap_send_base(rdata, sizeof(rdata)); +} + +void xap_broadcast(uint8_t type, const void *data, size_t length) { + uint8_t rdata[XAP_EPSIZE] = {0}; + xap_broadcast_header_t *header = (xap_broadcast_header_t *)&rdata[0]; + header->token = XAP_BROADCAST_TOKEN; + header->type = type; + + if (length > (XAP_EPSIZE - sizeof(xap_broadcast_header_t))) return; + + header->length = (uint8_t)length; + if (data != NULL) { + memcpy(&rdata[sizeof(xap_broadcast_header_t)], data, length); + } + xap_send_base(rdata, sizeof(rdata)); +} + +void xap_receive_base(const void *data) { + const uint8_t * u8data = (const uint8_t *)data; + xap_request_header_t *header = (xap_request_header_t *)&u8data[0]; + if (header->length <= (XAP_EPSIZE - sizeof(xap_request_header_t))) { + xap_receive(header->token, &u8data[sizeof(xap_request_header_t)], header->length); + } +} + +void xap_task(void) { + // Create a temporary buffer to hold the read in data from the host + uint8_t data[XAP_EPSIZE]; + bool data_read = false; + + // Device must be connected and configured for the task to run + if (USB_DeviceState != DEVICE_STATE_Configured) return; + + Endpoint_SelectEndpoint(XAP_OUT_EPNUM); + + // Check to see if a packet has been sent from the host + if (Endpoint_IsOUTReceived()) { + // Check to see if the packet contains data + if (Endpoint_IsReadWriteAllowed()) { + /* Read data */ + Endpoint_Read_Stream_LE(data, sizeof(data), NULL); + data_read = true; + } + + // Finalize the stream transfer to receive the last packet + Endpoint_ClearOUT(); + + if (data_read) { + xap_receive_base(data); + } + } +} +#endif // XAP_ENABLE + /******************************************************************************* * Console ******************************************************************************/ @@ -390,6 +493,12 @@ void EVENT_USB_Device_ConfigurationChanged(void) { ConfigSuccess &= Endpoint_ConfigureEndpoint((DIGITIZER_IN_EPNUM | ENDPOINT_DIR_IN), EP_TYPE_INTERRUPT, DIGITIZER_EPSIZE, 1); #endif +#ifdef XAP_ENABLE + /* Setup XAP endpoints */ + ConfigSuccess &= Endpoint_ConfigureEndpoint((XAP_IN_EPNUM | ENDPOINT_DIR_IN), EP_TYPE_INTERRUPT, XAP_EPSIZE, 1); + ConfigSuccess &= Endpoint_ConfigureEndpoint((XAP_OUT_EPNUM | ENDPOINT_DIR_OUT), EP_TYPE_INTERRUPT, XAP_EPSIZE, 1); +#endif // XAP_ENABLE + usb_device_state_set_configuration(USB_DeviceState == DEVICE_STATE_Configured, USB_Device_ConfigurationNumber); } diff --git a/tmk_core/protocol/usb_descriptor.c b/tmk_core/protocol/usb_descriptor.c index 7efd085ea32e..6078984e42fc 100644 --- a/tmk_core/protocol/usb_descriptor.c +++ b/tmk_core/protocol/usb_descriptor.c @@ -424,6 +424,30 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM ConsoleReport[] = { }; #endif +#ifdef XAP_ENABLE +const USB_Descriptor_HIDReport_Datatype_t PROGMEM XapReport[] = { + HID_RI_USAGE_PAGE(16, 0xFF51), // Vendor Defined ('Q') + HID_RI_USAGE(8, 0x58), // Vendor Defined ('X') + HID_RI_COLLECTION(8, 0x01), // Application + // Data to host + HID_RI_USAGE(8, 0x62), // Vendor Defined + HID_RI_LOGICAL_MINIMUM(8, 0x00), + HID_RI_LOGICAL_MAXIMUM(16, 0x00FF), + HID_RI_REPORT_COUNT(8, XAP_EPSIZE), + HID_RI_REPORT_SIZE(8, 0x08), + HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE), + + // Data from host + HID_RI_USAGE(8, 0x63), // Vendor Defined + HID_RI_LOGICAL_MINIMUM(8, 0x00), + HID_RI_LOGICAL_MAXIMUM(16, 0x00FF), + HID_RI_REPORT_COUNT(8, XAP_EPSIZE), + HID_RI_REPORT_SIZE(8, 0x08), + HID_RI_OUTPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE | HID_IOF_NON_VOLATILE), + HID_RI_END_COLLECTION(0), +}; +#endif // XAP_ENABLE + /* * Device descriptor */ @@ -1032,6 +1056,56 @@ const USB_Descriptor_Configuration_t PROGMEM ConfigurationDescriptor = { .PollingIntervalMS = USB_POLLING_INTERVAL_MS }, #endif + +#ifdef XAP_ENABLE + /* + * QMK XAP + */ + .Xap_Interface = { + .Header = { + .Size = sizeof(USB_Descriptor_Interface_t), + .Type = DTYPE_Interface + }, + .InterfaceNumber = XAP_INTERFACE, + .AlternateSetting = 0x00, + .TotalEndpoints = 2, + .Class = HID_CSCP_HIDClass, + .SubClass = HID_CSCP_NonBootSubclass, + .Protocol = HID_CSCP_NonBootProtocol, + .InterfaceStrIndex = NO_DESCRIPTOR + }, + .Xap_HID = { + .Header = { + .Size = sizeof(USB_HID_Descriptor_HID_t), + .Type = HID_DTYPE_HID + }, + .HIDSpec = VERSION_BCD(1, 1, 1), + .CountryCode = 0x00, + .TotalReportDescriptors = 1, + .HIDReportType = HID_DTYPE_Report, + .HIDReportLength = sizeof(XapReport) + }, + .Xap_INEndpoint = { + .Header = { + .Size = sizeof(USB_Descriptor_Endpoint_t), + .Type = DTYPE_Endpoint + }, + .EndpointAddress = (ENDPOINT_DIR_IN | XAP_IN_EPNUM), + .Attributes = (EP_TYPE_INTERRUPT | ENDPOINT_ATTR_NO_SYNC | ENDPOINT_USAGE_DATA), + .EndpointSize = XAP_EPSIZE, + .PollingIntervalMS = 0x01 + }, + .Xap_OUTEndpoint = { + .Header = { + .Size = sizeof(USB_Descriptor_Endpoint_t), + .Type = DTYPE_Endpoint + }, + .EndpointAddress = (ENDPOINT_DIR_OUT | XAP_OUT_EPNUM), + .Attributes = (EP_TYPE_INTERRUPT | ENDPOINT_ATTR_NO_SYNC | ENDPOINT_USAGE_DATA), + .EndpointSize = XAP_EPSIZE, + .PollingIntervalMS = 0x01 + }, +#endif }; /* @@ -1167,12 +1241,14 @@ uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const break; #endif + #if defined(JOYSTICK_ENABLE) && !defined(JOYSTICK_SHARED_EP) case JOYSTICK_INTERFACE: Address = &ConfigurationDescriptor.Joystick_HID; Size = sizeof(USB_HID_Descriptor_HID_t); break; #endif + #if defined(DIGITIZER_ENABLE) && !defined(DIGITIZER_SHARED_EP) case DIGITIZER_INTERFACE: Address = &ConfigurationDescriptor.Digitizer_HID; @@ -1180,6 +1256,14 @@ uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const break; #endif + +#ifdef XAP_ENABLE + case XAP_INTERFACE: + Address = &ConfigurationDescriptor.Xap_HID; + Size = sizeof(USB_HID_Descriptor_HID_t); + + break; +#endif } break; @@ -1236,6 +1320,14 @@ uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const Size = sizeof(DigitizerReport); break; #endif + +#ifdef XAP_ENABLE + case XAP_INTERFACE: + Address = &XapReport; + Size = sizeof(XapReport); + + break; +#endif } break; diff --git a/tmk_core/protocol/usb_descriptor.h b/tmk_core/protocol/usb_descriptor.h index 1de8c5ec88a3..526d054f75ae 100644 --- a/tmk_core/protocol/usb_descriptor.h +++ b/tmk_core/protocol/usb_descriptor.h @@ -144,6 +144,14 @@ typedef struct { USB_HID_Descriptor_HID_t Digitizer_HID; USB_Descriptor_Endpoint_t Digitizer_INEndpoint; #endif + +#ifdef XAP_ENABLE + // XAP HID Interface + USB_Descriptor_Interface_t Xap_Interface; + USB_HID_Descriptor_HID_t Xap_HID; + USB_Descriptor_Endpoint_t Xap_INEndpoint; + USB_Descriptor_Endpoint_t Xap_OUTEndpoint; +#endif } USB_Descriptor_Configuration_t; /* @@ -193,6 +201,10 @@ enum usb_interfaces { #if defined(DIGITIZER_ENABLE) && !defined(DIGITIZER_SHARED_EP) DIGITIZER_INTERFACE, #endif + +#ifdef XAP_ENABLE + XAP_INTERFACE, +#endif TOTAL_INTERFACES }; @@ -269,6 +281,15 @@ enum usb_endpoints { # define DIGITIZER_IN_EPNUM SHARED_IN_EPNUM # endif #endif + +#ifdef XAP_ENABLE + XAP_IN_EPNUM = NEXT_EPNUM, +# if STM32_USB_USE_OTG1 +# define XAP_OUT_EPNUM XAP_IN_EPNUM +# else + XAP_OUT_EPNUM = NEXT_EPNUM, +# endif +#endif }; #ifdef PROTOCOL_LUFA @@ -280,7 +301,7 @@ enum usb_endpoints { #endif #if (NEXT_EPNUM - 1) > MAX_ENDPOINTS -# error There are not enough available endpoints to support all functions. Please disable one or more of the following: Mouse Keys, Extra Keys, Console, NKRO, MIDI, Serial, Steno +# error There are not enough available endpoints to support all functions. Please disable one or more of the following: Mouse Keys, Extra Keys, Console, NKRO, MIDI, Serial, Steno, XAP #endif #define KEYBOARD_EPSIZE 8 @@ -293,5 +314,6 @@ enum usb_endpoints { #define CDC_EPSIZE 16 #define JOYSTICK_EPSIZE 8 #define DIGITIZER_EPSIZE 8 +#define XAP_EPSIZE 64 uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const uint16_t wLength, const void** const DescriptorAddress); diff --git a/tmk_core/protocol/vusb/vusb.c b/tmk_core/protocol/vusb/vusb.c index c8ab49425366..637ce288ff2c 100644 --- a/tmk_core/protocol/vusb/vusb.c +++ b/tmk_core/protocol/vusb/vusb.c @@ -39,6 +39,11 @@ along with this program. If not, see . # include "joystick.h" #endif +#ifdef XAP_ENABLE +# include "xap.h" +# include +#endif + #if defined(CONSOLE_ENABLE) # define RBUF_SIZE 128 # include "ring_buffer.h" @@ -74,6 +79,10 @@ enum usb_interfaces { CONSOLE_INTERFACE, #endif +#ifdef XAP_ENABLE + XAP_INTERFACE, +#endif + TOTAL_INTERFACES }; @@ -152,7 +161,7 @@ void raw_hid_send(uint8_t *data, uint8_t length) { return; } - send_report(4, data, 32); + send_report(4, data, RAW_BUFFER_SIZE); } __attribute__((weak)) void raw_hid_receive(uint8_t *data, uint8_t length) { @@ -175,6 +184,80 @@ void raw_hid_task(void) { } #endif +/*------------------------------------------------------------------* + * XAP + *------------------------------------------------------------------*/ +#ifdef XAP_ENABLE +# define XAP_BUFFER_SIZE 64 +# define XAP_EPSIZE 8 + +static uint8_t xap_output_buffer[XAP_BUFFER_SIZE]; +static uint8_t xap_output_received_bytes = 0; + +extern void xap_receive(xap_token_t token, const uint8_t *data, size_t length); + +void xap_send_base(uint8_t *data, uint8_t length) { + if (length != XAP_BUFFER_SIZE) { + return; + } + + send_report(4, data, XAP_BUFFER_SIZE); +} + +void xap_send(xap_token_t token, xap_response_flags_t response_flags, const void *data, size_t length) { + uint8_t rdata[XAP_BUFFER_SIZE] = {0}; + xap_response_header_t *header = (xap_response_header_t *)&rdata[0]; + header->token = token; + + if (length > (XAP_BUFFER_SIZE - sizeof(xap_response_header_t))) response_flags &= ~(XAP_RESPONSE_FLAG_SUCCESS); + header->flags = response_flags; + + if (response_flags & (XAP_RESPONSE_FLAG_SUCCESS)) { + header->length = (uint8_t)length; + if (data != NULL) { + memcpy(&rdata[sizeof(xap_response_header_t)], data, length); + } + } + xap_send_base(rdata, sizeof(rdata)); +} + +void xap_broadcast(uint8_t type, const void *data, size_t length) { + uint8_t rdata[XAP_BUFFER_SIZE] = {0}; + xap_broadcast_header_t *header = (xap_broadcast_header_t *)&rdata[0]; + header->token = XAP_BROADCAST_TOKEN; + header->type = type; + + if (length > (XAP_BUFFER_SIZE - sizeof(xap_broadcast_header_t))) return; + + header->length = (uint8_t)length; + if (data != NULL) { + memcpy(&rdata[sizeof(xap_broadcast_header_t)], data, length); + } + xap_send_base(rdata, sizeof(rdata)); +} + +void xap_receive_base(const void *data) { + const uint8_t * u8data = (const uint8_t *)data; + xap_request_header_t *header = (xap_request_header_t *)&u8data[0]; + if (header->length <= (XAP_BUFFER_SIZE - sizeof(xap_request_header_t))) { + xap_receive(header->token, &u8data[sizeof(xap_request_header_t)], header->length); + } +} + +void xap_task(void) { + usbPoll(); + + if (!usbConfiguration || !usbInterruptIsReady4()) { + return; + } + + if (xap_output_received_bytes == XAP_BUFFER_SIZE) { + xap_receive_base(xap_output_buffer); + xap_output_received_bytes = 0; + } +} +#endif + /*------------------------------------------------------------------* * Console *------------------------------------------------------------------*/ @@ -379,6 +462,24 @@ void usbFunctionWriteOut(uchar *data, uchar len) { raw_output_received_bytes += len; } #endif +#ifdef XAP_ENABLE + // Data from host must be divided every 8bytes + if (len != 8) { + dprint("XAP: invalid length\n"); + xap_output_received_bytes = 0; + return; + } + + if (xap_output_received_bytes + len > XAP_BUFFER_SIZE) { + dprint("XAP: buffer full\n"); + xap_output_received_bytes = 0; + } else { + for (uint8_t i = 0; i < 8; i++) { + xap_output_buffer[xap_output_received_bytes + i] = data[i]; + } + xap_output_received_bytes += len; + } +#endif } /*------------------------------------------------------------------* @@ -729,6 +830,29 @@ const PROGMEM uchar console_hid_report[] = { }; #endif +#ifdef XAP_ENABLE +const PROGMEM uchar xap_report[] = { + 0x06, 0x51, 0xFF, // Usage Page (Vendor Defined) + 0x09, 0x58, // Usage (Vendor Defined) + 0xA1, 0x01, // Collection (Application) + // Data to host + 0x09, 0x62, // Usage (Vendor Defined) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x95, XAP_BUFFER_SIZE, // Report Count + 0x75, 0x08, // Report Size (8) + 0x81, 0x02, // Input (Data, Variable, Absolute) + // Data from host + 0x09, 0x63, // Usage (Vendor Defined) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x00, // Logical Maximum (255) + 0x95, XAP_BUFFER_SIZE, // Report Count + 0x75, 0x08, // Report Size (8) + 0x91, 0x02, // Output (Data, Variable, Absolute) + 0xC0 // End Collection +}; +#endif + #ifndef USB_MAX_POWER_CONSUMPTION # define USB_MAX_POWER_CONSUMPTION 500 #endif @@ -993,6 +1117,56 @@ const PROGMEM usbConfigurationDescriptor_t usbConfigurationDescriptor = { .bInterval = 0x01 }, # endif + +# if defined(XAP_ENABLE) + /* + * XAP + */ + .xapInterface = { + .header = { + .bLength = sizeof(usbInterfaceDescriptor_t), + .bDescriptorType = USBDESCR_INTERFACE + }, + .bInterfaceNumber = XAP_INTERFACE, + .bAlternateSetting = 0x00, + .bNumEndpoints = 2, + .bInterfaceClass = 0x03, + .bInterfaceSubClass = 0x00, + .bInterfaceProtocol = 0x00, + .iInterface = 0x00 + }, + .xapHID = { + .header = { + .bLength = sizeof(usbHIDDescriptor_t), + .bDescriptorType = USBDESCR_HID + }, + .bcdHID = 0x0101, + .bCountryCode = 0x00, + .bNumDescriptors = 1, + .bDescriptorType = USBDESCR_HID_REPORT, + .wDescriptorLength = sizeof(xap_report) + }, + .xapINEndpoint = { + .header = { + .bLength = sizeof(usbEndpointDescriptor_t), + .bDescriptorType = USBDESCR_ENDPOINT + }, + .bEndpointAddress = (USBRQ_DIR_DEVICE_TO_HOST | USB_CFG_EP4_NUMBER), + .bmAttributes = 0x03, + .wMaxPacketSize = XAP_EPSIZE, + .bInterval = USB_POLLING_INTERVAL_MS + }, + .xapOUTEndpoint = { + .header = { + .bLength = sizeof(usbEndpointDescriptor_t), + .bDescriptorType = USBDESCR_ENDPOINT + }, + .bEndpointAddress = (USBRQ_DIR_HOST_TO_DEVICE | USB_CFG_EP4_NUMBER), + .bmAttributes = 0x03, + .wMaxPacketSize = XAP_EPSIZE, + .bInterval = USB_POLLING_INTERVAL_MS + }, +# endif }; // clang-format on @@ -1063,6 +1237,13 @@ USB_PUBLIC usbMsgLen_t usbFunctionDescriptor(struct usbRequest *rq) { len = sizeof(usbHIDDescriptor_t); break; #endif + +#if defined(XAP_ENABLE) + case XAP_INTERFACE: + usbMsgPtr = (usbMsgPtr_t)&usbConfigurationDescriptor.xapHID; + len = sizeof(usbHIDDescriptor_t); + break; +#endif } break; case USBDESCR_HID_REPORT: @@ -1095,6 +1276,13 @@ USB_PUBLIC usbMsgLen_t usbFunctionDescriptor(struct usbRequest *rq) { len = sizeof(console_hid_report); break; #endif + +#if defined(XAP_ENABLE) + case XAP_INTERFACE: + usbMsgPtr = (usbMsgPtr_t)xap_report; + len = sizeof(xap_report); + break; +#endif } break; } diff --git a/tmk_core/protocol/vusb/vusb.h b/tmk_core/protocol/vusb/vusb.h index 4750e95bf25f..91779de88a39 100644 --- a/tmk_core/protocol/vusb/vusb.h +++ b/tmk_core/protocol/vusb/vusb.h @@ -115,6 +115,13 @@ typedef struct usbConfigurationDescriptor { usbHIDDescriptor_t consoleHID; usbEndpointDescriptor_t consoleINEndpoint; #endif + +#if defined(XAP_ENABLE) + usbInterfaceDescriptor_t xapInterface; + usbHIDDescriptor_t xapHID; + usbEndpointDescriptor_t xapINEndpoint; + usbEndpointDescriptor_t xapOUTEndpoint; +#endif } __attribute__((packed)) usbConfigurationDescriptor_t; extern bool vusb_suspended; diff --git a/util/regen.sh b/util/regen.sh index ab03018893c0..cfa472299300 100755 --- a/util/regen.sh +++ b/util/regen.sh @@ -1,6 +1,9 @@ #!/bin/bash set -e +qmk generate-lighting-map -f rgblight -o quantum/rgblight/lighting_map.h +qmk generate-lighting-map -f rgb_matrix -o quantum/rgb_matrix/lighting_map.h +qmk generate-lighting-map -f led_matrix -o quantum/led_matrix/lighting_map.h qmk generate-rgb-breathe-table -o quantum/rgblight/rgblight_breathe_table.h qmk generate-keycodes --version latest -o quantum/keycodes.h qmk generate-keycodes-tests --version latest -o tests/test_common/keycode_table.cpp