diff --git a/build_keyboard.mk b/build_keyboard.mk
index 38ca2aaa99b..79bd87326ca 100644
--- a/build_keyboard.mk
+++ b/build_keyboard.mk
@@ -352,6 +352,30 @@ VPATH += $(KEYBOARD_PATHS)
VPATH += $(COMMON_VPATH)
include common_features.mk
+
+# XAP embedded info.json
+ifeq ($(strip $(XAP_ENABLE)), yes)
+
+$(KEYMAP_OUTPUT)/src/info_json_gz.h: $(INFO_JSON_FILES)
+ mkdir -p $(KEYMAP_OUTPUT)/src
+ $(QMK_BIN) info -f json -kb $(KEYBOARD) -km $(KEYMAP) | gzip -c9 > $(KEYMAP_OUTPUT)/src/info.json.gz
+ cd $(KEYMAP_OUTPUT)/src >/dev/null 2>&1 \
+ && xxd -i info.json.gz info_json_gz.h \
+ && cd - >/dev/null 2>&1
+
+XAP_FILES := $(shell ls -1 data/xap/* | sort | xargs echo)
+
+$(KEYMAP_OUTPUT)/src/xap_generated.inl: $(XAP_FILES)
+ $(QMK_BIN) xap-generate-qmk-inc -o "$(KEYMAP_OUTPUT)/src/xap_generated.inl"
+
+$(KEYMAP_OUTPUT)/src/xap_generated.h: $(XAP_FILES)
+ $(QMK_BIN) xap-generate-qmk-h -o "$(KEYMAP_OUTPUT)/src/xap_generated.h" -kb $(KEYBOARD)
+
+generated-files: $(KEYMAP_OUTPUT)/src/info_json_gz.h $(KEYMAP_OUTPUT)/src/xap_generated.inl $(KEYMAP_OUTPUT)/src/xap_generated.h
+
+VPATH += $(KEYMAP_OUTPUT)/src
+endif
+
include $(TMK_PATH)/protocol.mk
include $(TMK_PATH)/common.mk
include bootloader.mk
diff --git a/common_features.mk b/common_features.mk
index e442222eae5..9b5cfc55205 100644
--- a/common_features.mk
+++ b/common_features.mk
@@ -718,3 +718,15 @@ ifeq ($(strip $(USBPD_ENABLE)), yes)
endif
endif
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
+
+ OPT_DEFS += -DXAP_ENABLE
+ DYNAMIC_KEYMAP_ENABLE := yes
+ EMBED_INFO_JSON := yes
+ VPATH += $(QUANTUM_DIR)/xap
+ SRC += $(QUANTUM_DIR)/xap/xap.c $(QUANTUM_DIR)/xap/xap_handlers.c
+endif
diff --git a/data/xap/xap_0.0.1.hjson b/data/xap/xap_0.0.1.hjson
new file mode 100755
index 00000000000..34fd21d6237
--- /dev/null
+++ b/data/xap/xap_0.0.1.hjson
@@ -0,0 +1,178 @@
+{
+ 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!
+ term_definitions
+ !term_definitions!
+ request_response
+ reserved_tokens
+ response_flags
+ !response_flags!
+ example_conversation
+ ]
+
+ 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, allowing repsonse 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.
+
+ 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` |
+ '''
+ }
+
+ 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.
+ '''
+ ID:
+ '''
+ A single octet / 8-bit byte.
+ '''
+ 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.
+ '''
+ Token:
+ '''
+ A `u16` associated with a specific request as well as its corresponding response.
+ '''
+ Response:
+ '''
+ The data sent back to the host during execution of a _handler_.
+ '''
+ "Response Flags":
+ '''
+ An `u8` containing the status of the request.
+ '''
+ Payload:
+ '''
+ Any received data appended to the _route_, which gets delivered to the _handler_ when received.
+ '''
+ }
+
+ response_flags: {
+ define_prefix: XAP_RESP
+ 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}.
+ * Response:
+ * `u32` value.
+ '''
+ 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 00000000000..e24978f459a
--- /dev/null
+++ b/data/xap/xap_0.1.0.hjson
@@ -0,0 +1,248 @@
+{
+ version: 0.1.0
+
+ documentation: {
+ order: [
+ broadcast_messages
+ ]
+
+ reserved_tokens:
+ '''
+ Two token values are reserved: `0x0000` and `0xFFFF`:
+ * `0x0000`: 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, with 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.
+ '''
+ }
+ 6: {
+ name: Unlocking
+ define: UNLOCK_IN_PROGRESS
+ description:
+ '''
+ When this bit is set, an _unlock sequence_ is in progress.
+ '''
+ }
+ 7: {
+ name: Unlocked
+ define: UNLOCKED
+ description:
+ '''
+ When this bit is set, an _unlock sequence_ has completed, and _secure routes_ may be invoked.
+ '''
+ }
+ }
+ }
+
+ type_docs: {
+ 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 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_.
+ '''
+ }
+
+ 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 a `u8` signifying the length of the text, followed by the `u8[Length]` containing the text itself.
+
+ **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`(!) |
+ '''
+ }
+ }
+ }
+
+ 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
+ }
+ }
+ },
+
+ 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}.
+ * Response:
+ * `u32` value.
+ '''
+ 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_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)
+ }
+ }
+ },
+
+ 0x02: {
+ type: router
+ name: Keyboard
+ define: KB
+ description:
+ '''
+ This subsystem is always present, and reserved for user-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 00000000000..81c51873904
--- /dev/null
+++ b/data/xap/xap_0.2.0.hjson
@@ -0,0 +1,80 @@
+{
+ version: 0.2.0
+
+ routes: {
+ 0x04: {
+ type: router
+ name: Dynamic Keymap
+ define: DYNAMIC_KEYMAP
+ description:
+ '''
+ This subsystem allows for live modifications of the keymap, allowing keys to be reassigned without rebuilding the firmware.
+ '''
+ enable_if_preprocessor: defined(DYNAMIC_KEYMAP_ENABLE)
+ routes: {
+ 0x00: {
+ type: command
+ name: Capabilities Query
+ define: CAPABILITIES_QUERY
+ description:
+ '''
+ Dynamic 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_DYNAMIC_KEYMAP_CAPABILITIES
+ }
+ }
+ }
+
+ 0x05: {
+ type: router
+ name: Dynamic Encoders
+ define: DYNAMIC_ENCODER
+ description:
+ '''
+ This subsystem allows for live modifications of the keymap, allowing encoder functionality to be reassigned without rebuilding the firmware.
+ '''
+ enable_if_preprocessor: defined(DYNAMIC_KEYMAP_ENABLE) && defined(ENCODER_MAP_ENABLE)
+ routes: {
+ 0x00: {
+ type: command
+ name: Capabilities Query
+ define: CAPABILITIES_QUERY
+ description:
+ '''
+ Dynamic Encoders 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_DYNAMIC_ENCODER_CAPABILITIES
+ }
+ }
+ }
+
+ 0x06: {
+ type: router
+ name: Lighting
+ define: LIGHTING
+ description:
+ '''
+ This subsystem allows for control over the lighting subsystem.
+ '''
+ enable_if_preprocessor: defined(RGBLIGHT_ENABLE) || defined(RGB_MATRIX_ENABLE)
+ routes: {
+ 0x00: {
+ 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/docs/xap_0.0.1.md b/docs/xap_0.0.1.md
new file mode 100644
index 00000000000..ff45f1ae450
--- /dev/null
+++ b/docs/xap_0.0.1.md
@@ -0,0 +1,62 @@
+# 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. |
+| _ID_ | A single octet / 8-bit byte. |
+| _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_. |
+| _Response Flags_ | An `u8` containing the status of the request. |
+| _Route_ | A sequence of _IDs_ describing the route to invoke a _handler_. |
+| _Subsystem_ | A high-level area of functionality within XAP. |
+| _Token_ | A `u16` associated with a specific request as well as its corresponding 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, allowing repsonse 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.
+
+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`: 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` |
+
diff --git a/docs/xap_0.1.0.md b/docs/xap_0.1.0.md
new file mode 100644
index 00000000000..8dac167209f
--- /dev/null
+++ b/docs/xap_0.1.0.md
@@ -0,0 +1,78 @@
+# 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 |
+| -- | -- |
+| _struct{}_ | A structure of data, packing different objects together. Data is "compacted" -- there are no padding bytes between fields. Equivalent to a 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. |
+| _ID_ | A single octet / 8-bit byte. |
+| _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_. |
+| _Response Flags_ | An `u8` containing the status of the request. |
+| _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. |
+| _Token_ | A `u16` associated with a specific request as well as its corresponding response. |
+| _Unlock sequence_ | A physical sequence initiated by the user to enable execution of _secure routes_. |
+
+## 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, allowing repsonse 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.
+
+This token is followed by a `u8` signifying the length of data in the request.
+
+Two token values are reserved: `0x0000` and `0xFFFF`:
+* `0x0000`: 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 |
+|--|--|--|--|--|--|--|--|
+| Unlocked | Unlocking | - | - | - | - | Secure Failure | Success |
+
+* `Bit 7`: When this bit is set, an _unlock sequence_ has completed, and _secure routes_ may be invoked.
+* `Bit 6`: When this bit is set, an _unlock sequence_ is in progress.
+* `Bit 1`: When this bit is set, the requested _route_ was marked _secure_ but an _unlock sequence_ has not completed.
+* `Bit 0`: 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` |
+
+## 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, with corresponding _payload_.
+
diff --git a/docs/xap_0.2.0.md b/docs/xap_0.2.0.md
new file mode 100644
index 00000000000..8dac167209f
--- /dev/null
+++ b/docs/xap_0.2.0.md
@@ -0,0 +1,78 @@
+# 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 |
+| -- | -- |
+| _struct{}_ | A structure of data, packing different objects together. Data is "compacted" -- there are no padding bytes between fields. Equivalent to a 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. |
+| _ID_ | A single octet / 8-bit byte. |
+| _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_. |
+| _Response Flags_ | An `u8` containing the status of the request. |
+| _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. |
+| _Token_ | A `u16` associated with a specific request as well as its corresponding response. |
+| _Unlock sequence_ | A physical sequence initiated by the user to enable execution of _secure routes_. |
+
+## 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, allowing repsonse 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.
+
+This token is followed by a `u8` signifying the length of data in the request.
+
+Two token values are reserved: `0x0000` and `0xFFFF`:
+* `0x0000`: 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 |
+|--|--|--|--|--|--|--|--|
+| Unlocked | Unlocking | - | - | - | - | Secure Failure | Success |
+
+* `Bit 7`: When this bit is set, an _unlock sequence_ has completed, and _secure routes_ may be invoked.
+* `Bit 6`: When this bit is set, an _unlock sequence_ is in progress.
+* `Bit 1`: When this bit is set, the requested _route_ was marked _secure_ but an _unlock sequence_ has not completed.
+* `Bit 0`: 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` |
+
+## 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, with corresponding _payload_.
+
diff --git a/docs/xap_protocol.md b/docs/xap_protocol.md
new file mode 100644
index 00000000000..688d7dab3ea
--- /dev/null
+++ b/docs/xap_protocol.md
@@ -0,0 +1,5 @@
+# XAP Protocol Reference
+
+* [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/lib/python/qmk/casing.py b/lib/python/qmk/casing.py
new file mode 100755
index 00000000000..60b14cc5401
--- /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 dea0eaeaf93..fbad3b9329d 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -66,6 +66,9 @@ subcommands = [
'qmk.cli.new.keymap',
'qmk.cli.pyformat',
'qmk.cli.pytest',
+ 'qmk.cli.xap.generate_docs',
+ 'qmk.cli.xap.generate_json',
+ 'qmk.cli.xap.generate_qmk',
]
diff --git a/lib/python/qmk/cli/xap/__init__.py b/lib/python/qmk/cli/xap/__init__.py
new file mode 100755
index 00000000000..e69de29bb2d
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 00000000000..bf2fdb86bbb
--- /dev/null
+++ b/lib/python/qmk/cli/xap/generate_docs.py
@@ -0,0 +1,11 @@
+"""This script generates the XAP protocol documentation.
+"""
+from milc import cli
+from qmk.xap.gen_docs.generator import generate_docs
+
+
+@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/`.
+ """
+ generate_docs()
\ No newline at end of file
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 00000000000..e999f6ce87d
--- /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_qmk.py b/lib/python/qmk/cli/xap/generate_qmk.py
new file mode 100755
index 00000000000..0cb2fec1954
--- /dev/null
+++ b/lib/python/qmk/cli/xap/generate_qmk.py
@@ -0,0 +1,24 @@
+"""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.xap.gen_firmware.inline_generator import generate_inline
+from qmk.xap.gen_firmware.header_generator import generate_header
+
+
+@cli.argument('-o', '--output', type=normpath, help='File to write to')
+@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.
+ """
+ generate_inline(cli.args.output)
+
+
+@cli.argument('-o', '--output', type=normpath, help='File to write to')
+@cli.argument('-kb', '--keyboard', help='Name of the keyboard')
+@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.
+ """
+ generate_header(cli.args.output, cli.args.keyboard)
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 8c66228b2b7..58d7a381d68 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -87,11 +87,14 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
return create_make_target(':'.join(make_args), parallel, **env_vars)
-def get_git_version(current_time, repo_dir='.', check_dir='.'):
+def get_git_version(current_time=None, repo_dir='.', check_dir='.'):
"""Returns the current git version for a repo, or the current time.
"""
git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
+ if current_time is None:
+ current_time = strftime(time_fmt)
+
if repo_dir != '.':
repo_dir = Path('lib') / repo_dir
@@ -118,7 +121,7 @@ def create_version_h(skip_git=False, skip_all=False):
if skip_all:
current_time = "1970-01-01-00:00:00"
else:
- current_time = strftime(time_fmt)
+ current_time = None
if skip_git:
git_version = "NA"
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 71a6c91c778..c4bfc92a5f0 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -1,6 +1,7 @@
"""Information that should be available to the python library.
"""
from os import environ
+from datetime import date
from pathlib import Path
# The root of the qmk_firmware tree.
@@ -36,3 +37,92 @@ LED_INDICATORS = {
# Constants that should match their counterparts in make
BUILD_DIR = environ.get('BUILD_DIR', '.build')
KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_'
+
+# Headers for generated files
+this_year = date.today().year
+GPL2_HEADER_C_LIKE = f'''\
+/* Copyright {this_year} QMK
+ *
+ * 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 .
+ */
+'''
+
+GPL2_HEADER_SH_LIKE = f'''\
+# Copyright {this_year} QMK
+#
+# 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 .
+'''
+
+GENERATED_HEADER_C_LIKE = '''\
+/*******************************************************************************
+ 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"
+*******************************************************************************/
+'''
+
+GENERATED_HEADER_SH_LIKE = '''\
+################################################################################
+#
+# 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"
+#
+################################################################################
+'''
diff --git a/lib/python/qmk/xap/__init__.py b/lib/python/qmk/xap/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/python/qmk/xap/common.py b/lib/python/qmk/xap/common.py
new file mode 100755
index 00000000000..bec82d727bb
--- /dev/null
+++ b/lib/python/qmk/xap/common.py
@@ -0,0 +1,81 @@
+"""This script handles the XAP protocol data files.
+"""
+import re
+import hjson
+from typing import OrderedDict
+
+from qmk.constants import QMK_FIRMWARE
+
+
+def _merge_ordered_dicts(dicts):
+ """Merges nested OrderedDict objects resulting from reading a hjson file.
+
+ 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.
+ """
+
+ result = OrderedDict()
+
+ def add_entry(target, k, v):
+ if k in target and isinstance(v, OrderedDict):
+ if "!reset!" in v:
+ target[k] = v
+ else:
+ target[k] = _merge_ordered_dicts([target[k], v])
+ if "!reset!" in target[k]:
+ del target[k]["!reset!"]
+ elif k in target and isinstance(v, list):
+ if v[0] == '!reset!':
+ target[k] = v[1:]
+ else:
+ target[k] = target[k] + v
+ else:
+ target[k] = v
+
+ for d in dicts:
+ for (k, v) in d.items():
+ add_entry(result, k, v)
+
+ return result
+
+
+def get_xap_definition_files():
+ """Get the sorted list of XAP definition files, from /data/xap.
+ """
+ xap_defs = 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])
+
+
+def latest_xap_defs():
+ """Gets the latest version of the XAP definitions.
+ """
+ definitions = [hjson.load(file.open(encoding='utf-8')) for file in get_xap_definition_files()]
+ return _merge_ordered_dicts(definitions)
+
+
+def route_conditions(route_stack):
+ """Handles building the C preprocessor conditional based on the current route.
+ """
+ conditions = []
+ for route in route_stack:
+ if 'enable_if_preprocessor' in route:
+ conditions.append(route['enable_if_preprocessor'])
+
+ if len(conditions) == 0:
+ return None
+
+ return "(" + ' && '.join([f'({c})' for c in conditions]) + ")"
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 00000000000..e69de29bb2d
diff --git a/lib/python/qmk/xap/gen_docs/__init__.py b/lib/python/qmk/xap/gen_docs/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/python/qmk/xap/gen_docs/generator.py b/lib/python/qmk/xap/gen_docs/generator.py
new file mode 100755
index 00000000000..3e0ce528962
--- /dev/null
+++ b/lib/python/qmk/xap/gen_docs/generator.py
@@ -0,0 +1,103 @@
+"""This script generates the XAP protocol documentation.
+"""
+import hjson
+from qmk.constants import QMK_FIRMWARE
+from qmk.xap.common import get_xap_definition_files, update_xap_definitions
+
+
+def _update_type_docs(overall):
+ defs = overall['type_docs']
+
+ type_docs = []
+ for (k, v) in sorted(defs.items(), key=lambda x: x[0]):
+ type_docs.append(f'| _{k}_ | {v} |')
+
+ desc_str = "\n".join(type_docs)
+
+ overall['documentation']['!type_docs!'] = f'''\
+| Name | Definition |
+| -- | -- |
+{desc_str}
+'''
+
+
+def _update_term_definitions(overall):
+ defs = overall['term_definitions']
+
+ term_descriptions = []
+ for (k, v) in sorted(defs.items(), key=lambda x: x[0]):
+ term_descriptions.append(f'| _{k}_ | {v} |')
+
+ desc_str = "\n".join(term_descriptions)
+
+ overall['documentation']['!term_definitions!'] = f'''\
+| Name | Definition |
+| -- | -- |
+{desc_str}
+'''
+
+
+def _update_response_flags(overall):
+ flags = overall['response_flags']['bits']
+ for n in range(0, 8):
+ if str(n) not in flags:
+ flags[str(n)] = {"name": "-", "description": "-"}
+
+ header = '| ' + " | ".join([f'Bit {n}' for n in range(7, -1, -1)]) + ' |'
+ dividers = '|' + "|".join(['--' for n in range(7, -1, -1)]) + '|'
+ bit_names = '| ' + " | ".join([flags[str(n)]['name'] for n in range(7, -1, -1)]) + ' |'
+
+ bit_descriptions = ''
+ for n in range(7, -1, -1):
+ bit_desc = flags[str(n)]
+ if bit_desc['name'] != '-':
+ desc = bit_desc['description']
+ bit_descriptions = bit_descriptions + f'\n* `Bit {n}`: {desc}'
+
+ overall['documentation']['!response_flags!'] = f'''\
+{header}
+{dividers}
+{bit_names}
+{bit_descriptions}
+'''
+
+
+def generate_docs():
+ """Generates the XAP protocol documentation by merging the definitions files, and producing the corresponding Markdown document under `/docs/`.
+ """
+ docs_list = []
+
+ overall = None
+ for file in get_xap_definition_files():
+
+ overall = update_xap_definitions(overall, hjson.load(file.open(encoding='utf-8')))
+
+ try:
+ if 'type_docs' in overall:
+ _update_type_docs(overall)
+ if 'term_definitions' in overall:
+ _update_term_definitions(overall)
+ if 'response_flags' in overall:
+ _update_response_flags(overall)
+ except:
+ print(hjson.dumps(overall))
+ exit(1)
+
+ output_doc = QMK_FIRMWARE / "docs" / f"{file.stem}.md"
+ docs_list.append(output_doc)
+
+ with open(output_doc, "w", encoding='utf-8') as out_file:
+ for e in overall['documentation']['order']:
+ out_file.write(overall['documentation'][e].strip())
+ out_file.write('\n\n')
+
+ output_doc = QMK_FIRMWARE / "docs" / f"xap_protocol.md"
+ with open(output_doc, "w", encoding='utf-8') as out_file:
+ out_file.write('''\
+# XAP Protocol Reference
+
+''')
+
+ for file in reversed(sorted(docs_list)):
+ ver = file.stem[4:]
+ out_file.write(f'* [XAP Version {ver}]({file.name})\n')
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 00000000000..e69de29bb2d
diff --git a/lib/python/qmk/xap/gen_firmware/header_generator.py b/lib/python/qmk/xap/gen_firmware/header_generator.py
new file mode 100755
index 00000000000..9b295ed8f3c
--- /dev/null
+++ b/lib/python/qmk/xap/gen_firmware/header_generator.py
@@ -0,0 +1,136 @@
+"""This script generates the XAP protocol generated header to be compiled into QMK.
+"""
+import re
+import pyhash
+
+from qmk.commands import get_git_version
+from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
+from qmk.xap.common import latest_xap_defs, route_conditions
+
+
+def _append_route_defines(lines, container, container_id=None, route_stack=None):
+ """Handles building the list of the XAP routes, combining parent and child names together, as well as the route number.
+ """
+ if route_stack is None:
+ route_stack = [container]
+ else:
+ route_stack.append(container)
+
+ route_name = '_'.join([r['define'] for r in route_stack])
+
+ if container_id:
+ lines.append(f'#define {route_name} {container_id}')
+
+ if 'routes' in container:
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ _append_route_defines(lines, route, route_id, route_stack)
+
+ route_stack.pop()
+
+
+def _append_route_masks(lines, container, container_id=None, route_stack=None):
+ """Handles creating the equivalent XAP route masks, for capabilities checks. Forces value of `0` if disabled in the firmware.
+ """
+ if route_stack is None:
+ route_stack = [container]
+ else:
+ route_stack.append(container)
+
+ route_name = '_'.join([r['define'] for r in route_stack])
+ condition = route_conditions(route_stack)
+
+ if container_id:
+ if condition:
+ lines.append('')
+ lines.append(f'#if {condition}')
+
+ lines.append(f'#define {route_name}_MASK (1ul << ({route_name}))')
+
+ if condition:
+ lines.append(f'#else // {condition}')
+ lines.append(f'#define {route_name}_MASK 0')
+ lines.append(f'#endif // {condition}')
+ lines.append('')
+
+ if 'routes' in container:
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ _append_route_masks(lines, route, route_id, route_stack)
+
+ route_stack.pop()
+
+
+def _append_route_capabilities(lines, container, container_id=None, route_stack=None):
+ """Handles creating the equivalent XAP route masks, for capabilities checks. Forces value of `0` if disabled in the firmware.
+ """
+ if route_stack is None:
+ route_stack = [container]
+ else:
+ route_stack.append(container)
+
+ route_name = '_'.join([r['define'] for r in route_stack])
+
+ if 'routes' in container:
+ lines.append('')
+ lines.append(f'#define {route_name}_CAPABILITIES (0 \\')
+
+ if 'routes' in container:
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ route_stack.append(route)
+ child_name = '_'.join([r['define'] for r in route_stack])
+ lines.append(f' | ({child_name}_MASK) \\')
+ route_stack.pop()
+
+ lines.append(' )')
+
+ if 'routes' in container:
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ _append_route_capabilities(lines, route, route_id, route_stack)
+
+ route_stack.pop()
+
+
+def generate_header(output_file, keyboard):
+ """Generates the XAP protocol header file, generated during normal build.
+ """
+ xap_defs = latest_xap_defs()
+
+ # Preamble
+ lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '']
+
+ # Versions
+ prog = re.compile(r'^(\d+)\.(\d+)\.(\d+)')
+ b = prog.match(xap_defs['version'])
+ lines.append(f'#define XAP_BCD_VERSION 0x{int(b.group(1)):02d}{int(b.group(2)):02d}{int(b.group(3)):04d}ul')
+ b = prog.match(get_git_version())
+ lines.append(f'#define QMK_BCD_VERSION 0x{int(b.group(1)):02d}{int(b.group(2)):02d}{int(b.group(3)):04d}ul')
+ keyboard_id = pyhash.murmur3_32()(keyboard)
+ lines.append(f'#define XAP_KEYBOARD_IDENTIFIER 0x{keyboard_id:08X}ul')
+ lines.append('')
+
+ # Append the route and command defines
+ _append_route_defines(lines, xap_defs)
+ lines.append('')
+ _append_route_masks(lines, xap_defs)
+ lines.append('')
+ _append_route_capabilities(lines, xap_defs)
+ lines.append('')
+
+ # Generate the full output
+ xap_generated_inl = '\n'.join(lines)
+
+ # Clean up newlines
+ while "\n\n\n" in xap_generated_inl:
+ xap_generated_inl = xap_generated_inl.replace("\n\n\n", "\n\n")
+
+ if output_file:
+ if output_file.name == '-':
+ print(xap_generated_inl)
+ else:
+ output_file.parent.mkdir(parents=True, exist_ok=True)
+ if output_file.exists():
+ output_file.replace(output_file.parent / (output_file.name + '.bak'))
+ output_file.write_text(xap_generated_inl)
diff --git a/lib/python/qmk/xap/gen_firmware/inline_generator.py b/lib/python/qmk/xap/gen_firmware/inline_generator.py
new file mode 100755
index 00000000000..756b0e86664
--- /dev/null
+++ b/lib/python/qmk/xap/gen_firmware/inline_generator.py
@@ -0,0 +1,222 @@
+"""This script generates the XAP protocol generated header to be compiled into QMK.
+"""
+import pyhash
+
+from qmk.casing import to_snake
+from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
+from qmk.xap.common import latest_xap_defs, route_conditions
+
+
+def _get_c_type(xap_type):
+ if xap_type == 'bool':
+ return 'bool'
+ elif xap_type == 'u8':
+ return 'uint8_t'
+ elif xap_type == 'u16':
+ return 'uint16_t'
+ elif xap_type == 'u32':
+ return 'uint32_t'
+ elif xap_type == 'u64':
+ return 'uint64_t'
+ elif xap_type == 'struct':
+ return 'struct'
+ elif xap_type == 'string':
+ return 'const char *'
+ return 'unknown'
+
+
+def _get_route_type(container):
+ if 'routes' in container:
+ return 'XAP_ROUTE'
+ elif 'return_constant' in container:
+ if container['return_type'] == 'u32':
+ return 'XAP_VALUE'
+ elif container['return_type'] == 'struct':
+ return 'XAP_CONST_MEM'
+ elif container['return_type'] == 'string':
+ return 'XAP_CONST_MEM'
+ elif 'return_getter' in container:
+ if container['return_type'] == 'u32':
+ return 'XAP_GETTER'
+ return 'UNSUPPORTED'
+
+
+def _append_routing_table_declaration(lines, container, container_id, route_stack):
+ route_stack.append(container)
+
+ route_name = to_snake('_'.join([r['define'] for r in route_stack]))
+
+ if 'routes' in container:
+ pass
+
+ elif 'return_constant' in container:
+
+ if container['return_type'] == 'u32':
+ pass
+
+ elif container['return_type'] == 'struct':
+ lines.append('')
+ lines.append(f'static const struct {route_name}_t {{')
+
+ for member in container['return_struct_members']:
+ member_type = _get_c_type(member['type'])
+ member_name = to_snake(member['name'])
+ lines.append(f' const {member_type} {member_name};')
+
+ lines.append(f'}} {route_name}_data PROGMEM = {{')
+
+ for constant in container['return_constant']:
+ lines.append(f' {constant},')
+
+ lines.append(f'}};')
+
+ elif container['return_type'] == 'string':
+ constant = container['return_constant']
+ lines.append('')
+ lines.append(f'static const char {route_name}_str[] PROGMEM = {constant};')
+
+ elif 'return_getter' in container:
+
+ if container['return_type'] == 'u32':
+ lines.append('')
+ lines.append(f'extern uint32_t {route_name}_getter(void);')
+
+ elif container['return_type'] == 'struct':
+ pass
+
+ route_stack.pop()
+
+
+def _append_routing_table_entry_flags(lines, container, container_id, route_stack):
+ is_secure = 1 if ('secure' in container and container['secure'] is True) else 0
+ lines.append(f' .flags = {{')
+ lines.append(f' .type = {_get_route_type(container)},')
+ lines.append(f' .is_secure = {is_secure},')
+ lines.append(f' }},')
+
+
+def _append_routing_table_entry_route(lines, container, container_id, route_stack):
+ route_name = to_snake('_'.join([r['define'] for r in route_stack]))
+ lines.append(f' .child_routes = {route_name}_table,')
+ lines.append(f' .child_routes_len = sizeof({route_name}_table)/sizeof(xap_route_t),')
+
+
+def _append_routing_table_entry_u32value(lines, container, container_id, route_stack):
+ value = container['return_constant']
+ lines.append(f' .u32value = {value},')
+
+
+def _append_routing_table_entry_u32getter(lines, container, container_id, route_stack):
+ route_name = to_snake('_'.join([r['define'] for r in route_stack]))
+ lines.append(f' .u32getter = &{route_name}_getter,')
+
+
+def _append_routing_table_entry_const_data(lines, container, container_id, route_stack):
+ route_name = to_snake('_'.join([r['define'] for r in route_stack]))
+ lines.append(f' .const_data = &{route_name}_data,')
+ lines.append(f' .const_data_len = sizeof({route_name}_data),')
+
+
+def _append_routing_table_entry_string(lines, container, container_id, route_stack):
+ route_name = to_snake('_'.join([r['define'] for r in route_stack]))
+ lines.append(f' .const_data = {route_name}_str,')
+ lines.append(f' .const_data_len = sizeof({route_name}_str) - 1,')
+
+
+def _append_routing_table_entry(lines, container, container_id, route_stack):
+ route_stack.append(container)
+ route_name = '_'.join([r['define'] for r in route_stack])
+ condition = route_conditions(route_stack)
+
+ if condition:
+ lines.append(f'#if {condition}')
+
+ lines.append(f' [{route_name}] = {{')
+
+ _append_routing_table_entry_flags(lines, container, container_id, route_stack)
+ if 'routes' in container:
+ _append_routing_table_entry_route(lines, container, container_id, route_stack)
+ elif 'return_constant' in container:
+ if container['return_type'] == 'u32':
+ _append_routing_table_entry_u32value(lines, container, container_id, route_stack)
+ elif container['return_type'] == 'struct':
+ _append_routing_table_entry_const_data(lines, container, container_id, route_stack)
+ elif container['return_type'] == 'string':
+ _append_routing_table_entry_string(lines, container, container_id, route_stack)
+ elif 'return_getter' in container:
+ if container['return_type'] == 'u32':
+ _append_routing_table_entry_u32getter(lines, container, container_id, route_stack)
+
+ lines.append(f' }},')
+
+ if condition:
+ lines.append(f'#endif // {condition}')
+
+ route_stack.pop()
+
+
+def _append_routing_tables(lines, container, container_id=None, route_stack=None):
+ """Handles building the list of the XAP routes, combining parent and child names together, as well as the route number.
+ """
+ if route_stack is None:
+ route_stack = [container]
+ else:
+ route_stack.append(container)
+
+ route_name = to_snake('_'.join([r['define'] for r in route_stack]))
+ condition = route_conditions(route_stack)
+
+ if 'routes' in container:
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ _append_routing_tables(lines, route, route_id, route_stack)
+
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ _append_routing_table_declaration(lines, route, route_id, route_stack)
+
+ lines.append('')
+ if condition:
+ lines.append(f'#if {condition}')
+
+ lines.append(f'static const xap_route_t {route_name}_table[] PROGMEM = {{')
+
+ for route_id in container['routes']:
+ route = container['routes'][route_id]
+ _append_routing_table_entry(lines, route, route_id, route_stack)
+
+ lines.append('};')
+
+ if condition:
+ lines.append(f'#endif // {condition}')
+ lines.append('')
+
+ route_stack.pop()
+
+
+def generate_inline(output_file):
+ """Generates the XAP protocol header file, generated during normal build.
+ """
+ xap_defs = latest_xap_defs()
+
+ # Preamble
+ lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '']
+
+ # Add all the generated code
+ _append_routing_tables(lines, xap_defs)
+
+ # Generate the full output
+ xap_generated_inl = '\n'.join(lines)
+
+ # Clean up newlines
+ while "\n\n\n" in xap_generated_inl:
+ xap_generated_inl = xap_generated_inl.replace("\n\n\n", "\n\n")
+
+ if output_file:
+ if output_file.name == '-':
+ print(xap_generated_inl)
+ else:
+ output_file.parent.mkdir(parents=True, exist_ok=True)
+ if output_file.exists():
+ output_file.replace(output_file.parent / (output_file.name + '.bak'))
+ output_file.write_text(xap_generated_inl)
diff --git a/quantum/quantum.h b/quantum/quantum.h
index ffb5e0df45d..d9114a0aef2 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -184,6 +184,10 @@ extern layer_state_t layer_state;
# include "dynamic_keymap.h"
#endif
+#ifdef XAP_ENABLE
+# include "xap.h"
+#endif
+
#ifdef VIA_ENABLE
# include "via.h"
#endif
diff --git a/quantum/xap/xap.c b/quantum/xap/xap.c
new file mode 100644
index 00000000000..df25f799d59
--- /dev/null
+++ b/quantum/xap/xap.c
@@ -0,0 +1,116 @@
+/* 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
+
+#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_GETTER,
+ XAP_CONST_MEM,
+ TOTAL_XAP_ROUTE_TYPES
+} xap_route_type_t;
+
+#define XAP_ROUTE_TYPE_BIT_COUNT 3
+
+typedef struct __attribute__((packed)) xap_route_flags_t {
+ xap_route_type_t type : XAP_ROUTE_TYPE_BIT_COUNT;
+ uint8_t is_secure : 1;
+} 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
+ uint32_t u32value;
+
+ // XAP_GETTER
+ uint32_t (*u32getter)(void);
+
+ // XAP_CONST_MEM
+ struct {
+ const void * const_data;
+ const uint8_t const_data_len;
+ };
+ };
+};
+
+#include
+
+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));
+ 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_u32(token, route.u32value);
+ return;
+
+ case XAP_GETTER:
+ xap_respond_u32(token, (route.u32getter)());
+ 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 00000000000..f3cab6aef29
--- /dev/null
+++ b/quantum/xap/xap.h
@@ -0,0 +1,44 @@
+/* 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
+
+typedef uint8_t xap_identifier_t;
+typedef uint8_t xap_response_flags_t;
+typedef uint16_t xap_token_t;
+
+#ifndef XAP_SUBSYSTEM_VERSION_KB
+# define XAP_SUBSYSTEM_VERSION_KB 0
+#endif
+
+#ifndef XAP_SUBSYSTEM_VERSION_USER
+# define XAP_SUBSYSTEM_VERSION_USER 0
+#endif
+
+#define XAP_RESPONSE_FLAG_FAILED 0
+#define XAP_RESPONSE_FLAG_SUCCESS (1 << 0)
+
+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);
+
+#include
diff --git a/quantum/xap/xap_handlers.c b/quantum/xap/xap_handlers.c
new file mode 100644
index 00000000000..7c3d8127ce9
--- /dev/null
+++ b/quantum/xap/xap_handlers.c
@@ -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 .
+ */
+
+#include
+#include
+#include
+
+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;
+}
+
+uint32_t xap_route_qmk_ffffffffffffffff_getter(void) { return 0x12345678; }
diff --git a/requirements.txt b/requirements.txt
index 92381d7d518..079dcdb8acc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,5 +7,6 @@ hjson
jsonschema>=3
milc>=1.4.2
pygments
+pyhash
pyusb
qmk-dotty-dict
diff --git a/tmk_core/common/report.h b/tmk_core/common/report.h
index db6370657d5..cf9f1df5890 100644
--- a/tmk_core/common/report.h
+++ b/tmk_core/common/report.h
@@ -30,7 +30,8 @@ enum hid_report_ids {
REPORT_ID_SYSTEM,
REPORT_ID_CONSUMER,
REPORT_ID_NKRO,
- REPORT_ID_JOYSTICK
+ REPORT_ID_JOYSTICK,
+ REPORT_ID_XAP
};
/* Mouse buttons */
diff --git a/tmk_core/protocol/lufa/lufa.c b/tmk_core/protocol/lufa/lufa.c
index 4ac079e1687..16ad71414c6 100644
--- a/tmk_core/protocol/lufa/lufa.c
+++ b/tmk_core/protocol/lufa/lufa.c
@@ -85,6 +85,10 @@ extern keymap_config_t keymap_config;
# include "raw_hid.h"
#endif
+#ifdef XAP_ENABLE
+# include "xap.h"
+#endif
+
#ifdef JOYSTICK_ENABLE
# include "joystick.h"
#endif
@@ -249,6 +253,88 @@ static 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, uint8_t response_flags, const void *data, size_t length) {
+ uint8_t rdata[XAP_EPSIZE] = {0};
+ *(xap_token_t *)&rdata[0] = token;
+ if (length > (XAP_EPSIZE - 4)) response_flags &= ~(XAP_RESPONSE_FLAG_SUCCESS);
+ rdata[2] = response_flags;
+ if (response_flags & (XAP_RESPONSE_FLAG_SUCCESS)) {
+ rdata[3] = (uint8_t)length;
+ if (data != NULL) {
+ memcpy(&rdata[4], data, length);
+ }
+ }
+ xap_send_base(rdata, sizeof(rdata));
+}
+
+void xap_receive_base(const void *data) {
+ const uint8_t *u8data = (const uint8_t *)data;
+ xap_token_t token = *(xap_token_t *)&u8data[0];
+ uint8_t length = u8data[2];
+ if (length <= (XAP_EPSIZE - 3)) {
+ xap_receive(token, &u8data[3], length);
+ }
+}
+
+static 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
******************************************************************************/
@@ -500,6 +586,12 @@ void EVENT_USB_Device_ConfigurationChanged(void) {
ConfigSuccess &= Endpoint_ConfigureEndpoint((RAW_OUT_EPNUM | ENDPOINT_DIR_OUT), EP_TYPE_INTERRUPT, RAW_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
+
#ifdef CONSOLE_ENABLE
/* Setup console endpoint */
ConfigSuccess &= Endpoint_ConfigureEndpoint((CONSOLE_IN_EPNUM | ENDPOINT_DIR_IN), EP_TYPE_INTERRUPT, CONSOLE_EPSIZE, 1);
@@ -1102,6 +1194,10 @@ int main(void) {
raw_hid_task();
#endif
+#ifdef XAP_ENABLE
+ xap_task();
+#endif
+
#if !defined(INTERRUPT_CONTROL_ENDPOINT)
USB_USBTask();
#endif
diff --git a/tmk_core/protocol/usb_descriptor.c b/tmk_core/protocol/usb_descriptor.c
index 7a4a7903154..0a15e0c6d1d 100644
--- a/tmk_core/protocol/usb_descriptor.c
+++ b/tmk_core/protocol/usb_descriptor.c
@@ -227,6 +227,7 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM SharedReport[] = {
HID_RI_OUTPUT(8, HID_IOF_CONSTANT),
HID_RI_END_COLLECTION(0),
#endif
+
#ifdef SHARED_EP_ENABLE
};
#endif
@@ -255,6 +256,30 @@ const USB_Descriptor_HIDReport_Datatype_t PROGMEM RawReport[] = {
};
#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
+
#ifdef CONSOLE_ENABLE
const USB_Descriptor_HIDReport_Datatype_t PROGMEM ConsoleReport[] = {
HID_RI_USAGE_PAGE(16, 0xFF31), // Vendor Defined (PJRC Teensy compatible)
@@ -492,6 +517,56 @@ const USB_Descriptor_Configuration_t PROGMEM ConfigurationDescriptor = {
},
#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
+
#if defined(MOUSE_ENABLE) && !defined(MOUSE_SHARED_EP)
/*
* Mouse
@@ -1046,6 +1121,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
+
#ifdef CONSOLE_ENABLE
case CONSOLE_INTERFACE:
Address = &ConfigurationDescriptor.Console_HID;
@@ -1096,6 +1179,14 @@ uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const
break;
#endif
+#ifdef XAP_ENABLE
+ case XAP_INTERFACE:
+ Address = &XapReport;
+ Size = sizeof(XapReport);
+
+ break;
+#endif
+
#ifdef CONSOLE_ENABLE
case CONSOLE_INTERFACE:
Address = &ConsoleReport;
diff --git a/tmk_core/protocol/usb_descriptor.h b/tmk_core/protocol/usb_descriptor.h
index 867e549b4f8..e7afe68640c 100644
--- a/tmk_core/protocol/usb_descriptor.h
+++ b/tmk_core/protocol/usb_descriptor.h
@@ -75,6 +75,14 @@ typedef struct {
USB_Descriptor_Endpoint_t Raw_OUTEndpoint;
#endif
+#ifdef XAP_ENABLE
+ // Mouse 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
+
#if defined(MOUSE_ENABLE) && !defined(MOUSE_SHARED_EP)
// Mouse HID Interface
USB_Descriptor_Interface_t Mouse_Interface;
@@ -155,6 +163,10 @@ enum usb_interfaces {
RAW_INTERFACE,
#endif
+#ifdef XAP_ENABLE
+ XAP_INTERFACE,
+#endif
+
#if defined(MOUSE_ENABLE) && !defined(MOUSE_SHARED_EP)
MOUSE_INTERFACE,
#endif
@@ -212,6 +224,15 @@ enum usb_endpoints {
# 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 SHARED_EP_ENABLE
SHARED_IN_EPNUM = NEXT_EPNUM,
#endif
@@ -272,7 +293,7 @@ enum usb_endpoints {
// TODO - ARM_ATSAM
#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
@@ -284,5 +305,6 @@ enum usb_endpoints {
#define CDC_NOTIFICATION_EPSIZE 8
#define CDC_EPSIZE 16
#define JOYSTICK_EPSIZE 8
+#define XAP_EPSIZE 64
uint16_t get_usb_descriptor(const uint16_t wValue, const uint16_t wIndex, const void** const DescriptorAddress);