Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This repository contains libraries for building a Hall-effect keyboard firmware.
- [x] **Web Configurator**: Configure the firmware using [hmkconf](https://github.com/peppapighs/hmkconf) without needing to recompile the firmware.
- [x] **Tick Rate**: Customizable tick rate for Tap-Hold and Dynamic Keystroke.
- [x] **8kHz Polling Rate**: Support for 8kHz polling rate on some microcontrollers (e.g., AT32F405xx).
- [x] **Gamepad**: Support for XInput gamepad mode, allowing the keyboard to be used as a game controller.
- [x] **Gamepad**: Selectable XInput or standards-based HID gamepad output.

## Limitations

Expand Down Expand Up @@ -59,10 +59,18 @@ This repository contains libraries for building a Hall-effect keyboard firmware.

6. Flash the firmware to your keyboard using your preferred method (e.g., DFU, ISP). If your keyboard has a DFU bootloader, you can set `upload_protocol = dfu` in `platformio.ini` and use the command `pio run --target upload` or the PlatformIO IDE's "Upload" option while the keyboard is in DFU mode. If your browser supports WebUSB, you can also use [WebUSB DFU](https://devanlai.github.io/webdfu/dfu-util/) (Recommended method).

### Gamepad API

Choose XInput, standard HID, or disable gamepad output. Both APIs share the
same button and axis configuration. Changing the API takes effect after USB
reconnection or reboot.

## Development

The development branch is `dev`, which contains the latest features and bug fixes. The corresponding `dev` branch of [hmkconf](https://github.com/peppapighs/hmkconf/tree/dev) deployed at [https://dev.hmkconf.com](https://dev.hmkconf.com) is required to configure the `dev` branch of the firmware. To contribute, please create a pull request against the `dev` branch.

See [tests/README.md](tests/README.md) for host regression tests.

### Developing a New Keyboard

To develop a new keyboard, create a new directory under `keyboards/` with your keyboard's name. This directory should include the following files:
Expand Down
2 changes: 1 addition & 1 deletion include/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// Firmware Version
//--------------------------------------------------------------------+

#define FIRMWARE_VERSION 0x0108
#define FIRMWARE_VERSION 0x0109

//--------------------------------------------------------------------+
// Common Headers
Expand Down
34 changes: 29 additions & 5 deletions include/eeconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ typedef struct __attribute__((packed)) {
uint16_t initial_bottom_out_threshold;
} eeconfig_calibration_t;

typedef enum {
GAMEPAD_API_DISABLED = 0,
GAMEPAD_API_XINPUT = 1,
GAMEPAD_API_HID = 2,
} gamepad_api_t;

// Keyboard options configuration
typedef union __attribute__((packed)) {
struct __attribute__((packed)) {
// Whether the XInput interface is enabled
bool xinput_enabled : 1;
bool _unused0 : 1;
// Mutually exclusive gamepad API. Value 3 is reserved and cannot be set
// through the configuration protocol.
gamepad_api_t gamepad_api : 2;
// Whether 8kHz polling rate is enabled. Only applicable if USB HS is
// enabled. If disabled, the 1kHz polling rate is used instead.
bool high_polling_rate_enabled : 1;
Expand All @@ -60,6 +66,24 @@ typedef union __attribute__((packed)) {
_Static_assert(sizeof(eeconfig_options_t) == sizeof(uint16_t),
"Invalid eeconfig_options_t size");

static inline gamepad_api_t
eeconfig_get_gamepad_api(const eeconfig_options_t *options) {
if (options == NULL)
return GAMEPAD_API_DISABLED;
// Preserve the legacy bit's priority for reserved value 3 in corrupted
// storage. New configuration writes reject this value.
return options->gamepad_api <= GAMEPAD_API_HID ? options->gamepad_api
: GAMEPAD_API_XINPUT;
}

static inline bool
eeconfig_set_gamepad_api(eeconfig_options_t *options, gamepad_api_t api) {
if (options == NULL || (unsigned int)api > GAMEPAD_API_HID)
return false;
options->gamepad_api = api;
return true;
}

// Keyboard profile configuration
typedef struct __attribute__((packed)) {
uint8_t keymap[NUM_LAYERS][NUM_KEYS];
Expand All @@ -74,7 +98,7 @@ typedef struct __attribute__((packed)) {
// Persistent configuration version. The size of the configuration must be
// non-decreasing, so that the migration can assume that the new version is at
// least as large as the previous version.
#define EECONFIG_VERSION 0x0105
#define EECONFIG_VERSION 0x0106

// Keyboard configuration
// Whenever there is a change in the configuration, `EECONFIG_VERSION` must be
Expand Down Expand Up @@ -127,7 +151,7 @@ extern const eeconfig_t *eeconfig;
// Default global options
#define DEFAULT_OPTIONS \
{ \
.xinput_enabled = false, \
.gamepad_api = GAMEPAD_API_DISABLED, \
.high_polling_rate_enabled = true, \
}
#endif
Expand Down
4 changes: 2 additions & 2 deletions include/tusb_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
#define CFG_TUD_ENDPOINT0_SIZE 64

// Driver configuration
// Keyboard, generic, and raw HID interfaces
#define CFG_TUD_HID 3
// Keyboard, generic, raw HID, and optional standards-based gamepad interfaces
#define CFG_TUD_HID 4

// HID buffer size. Must be at least the size of the largest reports (+1 for
// interface with multiple reports)
Expand Down
13 changes: 8 additions & 5 deletions include/usb_descriptors.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,24 @@ enum {
USB_ITF_KEYBOARD = 0,
USB_ITF_HID,
USB_ITF_RAW_HID,
// We intentionally put the XInput interface last, so that if it is not
// enabled, we can subtract its size from the total configuration length
// without affecting the other interfaces.
USB_ITF_XINPUT,
// The optional gamepad is last so the configuration can expose either an
// XInput vendor interface, a standard HID gamepad, or neither.
USB_ITF_GAMEPAD,
USB_ITF_COUNT,
};

#define USB_ITF_XINPUT USB_ITF_GAMEPAD

// In endpoint addresses
enum {
EP_IN_ADDR_KEYBOARD = 0x81,
EP_IN_ADDR_HID,
EP_IN_ADDR_RAW_HID,
EP_IN_ADDR_XINPUT,
EP_IN_ADDR_GAMEPAD,
};

#define EP_IN_ADDR_XINPUT EP_IN_ADDR_GAMEPAD

// Out endpoint addresses
enum {
EP_OUT_ADDR_RAW_HID = 0x01,
Expand Down
16 changes: 13 additions & 3 deletions scripts/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import os
import utils
from schema.keyboard import KeyboardUSBPort
from uuid import uuid4
from uuid import NAMESPACE_URL, uuid5

METADATA_TEMPLATE = """#pragma once

Expand All @@ -41,7 +41,14 @@


def ms_os_20_guid_def():
uuid = uuid4().hex.upper()
# This identifies an interface class, not an individual keyboard. Include
# the target name so different designs sharing VID/PID do not share a GUID;
# keep it stable across builds for applications discovering this interface.
identity = (
f"https://github.com/peppapighs/libhmk/keyboards/{keyboard}/usb/"
f"{int(kb_json.usb.vid, 16):04x}:{int(kb_json.usb.pid, 16):04x}/xinput"
)
uuid = uuid5(NAMESPACE_URL, identity).hex.upper()
guid = f"{{{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}}}"
guid_array = [f"U16_TO_U8S_LE('{x}')" for x in guid] + ["U16_TO_U8S_LE('\\0')"] * 2

Expand All @@ -61,12 +68,15 @@ def keyboard_metadata_def():
"numAdvancedKeys": kb_json.keyboard.num_advanced_keys,
"numDynamicKeystrokeMaxBindings": kb_json.keyboard.num_dynamic_keystroke_max_bindings,
"numMacroNodes": kb_json.keyboard.num_macro_nodes,
# Older firmware metadata has no HID capability. Configurators can use
# this additive field without guessing support from an option bit.
"gamepadApis": ["xinput", "hid"],
"layout": kb_json.layout.model_dump(exclude_none=True),
"defaultKeymaps": utils.resolve_default_keymaps(kb_json),
}

uncompressed = json.dumps(metadata).encode("utf-8")
compressed = gzip.compress(uncompressed)
compressed = gzip.compress(uncompressed, mtime=0)
print(f"Uncompressed metadata size: {len(uncompressed)} bytes")
print(f"Compressed metadata size: {len(compressed)} bytes")

Expand Down
29 changes: 26 additions & 3 deletions src/commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@

static const uint8_t keyboard_metadata[] = {KEYBOARD_METADATA};

static bool gamepad_options_are_valid(const gamepad_options_t *options) {
if (options == NULL)
return false;
for (uint8_t i = 0u; i < 3u; i++) {
if (options->analog_curve[i][0] >= options->analog_curve[i + 1u][0])
return false;
}
return true;
}

static bool gamepad_buttons_are_valid(const uint8_t *buttons, uint8_t len) {
if (buttons == NULL)
return false;
for (uint8_t i = 0u; i < len; i++) {
if (buttons[i] > GP_BUTTON_RT)
return false;
}
return true;
}

// `volatile` to prevent compiler optimizations
static volatile bool command_request_pending;
static volatile bool command_response_pending;
Expand Down Expand Up @@ -242,7 +262,9 @@ static void command_process(void) {
break;
}
case COMMAND_SET_OPTIONS: {
success = EECONFIG_WRITE(options, &in->options);
eeconfig_options_t options = in->options;
COMMAND_VERIFY(options.gamepad_api <= GAMEPAD_API_HID);
success = EECONFIG_WRITE(options, &options);
break;
}
case COMMAND_RESET_PROFILE: {
Expand Down Expand Up @@ -447,6 +469,7 @@ static void command_process(void) {
COMMAND_VERIFY(p->offset < NUM_KEYS);
COMMAND_VERIFY(p->len <= M_ARRAY_SIZE(p->gamepad_buttons) &&
p->len <= NUM_KEYS - p->offset);
COMMAND_VERIFY(gamepad_buttons_are_valid(p->gamepad_buttons, p->len));

success = EECONFIG_WRITE_N(profiles[p->profile].gamepad_buttons[p->offset],
p->gamepad_buttons, sizeof(uint8_t) * p->len);
Expand All @@ -456,14 +479,14 @@ static void command_process(void) {
const command_in_gamepad_options_t *p = &in->gamepad_options;

COMMAND_VERIFY(p->profile < NUM_PROFILES);

out->gamepad_options = eeconfig->profiles[p->profile].gamepad_options;
break;
}
case COMMAND_SET_GAMEPAD_OPTIONS: {
const command_in_gamepad_options_t *p = &in->gamepad_options;

COMMAND_VERIFY(p->profile < NUM_PROFILES);
COMMAND_VERIFY(gamepad_options_are_valid(&p->gamepad_options));

success = EECONFIG_WRITE(profiles[p->profile].gamepad_options,
&p->gamepad_options);
Expand Down Expand Up @@ -491,4 +514,4 @@ void command_task(void) {
tud_hid_n_report(USB_ITF_RAW_HID, 0, out_buf, RAW_HID_EP_SIZE))
// The command response has been sent, so clear the queue.
command_response_pending = false;
}
}
4 changes: 3 additions & 1 deletion src/layout.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ void layout_task(void) {
const key_state_t *k = &key_matrix[i];
const bool last_key_press = bitmap_get(key_press_states, i);

if ((current_layer == 0) & eeconfig->options.xinput_enabled) {
if ((current_layer == 0) &
(eeconfig_get_gamepad_api(&eeconfig->options) !=
GAMEPAD_API_DISABLED)) {
// XInput key only applies to layer 0. We process it first since the
// subsequent key processing may be skipped due to the gamepad options.
if (CURRENT_PROFILE.gamepad_buttons[i] != GP_BUTTON_NONE) {
Expand Down
53 changes: 50 additions & 3 deletions src/migration.c
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ static bool v1_5_profile_config_func(uint8_t profile, uint8_t *dst,
NUM_ADVANCED_KEYS * MIGRATION_V1_5_ADVANCED_KEY_SIZE + \
NUM_MACRO_NODES * MIGRATION_V1_5_MACRO_NODE_SIZE + NUM_KEYS + 9 + 1)

static bool v1_6_global_config_func(uint8_t *dst, const uint8_t *src);
static bool v1_6_profile_config_func(uint8_t profile, uint8_t *dst,
const uint8_t *src);
#define MIGRATION_V1_6_GLOBAL_CONFIG_SIZE MIGRATION_V1_5_GLOBAL_CONFIG_SIZE
#define MIGRATION_V1_6_PROFILE_CONFIG_SIZE MIGRATION_V1_5_PROFILE_CONFIG_SIZE

// Migration metadata for each configuration version. The first entry is
// reserved for the initial version (v1.0) which does not require migration.
static const migration_t migrations[] = {
Expand Down Expand Up @@ -111,13 +117,20 @@ static const migration_t migrations[] = {
.global_config_func = v1_5_global_config_func,
.profile_config_func = v1_5_profile_config_func,
},
{
.version = 0x0106,
.global_config_size = MIGRATION_V1_6_GLOBAL_CONFIG_SIZE,
.profile_config_size = MIGRATION_V1_6_PROFILE_CONFIG_SIZE,
.global_config_func = v1_6_global_config_func,
.profile_config_func = v1_6_profile_config_func,
},
};

// An assertion to remind us to bump the persistent configuration version, and
// implement a migration function if there is a change to the configuration
// type. Update the assertion when a new version is added.
_Static_assert(MIGRATION_V1_5_GLOBAL_CONFIG_SIZE +
NUM_PROFILES * MIGRATION_V1_5_PROFILE_CONFIG_SIZE ==
_Static_assert(MIGRATION_V1_6_GLOBAL_CONFIG_SIZE +
NUM_PROFILES * MIGRATION_V1_6_PROFILE_CONFIG_SIZE ==
offsetof(eeconfig_t, magic_end),
"Invalid configuration size");

Expand All @@ -132,6 +145,14 @@ bool migration_try_migrate(void) {
return false;

const uint16_t config_version = eeconfig->version;
bool version_is_supported = false;
for (uint32_t i = 0; i < M_ARRAY_SIZE(migrations); i++)
version_is_supported |= migrations[i].version == config_version;
/* Never reinterpret an unknown/future layout as the current one. Also do
* not merely repair magic_end on an otherwise corrupt current layout. */
if (!version_is_supported || config_version >= EECONFIG_VERSION)
return false;

// We alternate between two buffers to save the memory.
uint8_t current_buf = 0;
uint8_t bufs[2][sizeof(eeconfig_t)];
Expand Down Expand Up @@ -390,4 +411,30 @@ bool v1_5_profile_config_func(uint8_t profile, uint8_t *dst,
migration_memcpy(&dst, &src, NUM_KEYS + 9 + 1);

return true;
}
}

//--------------------------------------------------------------------+
// v1.5 -> v1.6 Migration (gamepad API selector)
//--------------------------------------------------------------------+

static bool v1_6_global_config_func(uint8_t *dst, const uint8_t *src) {
if (((const eeconfig_t *)src)->version != 0x0105)
return false;

uint8_t *const global_config = dst;
migration_memcpy(&dst, &src, MIGRATION_V1_6_GLOBAL_CONFIG_SIZE);
// Only bit 0 selected XInput in v1.5. Bit 1 was reserved, but the historical
// v1.2 -> v1.3 migration set it. Ignore it when building the API selector so
// an upgrade never enables HID accidentally, or disables existing XInput.
eeconfig_options_t *options = &((eeconfig_t *)global_config)->options;
options->gamepad_api = (options->raw & 1u) ? GAMEPAD_API_XINPUT
: GAMEPAD_API_DISABLED;
return true;
}

static bool v1_6_profile_config_func(uint8_t profile, uint8_t *dst,
const uint8_t *src) {
(void)profile;
migration_memcpy(&dst, &src, MIGRATION_V1_6_PROFILE_CONFIG_SIZE);
return true;
}
Loading