From 175f3ed3382802601fd2c77ab5abe51b3529d48c Mon Sep 17 00:00:00 2001 From: Amadej Kastelic Date: Mon, 22 Jun 2026 00:07:08 +0000 Subject: [PATCH] feat: add Keychron Q1 HE keyboard support and restructure Keychron controllers --- .../QMKKeychronController.cpp | 375 ++++++++++++++++++ .../QMKKeychronController.h | 162 ++++++++ .../QMKKeychronControllerDetect.cpp | 34 ++ .../RGBController_QMKKeychron.cpp | 303 ++++++++++++++ .../RGBController_QMKKeychron.h | 35 ++ 5 files changed, 909 insertions(+) create mode 100644 Controllers/QMKController/QMKKeychronController/QMKKeychronController.cpp create mode 100644 Controllers/QMKController/QMKKeychronController/QMKKeychronController.h create mode 100644 Controllers/QMKController/QMKKeychronController/QMKKeychronControllerDetect.cpp create mode 100644 Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.cpp create mode 100644 Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.h diff --git a/Controllers/QMKController/QMKKeychronController/QMKKeychronController.cpp b/Controllers/QMKController/QMKKeychronController/QMKKeychronController.cpp new file mode 100644 index 000000000..03767bbe2 --- /dev/null +++ b/Controllers/QMKController/QMKKeychronController/QMKKeychronController.cpp @@ -0,0 +1,375 @@ +/*---------------------------------------------------------*\ +| QMKKeychronController.cpp | +| | +| Driver for Keychron QMK-based keyboards | +| (Q1 HE and other KEYCHRON_RGB-enabled models) | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include "QMKKeychronController.h" +#include "StringUtils.h" +#include "LogManager.h" + +using namespace std::chrono_literals; + +QMKKeychronController::QMKKeychronController(hid_device* dev_handle, const hid_device_info& info, std::string dev_name) +{ + dev = dev_handle; + location = info.path; + name = dev_name; +} + +QMKKeychronController::~QMKKeychronController() +{ + hid_close(dev); +} + +std::string QMKKeychronController::GetDeviceLocation() +{ + return("HID: " + location); +} + +std::string QMKKeychronController::GetNameString() +{ + return(name); +} + +std::string QMKKeychronController::GetSerialString() +{ + wchar_t serial_string[128]; + int ret = hid_get_serial_number_string(dev, serial_string, 128); + + if(ret != 0) + { + return(""); + } + + return(StringUtils::wstring_to_string(serial_string)); +} + +void QMKKeychronController::SendPacket(unsigned char* data, size_t len) +{ + unsigned char usb_buf[KEYCHRON_QHE_PACKET_SIZE + 1]; + memset(usb_buf, 0x00, sizeof(usb_buf)); + + usb_buf[0] = 0x00; + + size_t copy_len = len; + if(copy_len > KEYCHRON_QHE_PACKET_SIZE) + { + copy_len = KEYCHRON_QHE_PACKET_SIZE; + } + + memcpy(&usb_buf[1], data, copy_len); + + hid_write(dev, usb_buf, KEYCHRON_QHE_PACKET_SIZE + 1); + + std::this_thread::sleep_for(5ms); +} + +int QMKKeychronController::ReadPacket(unsigned char* buf, size_t buf_len) +{ + unsigned char usb_buf[KEYCHRON_QHE_PACKET_SIZE + 1]; + memset(usb_buf, 0x00, sizeof(usb_buf)); + + int bytes_read = hid_read_timeout(dev, usb_buf, KEYCHRON_QHE_PACKET_SIZE + 1, KEYCHRON_QHE_HID_READ_TIMEOUT); + + if(bytes_read > 0) + { + size_t copy_len = (size_t)bytes_read; + if(copy_len > buf_len) + { + copy_len = buf_len; + } + memcpy(buf, usb_buf, copy_len); + } + + return bytes_read; +} + +unsigned int QMKKeychronController::GetLedCount() +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_KC_RGB_CMD_GROUP; + cmd[1] = KEYCHRON_KC_RGB_LED_COUNT; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + memset(response, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + int bytes_read = ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); + + if(bytes_read > 3 && response[0] == KEYCHRON_KC_RGB_CMD_GROUP && response[1] == KEYCHRON_KC_RGB_LED_COUNT) + { + return response[3]; + } + + return 0; +} + +std::vector QMKKeychronController::GetLedNumbersByRow(unsigned char row) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_KC_RGB_CMD_GROUP; + cmd[1] = KEYCHRON_KC_RGB_LED_IDX; + cmd[2] = row; + cmd[3] = 0xFF; + cmd[4] = 0xFF; + cmd[5] = 0xFF; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + memset(response, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + int bytes_read = ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); + + std::vector result; + + if(bytes_read > 3 && response[0] == KEYCHRON_KC_RGB_CMD_GROUP && response[1] == KEYCHRON_KC_RGB_LED_IDX) + { + for(int i = 3; i < bytes_read; i++) + { + result.push_back(response[i] == 0xFF ? -1 : response[i]); + } + } + + return result; +} + +std::vector> QMKKeychronController::GetAllLedNumbers(unsigned char num_rows) +{ + std::vector> all_rows; + + for(unsigned char row = 0; row < num_rows; row++) + { + std::vector row_data = GetLedNumbersByRow(row); + all_rows.push_back(row_data); + } + + return all_rows; +} + +void QMKKeychronController::SetPerKeyRgbColor(unsigned char start, unsigned char count, const std::vector& hsv_data) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_KC_RGB_CMD_GROUP; + cmd[1] = KEYCHRON_KC_RGB_PER_KEY_SET_COLOR; + cmd[2] = start; + cmd[3] = count; + + size_t data_offset = 4; + size_t copy_len = hsv_data.size(); + if(data_offset + copy_len > KEYCHRON_QHE_PACKET_SIZE) + { + copy_len = KEYCHRON_QHE_PACKET_SIZE - data_offset; + } + + memcpy(&cmd[data_offset], hsv_data.data(), copy_len); + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); +} + +std::vector QMKKeychronController::GetPerKeyRgbColor(unsigned char start, unsigned char count) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_KC_RGB_CMD_GROUP; + cmd[1] = KEYCHRON_KC_RGB_PER_KEY_GET_COLOR; + cmd[2] = start; + cmd[3] = count; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + memset(response, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + int bytes_read = ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); + + std::vector result; + + if(bytes_read > 3 && response[0] == KEYCHRON_KC_RGB_CMD_GROUP && response[1] == KEYCHRON_KC_RGB_PER_KEY_GET_COLOR) + { + size_t color_bytes = (size_t)count * 3; + if(3 + color_bytes <= (size_t)bytes_read) + { + for(size_t i = 3; i < 3 + color_bytes; i++) + { + result.push_back(response[i]); + } + } + } + + return result; +} + +void QMKKeychronController::SetPerKeyRgbType(unsigned char type) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_KC_RGB_CMD_GROUP; + cmd[1] = KEYCHRON_KC_RGB_PER_KEY_SET_TYPE; + cmd[2] = type; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); +} + +void QMKKeychronController::SetRgbMatrixMode(unsigned char mode) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_SET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_EFFECT; + cmd[3] = mode; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); +} + +unsigned char QMKKeychronController::GetRgbMatrixMode() +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_GET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_EFFECT; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + memset(response, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + int bytes_read = ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); + + if(bytes_read > 3) + { + return response[3]; + } + + return 0; +} + +void QMKKeychronController::SetBrightness(unsigned char brightness) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_SET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_BRIGHTNESS; + cmd[3] = brightness; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); +} + +unsigned char QMKKeychronController::GetBrightness() +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_GET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_BRIGHTNESS; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + memset(response, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + int bytes_read = ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); + + if(bytes_read > 3) + { + return response[3]; + } + + return 0; +} + +void QMKKeychronController::SetSpeed(unsigned char speed) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_SET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_SPEED; + cmd[3] = speed; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); +} + +unsigned char QMKKeychronController::GetSpeed() +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_GET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_SPEED; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + memset(response, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + int bytes_read = ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); + + if(bytes_read > 3) + { + return response[3]; + } + + return 0; +} + +void QMKKeychronController::SetColorHSV(unsigned char h, unsigned char s) +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_VIA_BACKLIGHT_SET_VALUE; + cmd[1] = KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX; + cmd[2] = KEYCHRON_VIA_BACKLIGHT_COLOR; + cmd[3] = h; + cmd[4] = s; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); +} + +void QMKKeychronController::SaveLedConf() +{ + unsigned char cmd[KEYCHRON_QHE_PACKET_SIZE]; + memset(cmd, 0x00, KEYCHRON_QHE_PACKET_SIZE); + + cmd[0] = KEYCHRON_KC_RGB_CMD_GROUP; + cmd[1] = KEYCHRON_KC_RGB_SAVE; + + SendPacket(cmd, KEYCHRON_QHE_PACKET_SIZE); + + unsigned char response[KEYCHRON_QHE_PACKET_SIZE]; + ReadPacket(response, KEYCHRON_QHE_PACKET_SIZE); +} diff --git a/Controllers/QMKController/QMKKeychronController/QMKKeychronController.h b/Controllers/QMKController/QMKKeychronController/QMKKeychronController.h new file mode 100644 index 000000000..c1022ef02 --- /dev/null +++ b/Controllers/QMKController/QMKKeychronController/QMKKeychronController.h @@ -0,0 +1,162 @@ +/*---------------------------------------------------------*\ +| QMKKeychronController.h | +| | +| Driver for Keychron QMK-based keyboards | +| (Q1 HE and other KEYCHRON_RGB-enabled models) | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include +#include + +/*---------------------------------------------------------*\ +| Keychron vendor ID | +\*---------------------------------------------------------*/ +#define KEYCHRON_VID 0x3434 + +/*---------------------------------------------------------*\ +| Product IDs | +\*---------------------------------------------------------*/ +#define KEYCHRON_Q1_HE_PID 0x0B10 + +/*---------------------------------------------------------*\ +| QMK raw HID usage page/usage | +\*---------------------------------------------------------*/ +#define KEYCHRON_QMK_USAGE_PAGE 0xFF60 +#define KEYCHRON_QMK_USAGE 0x61 + +/*---------------------------------------------------------*\ +| HID packet constants | +\*---------------------------------------------------------*/ +#define KEYCHRON_QHE_PACKET_SIZE 32 +#define KEYCHRON_QHE_HID_READ_TIMEOUT 1000 + +/*---------------------------------------------------------*\ +| KC_RGB command group (0xA8) | +| | +| Keychron-specific per-key RGB protocol. | +| Matches the firmware enum in | +| keyboards/keychron/common/rgb/keychron_rgb.c | +\*---------------------------------------------------------*/ +#define KEYCHRON_KC_RGB_CMD_GROUP 0xA8 + +enum KeychronKCRGBCommand +{ + KEYCHRON_KC_RGB_PROTOCOL_VER = 0x01, + KEYCHRON_KC_RGB_SAVE = 0x02, + KEYCHRON_KC_RGB_GET_INDICATORS = 0x03, + KEYCHRON_KC_RGB_SET_INDICATORS = 0x04, + KEYCHRON_KC_RGB_LED_COUNT = 0x05, + KEYCHRON_KC_RGB_LED_IDX = 0x06, + KEYCHRON_KC_RGB_PER_KEY_GET_TYPE = 0x07, + KEYCHRON_KC_RGB_PER_KEY_SET_TYPE = 0x08, + KEYCHRON_KC_RGB_PER_KEY_GET_COLOR = 0x09, + KEYCHRON_KC_RGB_PER_KEY_SET_COLOR = 0x0A, + KEYCHRON_KC_RGB_MIXED_GET_INFO = 0x0B, + KEYCHRON_KC_RGB_MIXED_GET_REGIONS = 0x0C, + KEYCHRON_KC_RGB_MIXED_SET_REGIONS = 0x0D, + KEYCHRON_KC_RGB_MIXED_GET_EFFECTS = 0x0E, + KEYCHRON_KC_RGB_MIXED_SET_EFFECTS = 0x0F, +}; + +/*---------------------------------------------------------*\ +| Per-key RGB animation types (PER_KEY_RGB_SET_TYPE value) | +\*---------------------------------------------------------*/ +enum KeychronPerKeyRgbType +{ + KEYCHRON_PER_KEY_RGB_SOLID = 0, + KEYCHRON_PER_KEY_RGB_BREATHING = 1, + KEYCHRON_PER_KEY_RGB_REACTIVE_SIMPLE = 2, + KEYCHRON_PER_KEY_RGB_REACTIVE_WIDE = 3, + KEYCHRON_PER_KEY_RGB_REACTIVE_SPLASH = 4, +}; + +/*---------------------------------------------------------*\ +| VIA backlight config commands | +| | +| Standard QMK/VIA rgb_matrix control. | +| Format: [cmd, value_type, value_id, value] | +| value_type = 3 for rgb_matrix | +\*---------------------------------------------------------*/ +enum KeychronVIABacklightCommand +{ + KEYCHRON_VIA_BACKLIGHT_SET_VALUE = 0x07, + KEYCHRON_VIA_BACKLIGHT_GET_VALUE = 0x08, + KEYCHRON_VIA_BACKLIGHT_SAVE = 0x09, +}; + +/*---------------------------------------------------------*\ +| VIA backlight value IDs | +\*---------------------------------------------------------*/ +#define KEYCHRON_VIA_BACKLIGHT_TYPE_RGB_MATRIX 0x03 + +enum KeychronVIABacklightValueID +{ + KEYCHRON_VIA_BACKLIGHT_BRIGHTNESS = 0x01, + KEYCHRON_VIA_BACKLIGHT_EFFECT = 0x02, + KEYCHRON_VIA_BACKLIGHT_SPEED = 0x03, + KEYCHRON_VIA_BACKLIGHT_COLOR = 0x04, +}; + +/*---------------------------------------------------------*\ +| Q1 HE effect IDs | +| | +| Determined by the number of standard effects enabled | +| in the Q1 HE firmware (info.json animations) + | +| 2 custom effects (PER_KEY_RGB, MIXED_RGB). | +| May need adjustment for other firmware versions. | +\*---------------------------------------------------------*/ +#define KEYCHRON_QHE_PER_KEY_RGB_EFFECT 23 + +/*---------------------------------------------------------*\ +| Brightness and speed ranges | +\*---------------------------------------------------------*/ +#define KEYCHRON_QHE_MIN_BRIGHTNESS 0x00 +#define KEYCHRON_QHE_MAX_BRIGHTNESS 0xFF +#define KEYCHRON_QHE_MIN_SPEED 0x00 +#define KEYCHRON_QHE_MAX_SPEED 0xFF + +class QMKKeychronController +{ +public: + QMKKeychronController(hid_device* dev_handle, const hid_device_info& info, std::string dev_name); + ~QMKKeychronController(); + + std::string GetDeviceLocation(); + std::string GetNameString(); + std::string GetSerialString(); + + unsigned int GetLedCount(); + std::vector GetLedNumbersByRow(unsigned char row); + std::vector> GetAllLedNumbers(unsigned char num_rows); + + void SetPerKeyRgbColor(unsigned char start, unsigned char count, const std::vector& hsv_data); + std::vector GetPerKeyRgbColor(unsigned char start, unsigned char count); + + void SetRgbMatrixMode(unsigned char mode); + unsigned char GetRgbMatrixMode(); + + void SetPerKeyRgbType(unsigned char type); + + void SetBrightness(unsigned char brightness); + unsigned char GetBrightness(); + + void SetSpeed(unsigned char speed); + unsigned char GetSpeed(); + + void SetColorHSV(unsigned char h, unsigned char s); + void SaveLedConf(); + +private: + hid_device* dev; + std::string location; + std::string name; + + void SendPacket(unsigned char* data, size_t len); + int ReadPacket(unsigned char* buf, size_t buf_len); +}; diff --git a/Controllers/QMKController/QMKKeychronController/QMKKeychronControllerDetect.cpp b/Controllers/QMKController/QMKKeychronController/QMKKeychronControllerDetect.cpp new file mode 100644 index 000000000..c537f5d8c --- /dev/null +++ b/Controllers/QMKController/QMKKeychronController/QMKKeychronControllerDetect.cpp @@ -0,0 +1,34 @@ +/*---------------------------------------------------------*\ +| QMKKeychronControllerDetect.cpp | +| | +| Detector for Keychron QMK-based keyboards | +| (Q1 HE and other KEYCHRON_RGB-enabled models) | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "Detector.h" +#include "QMKKeychronController.h" +#include "RGBController_QMKKeychron.h" + +/*---------------------------------------------------------*\ +| Q1 HE detector | +\*---------------------------------------------------------*/ +void DetectQMKKeychronController(hid_device_info* info, const std::string& name) +{ + hid_device* dev = hid_open_path(info->path); + + if(dev) + { + QMKKeychronController* controller = new QMKKeychronController(dev, *info, name); + RGBController_QMKKeychron* rgb_controller = new RGBController_QMKKeychron(controller); + + ResourceManager::get()->RegisterRGBController(rgb_controller); + } +} + +/*---------------------------------------------------------*\ +| Register HID detectors | +\*---------------------------------------------------------*/ +REGISTER_HID_DETECTOR_IPU("Keychron Q1 HE", DetectQMKKeychronController, KEYCHRON_VID, KEYCHRON_Q1_HE_PID, 1, KEYCHRON_QMK_USAGE_PAGE, KEYCHRON_QMK_USAGE); diff --git a/Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.cpp b/Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.cpp new file mode 100644 index 000000000..7cff89eaa --- /dev/null +++ b/Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.cpp @@ -0,0 +1,303 @@ +/*---------------------------------------------------------*\ +| RGBController_QMKKeychron.cpp | +| | +| RGBController for Keychron QMK-based keyboards | +| (Q1 HE and other KEYCHRON_RGB-enabled models) | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include +#include +#include +#include "RGBControllerKeyNames.h" +#include "RGBController_QMKKeychron.h" +#include "QMKKeychronController.h" + +#define NA 0xFFFFFFFF + +/**------------------------------------------------------------------*\ + @name Keychron Q1 HE + @category Keyboard + @type USB + @save :white_check_mark: + @direct :white_check_mark: + @effects :white_check_mark: + @detectors DetectQMKKeychronController + @comment +\*-------------------------------------------------------------------*/ + +typedef struct +{ + std::string name; + int value; + int flags; +} keychron_qhe_effect; + +static const keychron_qhe_effect qhe_effects[] = +{ + { "Direct", -1, MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_HAS_BRIGHTNESS }, + { "Solid Color", 0, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_AUTOMATIC_SAVE }, + { "Breathing", 1, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Band Spiral", 2, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle All", 3, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle Left Right", 4, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle Up Down", 5, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle Out In", 6, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle Out In Dual", 7, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle Pinwheel", 8, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Cycle Spiral", 9, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Dual Beacon", 10, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Rainbow Beacon", 11, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Rainbow Moving Chevron", 12, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Jellybean Raindrops", 13, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_AUTOMATIC_SAVE }, + { "Pixel Rain", 14, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_AUTOMATIC_SAVE }, + { "Typing Heatmap", 15, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_AUTOMATIC_SAVE }, + { "Digital Rain", 16, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Solid Reactive Simple", 17, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Solid Reactive Multiwide", 18, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Solid Reactive Multinexus", 19, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Splash", 20, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, + { "Solid Splash", 21, MODE_FLAG_HAS_RANDOM_COLOR | MODE_FLAG_HAS_MODE_SPECIFIC_COLOR | MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_SPEED | MODE_FLAG_AUTOMATIC_SAVE }, +}; + +RGBController_QMKKeychron::RGBController_QMKKeychron(QMKKeychronController* controller_ptr) +{ + controller = controller_ptr; + + name = controller->GetNameString(); + vendor = "Keychron"; + type = DEVICE_TYPE_KEYBOARD; + description = name; + location = controller->GetDeviceLocation(); + serial = controller->GetSerialString(); + + for(const keychron_qhe_effect& effect : qhe_effects) + { + mode m; + m.name = effect.name; + m.value = effect.value; + m.flags = effect.flags; + + if(m.flags & MODE_FLAG_HAS_MODE_SPECIFIC_COLOR) + { + m.color_mode = MODE_COLORS_MODE_SPECIFIC; + m.colors_min = 1; + m.colors_max = 1; + m.colors.resize(1); + } + else if(m.flags & MODE_FLAG_HAS_PER_LED_COLOR) + { + m.color_mode = MODE_COLORS_PER_LED; + m.colors_min = 0; + m.colors_max = 0; + m.colors.resize(0); + } + else + { + m.color_mode = MODE_COLORS_NONE; + m.colors_min = 0; + m.colors_max = 0; + m.colors.resize(0); + } + + if(m.flags & MODE_FLAG_HAS_SPEED) + { + m.speed_min = KEYCHRON_QHE_MIN_SPEED; + m.speed_max = KEYCHRON_QHE_MAX_SPEED; + m.speed = KEYCHRON_QHE_MAX_SPEED / 2; + } + + if(m.flags & MODE_FLAG_HAS_BRIGHTNESS) + { + m.brightness_min = KEYCHRON_QHE_MIN_BRIGHTNESS; + m.brightness_max = KEYCHRON_QHE_MAX_BRIGHTNESS; + m.brightness = KEYCHRON_QHE_MAX_BRIGHTNESS; + } + + modes.push_back(m); + } + + SetupZones(); +} + +RGBController_QMKKeychron::~RGBController_QMKKeychron() +{ + delete controller; +} + +void RGBController_QMKKeychron::SetupZones() +{ + unsigned int led_count = controller->GetLedCount(); + + zone keyboard_zone; + keyboard_zone.name = ZONE_EN_KEYBOARD; + keyboard_zone.type = ZONE_TYPE_MATRIX; + + if(led_count > 0) + { + keyboard_zone.matrix_map = new matrix_map_type; + keyboard_zone.matrix_map->height = 6; + keyboard_zone.matrix_map->width = 16; + + keyboard_zone.matrix_map->map = new unsigned int[6 * 16]; + memset(keyboard_zone.matrix_map->map, 0xFF, 6 * 16 * sizeof(unsigned int)); + + std::vector> led_map = controller->GetAllLedNumbers(6); + + unsigned int led_idx = 0; + for(unsigned int h = 0; h < 6 && h < led_map.size(); h++) + { + for(unsigned int w = 0; w < 16 && w < led_map[h].size(); w++) + { + int val = led_map[h][w]; + keyboard_zone.matrix_map->map[h * 16 + w] = (val >= 0) ? (unsigned int)val : NA; + + if(val >= 0) + { + led new_led; + new_led.name = "Key: " + std::to_string(val); + leds.push_back(new_led); + led_idx++; + } + } + } + + keyboard_zone.leds_min = led_idx; + keyboard_zone.leds_max = led_idx; + keyboard_zone.leds_count = led_idx; + } + else + { + keyboard_zone.matrix_map = nullptr; + keyboard_zone.leds_min = 0; + keyboard_zone.leds_max = 0; + keyboard_zone.leds_count = 0; + } + + zones.push_back(keyboard_zone); + + SetupColors(); +} + +void RGBController_QMKKeychron::ResizeZone(int /*zone*/, int /*new_size*/) +{ +} + +void RGBController_QMKKeychron::DeviceUpdateLEDs() +{ + UpdateZoneLEDs(0); +} + +void RGBController_QMKKeychron::UpdateZoneLEDs(int /*zone*/) +{ + if(active_mode == 0) + { + controller->SetRgbMatrixMode(KEYCHRON_QHE_PER_KEY_RGB_EFFECT); + controller->SetPerKeyRgbType(KEYCHRON_PER_KEY_RGB_SOLID); + + unsigned int total_leds = leds.size(); + unsigned char max_per_packet = 9; + + for(unsigned char start = 0; start < total_leds; start += max_per_packet) + { + unsigned char count = (unsigned char)((total_leds - start) > max_per_packet ? max_per_packet : (total_leds - start)); + + std::vector hsv_data; + hsv_data.reserve(count * 3); + + for(unsigned char i = 0; i < count; i++) + { + RGBColor color = colors[start + i]; + unsigned char r = RGBGetRValue(color); + unsigned char g = RGBGetGValue(color); + unsigned char b = RGBGetBValue(color); + unsigned char h, s, v; + + RGBToHSV(r, g, b, h, s, v); + + hsv_data.push_back(h); + hsv_data.push_back(s); + hsv_data.push_back(v); + } + + controller->SetPerKeyRgbColor(start, count, hsv_data); + } + + controller->SetBrightness(modes[active_mode].brightness); + } + else + { + int effect_id = modes[active_mode].value; + + controller->SetRgbMatrixMode((unsigned char)effect_id); + controller->SetBrightness(modes[active_mode].brightness); + controller->SetSpeed(modes[active_mode].speed); + + if(modes[active_mode].flags & MODE_FLAG_HAS_MODE_SPECIFIC_COLOR) + { + RGBColor color = modes[active_mode].colors[0]; + unsigned char r = RGBGetRValue(color); + unsigned char g = RGBGetGValue(color); + unsigned char b = RGBGetBValue(color); + unsigned char h, s, v; + + RGBToHSV(r, g, b, h, s, v); + + controller->SetColorHSV(h, s); + } + + controller->SaveLedConf(); + } +} + +void RGBController_QMKKeychron::UpdateSingleLED(int /*led*/) +{ + UpdateZoneLEDs(0); +} + +void RGBController_QMKKeychron::DeviceUpdateMode() +{ + UpdateZoneLEDs(0); +} + +void RGBController_QMKKeychron::RGBToHSV(unsigned char r, unsigned char g, unsigned char b, unsigned char& h, unsigned char& s, unsigned char& v) +{ + double rd = r / 255.0; + double gd = g / 255.0; + double bd = b / 255.0; + + double max_val = rd > gd ? (rd > bd ? rd : bd) : (gd > bd ? gd : bd); + double min_val = rd < gd ? (rd < bd ? rd : bd) : (gd < bd ? gd : bd); + double delta = max_val - min_val; + + v = (unsigned char)(max_val * 255.0); + + if(delta < 0.00001) + { + h = 0; + s = 0; + return; + } + + s = (unsigned char)((max_val > 0.0) ? (delta / max_val) * 255.0 : 0.0); + + double hd; + if(max_val == rd) + { + hd = (gd - bd) / delta; + if(gd < bd) hd += 6.0; + } + else if(max_val == gd) + { + hd = (bd - rd) / delta + 2.0; + } + else + { + hd = (rd - gd) / delta + 4.0; + } + + h = (unsigned char)((hd / 6.0) * 255.0); +} diff --git a/Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.h b/Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.h new file mode 100644 index 000000000..2b955a7db --- /dev/null +++ b/Controllers/QMKController/QMKKeychronController/RGBController_QMKKeychron.h @@ -0,0 +1,35 @@ +/*---------------------------------------------------------*\ +| RGBController_QMKKeychron.h | +| | +| RGBController for Keychron QMK-based keyboards | +| (Q1 HE and other KEYCHRON_RGB-enabled models) | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "QMKKeychronController.h" + +class RGBController_QMKKeychron : public RGBController +{ +public: + RGBController_QMKKeychron(QMKKeychronController* controller_ptr); + ~RGBController_QMKKeychron(); + + void SetupZones(); + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); + +private: + QMKKeychronController* controller; + + static void RGBToHSV(unsigned char r, unsigned char g, unsigned char b, unsigned char& h, unsigned char& s, unsigned char& v); +};