From 3024507f542b340b60208330403dbde059bb7f33 Mon Sep 17 00:00:00 2001 From: juampe Date: Sat, 23 May 2026 20:40:31 +0200 Subject: [PATCH] Add support for the Holtek A09F RGB mouse, including drivers and lighting modes. --- .../HoltekA09FController.cpp | 386 ++++++++++++++++++ .../HoltekA09FController.h | 135 ++++++ .../RGBController_HoltekA09F.cpp | 221 ++++++++++ .../RGBController_HoltekA09F.h | 35 ++ .../HoltekControllerDetect.cpp | 32 +- 5 files changed, 807 insertions(+), 2 deletions(-) create mode 100644 Controllers/HoltekController/HoltekA09FController/HoltekA09FController.cpp create mode 100644 Controllers/HoltekController/HoltekA09FController/HoltekA09FController.h create mode 100644 Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.cpp create mode 100644 Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.h diff --git a/Controllers/HoltekController/HoltekA09FController/HoltekA09FController.cpp b/Controllers/HoltekController/HoltekA09FController/HoltekA09FController.cpp new file mode 100644 index 000000000..211208d16 --- /dev/null +++ b/Controllers/HoltekController/HoltekA09FController/HoltekA09FController.cpp @@ -0,0 +1,386 @@ +/*---------------------------------------------------------*\ +| HoltekA09FController.cpp | +| | +| Driver for Holtek A09F RGB gaming mouse | +| (E-Signal LUOM G10, VID 04D9 PID A09F) | +| | +| Protocol reverse-engineered from USB captures | +| Juampe 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include +#include +#include +#include +#include +#include "HoltekA09FController.h" +#include "StringUtils.h" + +/*-----------------------------------------------------------------*\ +| TX0 common selectors (positions C1-C6 and C8, 8 bytes each). | +| The 7th position (index 6) is the variable mode selector. | +\*-----------------------------------------------------------------*/ +static const uint8_t TX0_SEL_COMMON[7][8] = +{ + { 0x27,0x27,0xD5,0xFF,0xEC,0xE5,0x7E,0x76 }, /* C1 */ + { 0x25,0x2D,0xAD,0xFF,0xE8,0xEA,0xEE,0xEE }, /* C2 */ + { 0x27,0x2B,0xD5,0xFF,0xF0,0xED,0x7E,0x76 }, /* C3 */ + { 0x27,0x2B,0xDD,0xFF,0xF0,0xD5,0x7E,0x76 }, /* C4 */ + { 0x27,0x2B,0x75,0xFF,0xC8,0x25,0x7E,0x76 }, /* C5 */ + { 0x27,0x27,0xD5,0x00,0xD8,0x40,0xB1,0xAE }, /* C6 */ + { 0x27,0x2A,0x8D,0xFF,0xE8,0x5D,0x7E,0x36 }, /* C8 */ +}; + +/*-----------------------------------------------------------------*\ +| Per-transaction block selectors (TX1-TX5). | +\*-----------------------------------------------------------------*/ +static const uint8_t SEL_B1[8] = { 0x27,0x2A,0x85,0xFF,0xF0,0x5D,0x7E,0x36 }; +static const uint8_t SEL_B2[8] = { 0x27,0x2B,0xF5,0xFF,0xD8,0x55,0x7E,0xB6 }; +static const uint8_t SEL_B3[8] = { 0x27,0x2D,0x55,0xFF,0xF0,0x6D,0x80,0x76 }; +static const uint8_t SEL_B5[8] = { 0x27,0x2D,0x2D,0xFF,0xF8,0x6D,0x80,0x76 }; +static const uint8_t SEL_B7[8] = { 0x27,0x2B,0xF5,0xFF,0x00,0x5D,0x7E,0xD6 }; + +/*-----------------------------------------------------------------*\ +| Fixed data blocks (from captures, unchanged across all modes). | +\*-----------------------------------------------------------------*/ +static const uint8_t BLK_B0[32] = +{ + 0xFF,0x00,0x00,0x00,0xFF,0x00,0x00,0x00, + 0xFF,0xFF,0xFF,0xFF,0x00,0xFF,0x00,0xFF, + 0x00,0xFF,0xFF,0xFF,0xFF,0x80,0x00,0xFF, + 0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t BLK_B2[32] = +{ + 0x08,0x10,0x18,0x30,0x60,0x7E,0x7E,0x78, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; +static const uint8_t BLK_B3[32] = +{ + 0x01,0x00,0xF0,0x00,0x01,0x00,0xF1,0x00, + 0x01,0x00,0xF2,0x00,0x01,0x00,0xF4,0x00, + 0x01,0x00,0xF3,0x00,0x07,0x00,0x01,0x00, + 0x07,0x00,0x02,0x00,0x0B,0x00,0x00,0x00 +}; +static const uint8_t BLK_B4[32] = +{ + 0x0B,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x04,0x00,0x01,0x00,0x04,0x00,0x02,0x00 +}; +static const uint8_t BLK_B5[32] = +{ + 0x0C,0x00,0x00,0x00,0x01,0x00,0xF1,0x00, + 0x01,0x00,0xF2,0x00,0x01,0x00,0xF4,0x00, + 0x01,0x00,0xF3,0x00,0x07,0x00,0x01,0x00, + 0x07,0x00,0x02,0x00,0x0B,0x00,0x00,0x00 +}; +static const uint8_t BLK_B7[32] = +{ + 0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; + +/*-----------------------------------------------------------------*\ +| Default B1 (Standard mode, default DPI stage colours). | +\*-----------------------------------------------------------------*/ +static const uint8_t DEFAULT_B1[32] = +{ + 0xFF,0x00,0x00,0xFF,0x80,0x00,0xFF,0xFF, + 0x00,0x66,0x67,0xAA,0x00,0xFF,0x00,0x00, + 0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x55, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 +}; + +/*-----------------------------------------------------------------*\ +| Mode selector table (indexed by HOLTEK_A09F_MODE_VALUE_*) | +\*-----------------------------------------------------------------*/ +static const uint8_t MODE_SELECTORS[12][8] = +{ + HOLTEK_A09F_MODE_OFF, + HOLTEK_A09F_MODE_STANDARD, + HOLTEK_A09F_MODE_BREATHING, + HOLTEK_A09F_MODE_NEON, + HOLTEK_A09F_MODE_WAVE, + HOLTEK_A09F_MODE_KEY_REACTION, + HOLTEK_A09F_MODE_TRAILING, + HOLTEK_A09F_MODE_DRAG, + HOLTEK_A09F_MODE_SLIDE, + HOLTEK_A09F_MODE_YO_YO, + HOLTEK_A09F_MODE_MARBLES, + HOLTEK_A09F_MODE_FLYING_STAR, +}; + +/*-----------------------------------------------------------------*\ +| Sysfs helper: resolve USB bus/device numbers from a hidraw path. | +\*-----------------------------------------------------------------*/ +static bool GetUSBBusDevice(const std::string& hidraw_path, uint8_t& bus_out, uint8_t& dev_out) +{ + std::string devname = hidraw_path.substr(hidraw_path.rfind('/') + 1); + std::string sysfs = "/sys/class/hidraw/" + devname + "/device"; + + char real_path[PATH_MAX]; + if(!realpath(sysfs.c_str(), real_path)) + return false; + + /* The USB device dir is two levels above the HID device dir */ + std::string usb_base = std::string(real_path) + "/../../"; + + auto read_int = [](const std::string& p, int& v) -> bool + { + FILE* f = fopen(p.c_str(), "r"); + if(!f) return false; + bool ok = (fscanf(f, "%d", &v) == 1); + fclose(f); + return ok; + }; + + int bus = 0, devnum = 0; + if(!read_int(usb_base + "busnum", bus)) return false; + if(!read_int(usb_base + "devnum", devnum)) return false; + + bus_out = static_cast(bus); + dev_out = static_cast(devnum); + return true; +} + +/*=================================================================*\ +| Constructor / destructor | +\*=================================================================*/ + +HoltekA09FController::HoltekA09FController(hid_device* dev_handle, + const char* path, + std::string dev_name) +{ + dev = dev_handle; + location = path; + name = dev_name; + usb_ctx = nullptr; + usb_handle = nullptr; + ready = false; + lights_off = false; + + memcpy(b1, DEFAULT_B1, 32); + memcpy(current_mode_sel, MODE_SELECTORS[HOLTEK_A09F_MODE_VALUE_STANDARD], 8); + + /*-------------------------------------------------------------*\ + | Open the device via libusb, detach any kernel driver from | + | interface 2, and claim it for exclusive use. | + \*-------------------------------------------------------------*/ + libusb_init(&usb_ctx); + + uint8_t bus_num = 0, dev_num = 0; + if(GetUSBBusDevice(path, bus_num, dev_num)) + { + libusb_device** list = nullptr; + ssize_t count = libusb_get_device_list(usb_ctx, &list); + for(ssize_t i = 0; i < count; i++) + { + if(libusb_get_bus_number(list[i]) == bus_num && + libusb_get_device_address(list[i]) == dev_num) + { + libusb_open(list[i], &usb_handle); + break; + } + } + libusb_free_device_list(list, 1); + } + + if(usb_handle) + { + if(libusb_kernel_driver_active(usb_handle, 2) == 1) + libusb_detach_kernel_driver(usb_handle, 2); + + int rc = libusb_claim_interface(usb_handle, 2); + if(rc == LIBUSB_SUCCESS) + { + ready = true; + } + else + { + /* Interface already claimed by a previous instance (duplicate HID + * collection entry for the same physical device). Close and bail. */ + libusb_close(usb_handle); + usb_handle = nullptr; + } + } +} + +HoltekA09FController::~HoltekA09FController() +{ + if(usb_handle) + { + libusb_release_interface(usb_handle, 2); + libusb_attach_kernel_driver(usb_handle, 2); + libusb_close(usb_handle); + } + if(usb_ctx) + libusb_exit(usb_ctx); + + hid_close(dev); +} + +/*=================================================================*\ +| Accessors | +\*=================================================================*/ + +std::string HoltekA09FController::GetDeviceLocation() +{ + return "HID: " + location; +} + +std::string HoltekA09FController::GetNameString() +{ + return name; +} + +std::string HoltekA09FController::GetSerialString() +{ + wchar_t serial_string[128]; + if(hid_get_serial_number_string(dev, serial_string, 128) != 0) + return ""; + return StringUtils::wstring_to_string(serial_string); +} + +/*=================================================================*\ +| Private helpers | +\*=================================================================*/ + +void HoltekA09FController::SendSelector(const uint8_t sel[8]) +{ + if(!usb_handle) return; + + uint8_t buf[8]; + memcpy(buf, sel, 8); + + /* bmRequestType = 0x21 (Host->Device, Class, Interface) *\ + | bRequest = 0x09 (SET_REPORT) | + | wValue = 0x0300 (Feature report, ID=0) | + | wIndex = 2 (interface 2) | + | wLength = 8 | + \*-------------------------------------------------------------*/ + libusb_control_transfer(usb_handle, + 0x21, 0x09, 0x0300, 2, + buf, 8, + 2000); +} + +void HoltekA09FController::SendBlock(const uint8_t data[32]) +{ + if(!usb_handle) return; + + int transferred = 0; + libusb_interrupt_transfer(usb_handle, 0x03, + const_cast(data), 32, + &transferred, 2000); +} + +void HoltekA09FController::SendApplySequence(const uint8_t mode_sel[8], + const uint8_t b1_data[32]) +{ + /* TX0: common C1-C6 + mode selector + common C8 → B0 */ + for(int i = 0; i < 6; i++) + SendSelector(TX0_SEL_COMMON[i]); + SendSelector(mode_sel); + SendSelector(TX0_SEL_COMMON[6]); + SendBlock(BLK_B0); + + /* TX1: SEL_B1 → B1 (DPI colour table) */ + SendSelector(SEL_B1); + SendBlock(b1_data); + + /* TX2: SEL_B2 → B2 */ + SendSelector(SEL_B2); + SendBlock(BLK_B2); + + /* TX3: SEL_B3 → B3 + B4 */ + SendSelector(SEL_B3); + SendBlock(BLK_B3); + SendBlock(BLK_B4); + + /* TX4: SEL_B5 → B5 + B4 */ + SendSelector(SEL_B5); + SendBlock(BLK_B5); + SendBlock(BLK_B4); + + /* TX5: SEL_B7 → B7 */ + SendSelector(SEL_B7); + SendBlock(BLK_B7); +} + +/*=================================================================*\ +| Public API | +\*=================================================================*/ + +void HoltekA09FController::SetMode(uint8_t mode_value) +{ + if(mode_value >= 12) + return; + + if(mode_value == HOLTEK_A09F_MODE_VALUE_OFF) + { + uint8_t off_b1[32] = { 0x00 }; + + lights_off = true; + SendApplySequence(MODE_SELECTORS[HOLTEK_A09F_MODE_VALUE_STANDARD], off_b1); + return; + } + + lights_off = false; + memcpy(current_mode_sel, MODE_SELECTORS[mode_value], 8); + SendApplySequence(current_mode_sel, b1); +} + +void HoltekA09FController::SetDPIColors(const RGBColor* colors, unsigned int dpi_stages) +{ + /*-------------------------------------------------------------*\ + | B1 layout (32 bytes): | + | bytes 0- 2 = RGB stage 1 | + | bytes 3- 5 = RGB stage 2 | + | bytes 6- 8 = RGB stage 3 | + | bytes 9-11 = RGB stage 4 | + | bytes 12-14 = RGB stage 5 | + | bytes 15-17 = RGB stage 6 | + | bytes 18-20 = RGB stage 7 | + | bytes 21-23 = RGB stage 8 | + | bytes 24-31 = 0x00 (stable) | + \*-------------------------------------------------------------*/ + if(dpi_stages > HOLTEK_A09F_DPI_STAGES) + dpi_stages = HOLTEK_A09F_DPI_STAGES; + + for(unsigned int i = 0; i < HOLTEK_A09F_DPI_STAGES; i++) + { + unsigned int base = i * 3; + if(i < dpi_stages) + { + b1[base + 0] = RGBGetRValue(colors[i]); + b1[base + 1] = RGBGetGValue(colors[i]); + b1[base + 2] = RGBGetBValue(colors[i]); + } + else + { + b1[base + 0] = 0xFF; + b1[base + 1] = 0xFF; + b1[base + 2] = 0xFF; + } + } + memset(b1 + 24, 0x00, 8); + + if(lights_off) + { + uint8_t off_b1[32] = { 0x00 }; + SendApplySequence(MODE_SELECTORS[HOLTEK_A09F_MODE_VALUE_STANDARD], off_b1); + return; + } + + SendApplySequence(current_mode_sel, b1); +} + diff --git a/Controllers/HoltekController/HoltekA09FController/HoltekA09FController.h b/Controllers/HoltekController/HoltekA09FController/HoltekA09FController.h new file mode 100644 index 000000000..6237753bc --- /dev/null +++ b/Controllers/HoltekController/HoltekA09FController/HoltekA09FController.h @@ -0,0 +1,135 @@ +/*---------------------------------------------------------*\ +| HoltekA09FController.h | +| | +| Driver for Holtek A09F RGB gaming mouse | +| (E-Signal LUOM G10, VID 04D9 PID A09F) | +| | +| Protocol reverse-engineered from USB captures | +| Juampe 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include +#include +#include +#include "RGBController.h" + +/*-----------------------------------------------------------------*\ +| Protocol overview | +| | +| The device uses 6 independent transactions per configuration | +| change, each preceded by its own EP0 selector(s): | +| | +| TX0: 6 common SELs + mode SEL + common SEL8 → B0 | +| TX1: SEL_B1 → B1 (DPI colors) | +| TX2: SEL_B2 → B2 | +| TX3: SEL_B3 → B3 + B4 | +| TX4: SEL_B5 → B5 + B4 | +| TX5: SEL_B7 → B7 | +| | +| Selectors: HID SET_REPORT Feature via EP0 | +| bmRequestType=0x21 bRequest=0x09 | +| wValue=0x0300 wIndex=2 wLength=8 | +| | +| Data blocks: libusb_interrupt_transfer EP 0x03, 32 bytes each. | +\*-----------------------------------------------------------------*/ + +#define HOLTEK_A09F_SELECTOR_SIZE 8 +#define HOLTEK_A09F_DPI_STAGES 8 + +/*-----------------------------------------------------------------*\ +| Mode selector bytes (8 bytes, position 7 of TX0) | +\*-----------------------------------------------------------------*/ +#define HOLTEK_A09F_MODE_OFF { 0x27,0x2B,0x65,0xFF,0xE8,0x35,0x7E,0x76 } +#define HOLTEK_A09F_MODE_STANDARD { 0x27,0x2B,0x65,0xFF,0xF0,0x35,0x85,0x6E } +#define HOLTEK_A09F_MODE_BREATHING { 0x27,0x2B,0x5D,0xFF,0xF8,0x35,0x7E,0x8E } +#define HOLTEK_A09F_MODE_NEON { 0x27,0x2B,0x5D,0xFF,0x00,0x3A,0x46,0x8E } +#define HOLTEK_A09F_MODE_WAVE { 0x27,0x2B,0x55,0xFF,0xC8,0x3A,0x46,0x8E } +#define HOLTEK_A09F_MODE_KEY_REACTION { 0x27,0x2B,0x45,0xFF,0xD0,0x35,0x85,0x6E } +#define HOLTEK_A09F_MODE_TRAILING { 0x27,0x2B,0x25,0xFF,0xD8,0x35,0x7E,0x86 } +#define HOLTEK_A09F_MODE_DRAG { 0x27,0x2B,0x3D,0xFF,0xE0,0x35,0x7E,0x86 } +#define HOLTEK_A09F_MODE_SLIDE { 0x27,0x2B,0x2D,0xFF,0x28,0x35,0x85,0x6E } +#define HOLTEK_A09F_MODE_YO_YO { 0x27,0x2A,0xA5,0xFF,0x30,0x35,0x05,0x6E } +#define HOLTEK_A09F_MODE_MARBLES { 0x27,0x2B,0x35,0xFF,0x38,0x35,0x7D,0x6E } +#define HOLTEK_A09F_MODE_FLYING_STAR { 0x27,0x2B,0x35,0xFF,0x40,0x35,0x85,0x6E } + +enum +{ + HOLTEK_A09F_MODE_VALUE_OFF = 0x00, + HOLTEK_A09F_MODE_VALUE_STANDARD = 0x01, + HOLTEK_A09F_MODE_VALUE_BREATHING = 0x02, + HOLTEK_A09F_MODE_VALUE_NEON = 0x03, + HOLTEK_A09F_MODE_VALUE_WAVE = 0x04, + HOLTEK_A09F_MODE_VALUE_KEY_REACTION = 0x05, + HOLTEK_A09F_MODE_VALUE_TRAILING = 0x06, + HOLTEK_A09F_MODE_VALUE_DRAG = 0x07, + HOLTEK_A09F_MODE_VALUE_SLIDE = 0x08, + HOLTEK_A09F_MODE_VALUE_YO_YO = 0x09, + HOLTEK_A09F_MODE_VALUE_MARBLES = 0x0A, + HOLTEK_A09F_MODE_VALUE_FLYING_STAR = 0x0B, +}; + +class HoltekA09FController +{ +public: + HoltekA09FController(hid_device* dev_handle, const char* path, std::string dev_name); + ~HoltekA09FController(); + + std::string GetDeviceLocation(); + std::string GetNameString(); + std::string GetSerialString(); + + /*-------------------------------------------------------------*\ + | Set illumination mode. colors holds the current DPI stage | + | colours; it will be embedded in B1 of the apply sequence. | + \*-------------------------------------------------------------*/ + void SetMode(uint8_t mode_value); + + /*-------------------------------------------------------------*\ + | Update the DPI stage colours without changing the mode. | + | colors must contain at least 1 entry; entries beyond | + | dpi_stages are filled with white. | + \*-------------------------------------------------------------*/ + void SetDPIColors(const RGBColor* colors, unsigned int dpi_stages); + + /*-------------------------------------------------------------*\ + | Returns false if libusb could not claim interface 2 | + | (e.g. duplicate detection of the same physical device). | + \*-------------------------------------------------------------*/ + bool IsReady() const { return ready; } + +private: + hid_device* dev; /* used only for serial-number query */ + libusb_context* usb_ctx; + libusb_device_handle* usb_handle; /* full-device handle, interface 2 */ + std::string location; + std::string name; + + bool ready; /* true if libusb claim succeeded */ + bool lights_off; /* off is emulated as static black */ + uint8_t b1[32]; /* cached DPI colour block */ + uint8_t current_mode_sel[8]; /* cached mode selector for TX0 */ + + /*-------------------------------------------------------------*\ + | Send one 8-byte selector via EP0 ctrl_transfer to iface 2. | + \*-------------------------------------------------------------*/ + void SendSelector(const uint8_t sel[8]); + + /*-------------------------------------------------------------*\ + | Send the full 6-transaction apply sequence. | + | mode_sel: 8-byte mode selector (TX0 position 7). | + | b1_data: 32-byte DPI colour block (TX1). | + \*-------------------------------------------------------------*/ + void SendApplySequence(const uint8_t mode_sel[8], const uint8_t b1_data[32]); + + /*-------------------------------------------------------------*\ + | Send a 32-byte interrupt OUT block to EP 0x03. | + \*-------------------------------------------------------------*/ + void SendBlock(const uint8_t data[32]); +}; + diff --git a/Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.cpp b/Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.cpp new file mode 100644 index 000000000..733783caf --- /dev/null +++ b/Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.cpp @@ -0,0 +1,221 @@ +/*---------------------------------------------------------*\ +| RGBController_HoltekA09F.cpp | +| | +| RGBController for Holtek A09F RGB gaming mouse | +| (E-Signal LUOM G10, VID 04D9 PID A09F) | +| | +| Protocol documented in reverse-engineering notes. | +| Supported features: | +| - 12 lighting modes (Off, Standard, Breathing, etc.) | +| - DPI stage colours (8 stages × RGB) | +| | +| Juampe 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#include "RGBController_HoltekA09F.h" + +/**------------------------------------------------------------------*\ + @name Holtek A09F + @category Mouse + @type USB + @save :white_check_mark: + @direct :x: + @effects :white_check_mark: + @detectors DetectHoltekA09FControllers + @comment Phoenix Void RGB gaming mouse (Holtek A09F chip, VID 04D9 PID A09F). + Supports 12 lighting modes and 8 DPI stage colours. + Lighting changes are saved automatically by the device. +\*-------------------------------------------------------------------*/ + +RGBController_HoltekA09F::RGBController_HoltekA09F(HoltekA09FController* controller_ptr) +{ + controller = controller_ptr; + + name = controller->GetNameString(); + vendor = "Holtek"; + type = DEVICE_TYPE_MOUSE; + description = "Phoenix Void RGB Gaming Mouse"; + location = controller->GetDeviceLocation(); + serial = controller->GetSerialString(); + + /*-----------------------------------------------------------------*\ + | Off mode | + \*-----------------------------------------------------------------*/ + mode off; + off.name = "Off"; + off.value = HOLTEK_A09F_MODE_VALUE_OFF; + off.flags = MODE_FLAG_AUTOMATIC_SAVE; + off.color_mode = MODE_COLORS_NONE; + modes.push_back(off); + + /*-----------------------------------------------------------------*\ + | Standard (static colour — per DPI stage) | + \*-----------------------------------------------------------------*/ + mode static_mode; + static_mode.name = "Static"; + static_mode.value = HOLTEK_A09F_MODE_VALUE_STANDARD; + static_mode.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + static_mode.color_mode = MODE_COLORS_PER_LED; + modes.push_back(static_mode); + + /*-----------------------------------------------------------------*\ + | Breathing | + \*-----------------------------------------------------------------*/ + mode breathing; + breathing.name = "Breathing"; + breathing.value = HOLTEK_A09F_MODE_VALUE_BREATHING; + breathing.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + breathing.color_mode = MODE_COLORS_PER_LED; + modes.push_back(breathing); + + /*-----------------------------------------------------------------*\ + | Neon | + \*-----------------------------------------------------------------*/ + mode neon; + neon.name = "Neon"; + neon.value = HOLTEK_A09F_MODE_VALUE_NEON; + neon.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + neon.color_mode = MODE_COLORS_PER_LED; + modes.push_back(neon); + + /*-----------------------------------------------------------------*\ + | Wave | + \*-----------------------------------------------------------------*/ + mode wave; + wave.name = "Wave"; + wave.value = HOLTEK_A09F_MODE_VALUE_WAVE; + wave.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + wave.color_mode = MODE_COLORS_PER_LED; + modes.push_back(wave); + + /*-----------------------------------------------------------------*\ + | Key Reaction | + \*-----------------------------------------------------------------*/ + mode key_reaction; + key_reaction.name = "Key Reaction"; + key_reaction.value = HOLTEK_A09F_MODE_VALUE_KEY_REACTION; + key_reaction.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + key_reaction.color_mode = MODE_COLORS_PER_LED; + modes.push_back(key_reaction); + + /*-----------------------------------------------------------------*\ + | Trailing | + \*-----------------------------------------------------------------*/ + mode trailing; + trailing.name = "Trailing"; + trailing.value = HOLTEK_A09F_MODE_VALUE_TRAILING; + trailing.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + trailing.color_mode = MODE_COLORS_PER_LED; + modes.push_back(trailing); + + /*-----------------------------------------------------------------*\ + | Drag | + \*-----------------------------------------------------------------*/ + mode drag; + drag.name = "Drag"; + drag.value = HOLTEK_A09F_MODE_VALUE_DRAG; + drag.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + drag.color_mode = MODE_COLORS_PER_LED; + modes.push_back(drag); + + /*-----------------------------------------------------------------*\ + | Slide | + \*-----------------------------------------------------------------*/ + mode slide; + slide.name = "Slide"; + slide.value = HOLTEK_A09F_MODE_VALUE_SLIDE; + slide.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + slide.color_mode = MODE_COLORS_PER_LED; + modes.push_back(slide); + + /*-----------------------------------------------------------------*\ + | Yo-Yo | + \*-----------------------------------------------------------------*/ + mode yoyo; + yoyo.name = "Yo-Yo"; + yoyo.value = HOLTEK_A09F_MODE_VALUE_YO_YO; + yoyo.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + yoyo.color_mode = MODE_COLORS_PER_LED; + modes.push_back(yoyo); + + /*-----------------------------------------------------------------*\ + | Marbles | + \*-----------------------------------------------------------------*/ + mode marbles; + marbles.name = "Marbles"; + marbles.value = HOLTEK_A09F_MODE_VALUE_MARBLES; + marbles.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + marbles.color_mode = MODE_COLORS_PER_LED; + modes.push_back(marbles); + + /*-----------------------------------------------------------------*\ + | Flying Star | + \*-----------------------------------------------------------------*/ + mode flying_star; + flying_star.name = "Flying Star"; + flying_star.value = HOLTEK_A09F_MODE_VALUE_FLYING_STAR; + flying_star.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_AUTOMATIC_SAVE; + flying_star.color_mode = MODE_COLORS_PER_LED; + modes.push_back(flying_star); + + SetupZones(); +} + +RGBController_HoltekA09F::~RGBController_HoltekA09F() +{ + delete controller; +} + +void RGBController_HoltekA09F::SetupZones() +{ + /*-----------------------------------------------------------------*\ + | One zone with 8 LEDs — one per DPI stage | + \*-----------------------------------------------------------------*/ + zone dpi_zone; + dpi_zone.name = "DPI Stages"; + dpi_zone.type = ZONE_TYPE_LINEAR; + dpi_zone.leds_min = HOLTEK_A09F_DPI_STAGES; + dpi_zone.leds_max = HOLTEK_A09F_DPI_STAGES; + dpi_zone.leds_count = HOLTEK_A09F_DPI_STAGES; + dpi_zone.matrix_map = NULL; + zones.push_back(dpi_zone); + + for(unsigned int i = 0; i < HOLTEK_A09F_DPI_STAGES; i++) + { + led stage_led; + stage_led.name = "DPI Stage " + std::to_string(i + 1); + leds.push_back(stage_led); + } + + SetupColors(); +} + +void RGBController_HoltekA09F::ResizeZone(int /*zone*/, int /*new_size*/) +{ + /*---------------------------------------------------------*\ + | This device does not support resizing zones | + \*---------------------------------------------------------*/ +} + +void RGBController_HoltekA09F::DeviceUpdateLEDs() +{ + controller->SetDPIColors(colors.data(), (unsigned int)colors.size()); +} + +void RGBController_HoltekA09F::UpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_HoltekA09F::UpdateSingleLED(int /*led*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_HoltekA09F::DeviceUpdateMode() +{ + controller->SetMode(modes[active_mode].value); +} diff --git a/Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.h b/Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.h new file mode 100644 index 000000000..1eb9e368c --- /dev/null +++ b/Controllers/HoltekController/HoltekA09FController/RGBController_HoltekA09F.h @@ -0,0 +1,35 @@ +/*---------------------------------------------------------*\ +| RGBController_HoltekA09F.h | +| | +| RGBController for Holtek A09F RGB gaming mouse | +| (E-Signal LUOM G10, VID 04D9 PID A09F) | +| | +| Juampe 2026 | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-or-later | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "HoltekA09FController.h" + +class RGBController_HoltekA09F : public RGBController +{ +public: + RGBController_HoltekA09F(HoltekA09FController* controller_ptr); + ~RGBController_HoltekA09F(); + + void SetupZones(); + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); + +private: + HoltekA09FController* controller; +}; diff --git a/Controllers/HoltekController/HoltekControllerDetect.cpp b/Controllers/HoltekController/HoltekControllerDetect.cpp index e8471ecd7..8006f1cf5 100644 --- a/Controllers/HoltekController/HoltekControllerDetect.cpp +++ b/Controllers/HoltekController/HoltekControllerDetect.cpp @@ -13,6 +13,8 @@ #include "RGBController_HoltekA070.h" #include "HoltekA1FAController.h" #include "RGBController_HoltekA1FA.h" +#include "HoltekA09FController.h" +#include "RGBController_HoltekA09F.h" /*-----------------------------------------------------*\ | Holtek Semiconductor Inc. vendor ID | @@ -23,6 +25,10 @@ \*-----------------------------------------------------*/ #define HOLTEK_A070_PID 0xA070 /*-----------------------------------------------------*\ +| Phoenix Void product ID | +\*-----------------------------------------------------*/ +#define HOLTEK_A09F_PID 0xA09F +/*-----------------------------------------------------*\ | Mousemats product IDs | \*-----------------------------------------------------*/ #define HOLTEK_A1FA_PID 0xA1FA @@ -53,5 +59,27 @@ void DetectHoltekMousemats(hid_device_info *info, const std::string &name) } } /* DetectHoltekMousemats() */ -REGISTER_HID_DETECTOR_IPU("Holtek USB Gaming Mouse", DetectHoltekControllers, HOLTEK_VID, HOLTEK_A070_PID, 1, 0xFF00, 2); -REGISTER_HID_DETECTOR_IPU("Holtek Mousemat", DetectHoltekMousemats, HOLTEK_VID, HOLTEK_A1FA_PID, 2, 0xFF00, 0xFF00); +void DetectHoltekA09FControllers(hid_device_info* info, const std::string& name) +{ + hid_device* dev = hid_open_path(info->path); + + if(dev) + { + HoltekA09FController* controller = new HoltekA09FController(dev, info->path, name); + + if(controller->IsReady()) + { + RGBController_HoltekA09F* rgb_controller = new RGBController_HoltekA09F(controller); + ResourceManager::get()->RegisterRGBController(rgb_controller); + } + else + { + /* Duplicate HID collection entry — physical device already registered */ + delete controller; + } + } +} /* DetectHoltekA09FControllers() */ + +REGISTER_HID_DETECTOR_IPU("Holtek USB Gaming Mouse", DetectHoltekControllers, HOLTEK_VID, HOLTEK_A070_PID, 1, 0xFF00, 2); +REGISTER_HID_DETECTOR_IPU("Holtek Mousemat", DetectHoltekMousemats, HOLTEK_VID, HOLTEK_A1FA_PID, 2, 0xFF00, 0xFF00); +REGISTER_HID_DETECTOR_I("Phoenix Void", DetectHoltekA09FControllers, HOLTEK_VID, HOLTEK_A09F_PID, 1);