Initial implementation of XAP protocol.

This commit is contained in:
Nick Brassel 2021-08-11 21:08:32 +10:00
parent f4c447f2df
commit eba91c6e28
34 changed files with 1934 additions and 4 deletions

View File

@ -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

View File

@ -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

178
data/xap/xap_0.0.1.hjson Executable file
View File

@ -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
}
}
}
}
}

248
data/xap/xap_0.1.0.hjson Executable file
View File

@ -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: {
}
}
}
}

80
data/xap/xap_0.2.0.hjson Executable file
View File

@ -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
}
}
}
}
}

62
docs/xap_0.0.1.md Normal file
View File

@ -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` |

78
docs/xap_0.1.0.md Normal file
View File

@ -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_.

78
docs/xap_0.2.0.md Normal file
View File

@ -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_.

5
docs/xap_protocol.md Normal file
View File

@ -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)

65
lib/python/qmk/casing.py Executable file
View File

@ -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)])

View File

@ -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',
]

View File

View File

@ -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()

View File

@ -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))

View File

@ -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)

View File

@ -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"

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
'''
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 <http://www.gnu.org/licenses/>.
'''
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"
#
################################################################################
'''

View File

81
lib/python/qmk/xap/common.py Executable file
View File

@ -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 <QMK>/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]) + ")"

View File

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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

116
quantum/xap/xap.c Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include <quantum.h>
#include <xap.h>
#include <usb_descriptor.h>
#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 <xap_generated.inl>
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); }

44
quantum/xap/xap.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdlib.h>
#include <stdint.h>
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 <xap_generated.h>

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include <quantum.h>
#include <xap.h>
#include <info_json_gz.h>
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; }

View File

@ -7,5 +7,6 @@ hjson
jsonschema>=3
milc>=1.4.2
pygments
pyhash
pyusb
qmk-dotty-dict

View File

@ -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 */

View File

@ -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

View File

@ -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;

View File

@ -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);