/*---------------------------------------------------------*\ | QMKKeychronController.cpp | | | | Driver for Keychron QMK-based keyboards | | | | Amadej Kastelic 21 Jun 2026 | | Adam Honse 22 Jun 2026 | | | | This file is part of the OpenRGB project | | SPDX-License-Identifier: GPL-2.0-or-later | \*---------------------------------------------------------*/ #include #include "hsv.h" #include "QMKKeychronController.h" #include "QMKViaCommands.h" #include "StringUtils.h" #include "LogManager.h" using namespace std::chrono_literals; QMKKeychronController::QMKKeychronController(hid_device* dev_handle, const char *path) { /*-----------------------------------------------------*\ | Initialize controller fields | \*-----------------------------------------------------*/ dev = dev_handle; location = path; kc_protocol_version = 0; supported_features = 0; via_protocol_version = 0; /*-----------------------------------------------------*\ | Read product string | \*-----------------------------------------------------*/ wchar_t product_string[256]; int ret = hid_get_product_string(dev, product_string, 256); if(ret != 0) { name = ""; } else { name = StringUtils::wstring_to_string(product_string); } /*-----------------------------------------------------*\ | Read vendor string | \*-----------------------------------------------------*/ wchar_t vendor_string[256]; ret = hid_get_manufacturer_string(dev, vendor_string, 256); if(ret != 0) { vendor = ""; } else { vendor = StringUtils::wstring_to_string(vendor_string); } /*-----------------------------------------------------*\ | Read serial string | \*-----------------------------------------------------*/ wchar_t serial_string[256]; ret = hid_get_serial_number_string(dev, serial_string, 256); if(ret != 0) { serial = ""; } else { serial = StringUtils::wstring_to_string(serial_string); } /*-----------------------------------------------------*\ | Get VIA protocol version | \*-----------------------------------------------------*/ CmdGetViaProtocolVersion(&via_protocol_version); /*-----------------------------------------------------*\ | Get Keychron protocol version | \*-----------------------------------------------------*/ CmdGetKeychronProtocolVersion(&kc_protocol_version); /*-----------------------------------------------------*\ | Get supported Keychron features | \*-----------------------------------------------------*/ CmdGetSupportFeature(&supported_features); if(!GetSupported()) { return; } /*-----------------------------------------------------*\ | Get Keychron RGB protocol version | \*-----------------------------------------------------*/ CmdGetKeychronRGBProtocolVersion(&kc_rgb_protocol_version); /*-----------------------------------------------------*\ | Get count of LEDs | \*-----------------------------------------------------*/ CmdGetNumberLEDs(&number_leds); led_info.resize(number_leds); keycodes.resize(number_leds); for(std::size_t led_idx = 0; led_idx < led_info.size(); led_idx++) { led_info[led_idx].valid = false; } /*-----------------------------------------------------*\ | Get info and keycode for all LEDs | \*-----------------------------------------------------*/ for(unsigned char row = 0; row < 32; row++) { std::vector row_leds = CmdGetLEDIndexByRow(row); for(unsigned char col = 0; col < row_leds.size(); col++) { if(row_leds[col] != 0xFF && row_leds[col] < led_info.size() && led_info[row_leds[col]].valid == false) { led_info[row_leds[col]].valid = true; led_info[row_leds[col]].col = col; led_info[row_leds[col]].row = row; } } } for(unsigned short led_index = 0; led_index < number_leds; led_index++) { keycodes[led_index] = CmdGetKeycode(0, led_info[led_index].row, led_info[led_index].col); } } QMKKeychronController::~QMKKeychronController() { hid_close(dev); } std::string QMKKeychronController::GetLocation() { return("HID: " + location); } std::string QMKKeychronController::GetName() { return(name); } std::string QMKKeychronController::GetSerial() { return(serial); } std::string QMKKeychronController::GetVendor() { return(vendor); } std::string QMKKeychronController::GetVersion() { /*-----------------------------------------------------*\ | Format multi-line version text | \*-----------------------------------------------------*/ return("VIA: " + std::to_string(via_protocol_version) + "\r\n" + "Keychron: " + std::to_string(kc_protocol_version) + "\r\n" + "Keychron RGB: " + std::to_string(kc_rgb_protocol_version)); } bool QMKKeychronController::GetSupported() { return(supported_features & KC_FEATURE_KEYCHRON_RGB); } unsigned short QMKKeychronController::GetKeycode(unsigned short led_index) { return(keycodes[led_index]); } unsigned short QMKKeychronController::GetLEDCount() { return(number_leds); } kc_led_info QMKKeychronController::GetLEDInfo(unsigned short led_index) { return(led_info[led_index]); } void QMKKeychronController::SaveMode() { CmdSaveMode(); } void QMKKeychronController::SendLEDs(unsigned short number_leds, RGBColor* color_data) { unsigned short led_start_index = 0; unsigned char number_packet_leds = 9; while(led_start_index < number_leds) { if((number_leds - led_start_index) < 9) { number_packet_leds = (number_leds - led_start_index); } CmdSendLEDs(led_start_index, number_packet_leds, &color_data[led_start_index]); led_start_index += number_packet_leds; } } void QMKKeychronController::SetMode(unsigned short mode, unsigned char speed, unsigned char hue, unsigned char sat, unsigned char val) { if(mode == 0xFFFF) { CmdSetRGBMatrixMode(KEYCHRON_QHE_PER_KEY_RGB_EFFECT); CmdSetPerKeyRGBType(KEYCHRON_PER_KEY_RGB_SOLID); } else { CmdSetRGBMatrixMode((unsigned char)mode); CmdSetColorHS(hue, sat); CmdSetBrightness(val); CmdSetSpeed(speed); } } unsigned short QMKKeychronController::CmdGetKeycode ( unsigned char layer, unsigned char row, unsigned char col ) { unsigned char args[3]; unsigned char response[5]; unsigned short keycode; args[0] = layer; args[1] = row; args[2] = col; ViaSendCommand(QMK_VIA_CMD_VIA_DYNAMIC_KEYMAP_GET_KEYCODE, args, sizeof(args), response, sizeof(response)); keycode = ( response[3] << 8 )| response[4]; return(keycode); } void QMKKeychronController::CmdGetKeychronProtocolVersion ( unsigned char* kc_protocol_version ) { ViaSendCommand(KC_GET_PROTOCOL_VERSION, NULL, 0, (unsigned char*)kc_protocol_version, sizeof(unsigned char)); } void QMKKeychronController::CmdGetKeychronRGBProtocolVersion(unsigned short* kc_rgb_protocol_version) { ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_PROTOCOL_VER, NULL, 0, (unsigned char*)kc_rgb_protocol_version, sizeof(unsigned short)); /*-----------------------------------------------------*\ | The RGB protocol version byte order is reversed | \*-----------------------------------------------------*/ *kc_rgb_protocol_version = ((*kc_rgb_protocol_version & 0x00FF) << 8) | ((*kc_rgb_protocol_version & 0xFF00) >> 8); } std::vector QMKKeychronController::CmdGetLEDIndexByRow(unsigned char row) { unsigned char args[4]; unsigned char response[KEYCHRON_QHE_PACKET_SIZE - 2]; args[0] = row; args[1] = 0xFF; args[2] = 0xFF; args[3] = 0xFF; int bytes_read = ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_LED_IDX, args, sizeof(args), response, sizeof(response)); std::vector result; if(bytes_read > 0) { for(int i = 1; i < bytes_read; i++) { result.push_back(response[i] == 0xFF ? -1 : response[i]); } } return result; } void QMKKeychronController::CmdGetNumberLEDs ( unsigned short* number_leds ) { ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_LED_COUNT, NULL, 0, (unsigned char*)number_leds, sizeof(unsigned short)); /*-----------------------------------------------------*\ | The LED count byte order is reversed | \*-----------------------------------------------------*/ *number_leds = ((*number_leds & 0x00FF) << 8) | ((*number_leds & 0xFF00) >> 8); } void QMKKeychronController::CmdGetSupportFeature(unsigned short* supported_features) { ViaSendCommand(KC_GET_SUPPORT_FEATURE, NULL, 0, (unsigned char*)supported_features, sizeof(unsigned short)); /*-----------------------------------------------------*\ | The supported features byte order is reversed | \*-----------------------------------------------------*/ *supported_features = ((*supported_features & 0x00FF) << 8) | ((*supported_features & 0xFF00) >> 8); } void QMKKeychronController::CmdGetViaProtocolVersion ( unsigned short* via_protocol_version ) { ViaSendCommand(QMK_VIA_CMD_GET_PROTOCOL_VERSION, NULL, 0, (unsigned char*)via_protocol_version, sizeof(unsigned short)); /*-----------------------------------------------------*\ | The protocol version byte order is reversed | \*-----------------------------------------------------*/ *via_protocol_version = ((*via_protocol_version & 0x00FF) << 8) | ((*via_protocol_version & 0xFF00) >> 8); } void QMKKeychronController::CmdSaveMode() { ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_SAVE, NULL, 0, NULL, 0); } void QMKKeychronController::CmdSendLEDs(unsigned char start_index, unsigned char number_leds, RGBColor* color_data) { unsigned char args[KEYCHRON_QHE_PACKET_SIZE - 2]; args[0] = start_index; args[1] = number_leds; if(number_leds > 9) { number_leds = 9; } for(unsigned char led_index = 0; led_index < number_leds; led_index++) { /*-------------------------------------------------*\ | VialRGB sends direct packets in HSV for some | | inexplicable reason, so do the RGB to HSV | | conversion before sending | \*-------------------------------------------------*/ hsv_t hsv_color; rgb2hsv(color_data[led_index], &hsv_color); args[2 + (led_index * 3)] = (unsigned char)((float)hsv_color.hue * (256.0f / 360.0f)); args[3 + (led_index * 3)] = hsv_color.saturation; args[4 + (led_index * 3)] = hsv_color.value; } ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_PER_KEY_SET_COLOR, args, sizeof(args), NULL, 0); } void QMKKeychronController::CmdSetBrightness(unsigned char brightness) { unsigned char args[3]; args[0] = QMK_VIA_RGB_MATRIX_CHANNEL; args[1] = QMK_VIA_RGB_MATRIX_BRIGHTNESS; args[2] = brightness; ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0); } void QMKKeychronController::CmdSetColorHS(unsigned char h, unsigned char s) { unsigned char args[4]; args[0] = QMK_VIA_RGB_MATRIX_CHANNEL; args[1] = QMK_VIA_RGB_MATRIX_COLOR; args[2] = h; args[3] = s; ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0); } void QMKKeychronController::CmdSetPerKeyRGBType(unsigned char type) { unsigned char args[1]; args[0] = type; ViaSendCommandSub(KC_KEYCHRON_RGB, KEYCHRON_RGB_PER_KEY_SET_TYPE, args, sizeof(args), NULL, 0); } void QMKKeychronController::CmdSetRGBMatrixMode(unsigned char mode) { unsigned char args[3]; args[0] = QMK_VIA_RGB_MATRIX_CHANNEL; args[1] = QMK_VIA_RGB_MATRIX_EFFECT; args[2] = mode; ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0); } void QMKKeychronController::CmdSetSpeed(unsigned char speed) { unsigned char args[3]; args[0] = QMK_VIA_RGB_MATRIX_CHANNEL; args[1] = QMK_VIA_RGB_MATRIX_EFFECT_SPEED; args[2] = speed; ViaSendCommand(QMK_VIA_CMD_CUSTOM_SET_VALUE, args, sizeof(args), NULL, 0); } int QMKKeychronController::ViaSendCommand ( unsigned char cmd, unsigned char* data_in, unsigned char data_in_size, unsigned char* data_out, unsigned char data_out_size ) { /*-----------------------------------------------------*\ | Standard VIA command with no sub-command | | | | Byte 0: Command | | Byte 1+: Data | \*-----------------------------------------------------*/ unsigned char usb_buf[KEYCHRON_QHE_PACKET_SIZE + 1]; memset(usb_buf, 0, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Write command, offsetting by 1 for HID report ID | \*-----------------------------------------------------*/ usb_buf[1] = cmd; memcpy(&usb_buf[2], data_in, data_in_size); hid_write(dev, usb_buf, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Read response | \*-----------------------------------------------------*/ int bytes_received = hid_read_timeout(dev, usb_buf, sizeof(usb_buf) - 1, 1000); if(usb_buf[0] != cmd) { return(-1); } memcpy(data_out, &usb_buf[1], data_out_size); return(bytes_received - 1); } int QMKKeychronController::ViaSendCommandSub ( unsigned char cmd, unsigned char subcmd, unsigned char* data_in, unsigned char data_in_size, unsigned char* data_out, unsigned char data_out_size ) { /*-----------------------------------------------------*\ | Standard VIA command with sub-command | | | | Byte 0: Command | | Byte 1: Sub-Command | | Byte 2+: Data | \*-----------------------------------------------------*/ unsigned char usb_buf[KEYCHRON_QHE_PACKET_SIZE + 1]; memset(usb_buf, 0, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Write command, offsetting by 1 for HID report ID | \*-----------------------------------------------------*/ usb_buf[1] = cmd; usb_buf[2] = subcmd; memcpy(&usb_buf[3], data_in, data_in_size); hid_write(dev, usb_buf, sizeof(usb_buf)); /*-----------------------------------------------------*\ | Read response | \*-----------------------------------------------------*/ int bytes_received = hid_read_timeout(dev, usb_buf, sizeof(usb_buf) - 1, 1000); if(usb_buf[0] != cmd || usb_buf[1] != subcmd) { return(-1); } memcpy(data_out, &usb_buf[2], data_out_size); return(bytes_received - 2); }