Initial commit for HyperX Quadcast S

This commit is contained in:
Matt Silva 2023-01-17 05:41:17 +00:00 committed by Adam Honse
parent 856fbc1bb2
commit 72fa099088
6 changed files with 682 additions and 0 deletions

View file

@ -0,0 +1,210 @@
/*-----------------------------------------*\
| HyperXQuadcastSController.cpp |
| |
| Implementation for the HyperX |
| Quadcast S RGB microphone |
| |
| Matt Silva (thesilvanator) 2022 |
\*-----------------------------------------*/
#include "HyperXQuadcastSController.h"
HyperXQuadcastSController::HyperXQuadcastSController(hid_device* dev_handle, HXQS_HIDAPI_WRAPPER wrapper, std::string path)
{
hidapi_wrapper = wrapper;
dev = dev_handle;
location = path;
wchar_t serial_string[128];
int ret = wrapper.get_serial_num_string(dev, serial_string, 128);
if(ret != 0)
{
serial_number = "";
}
else
{
std::wstring return_wstring = serial_string;
serial_number = std::string(return_wstring.begin(), return_wstring.end());
}
}
HyperXQuadcastSController::~HyperXQuadcastSController()
{
if(dev)
{
hidapi_wrapper.close(dev);
}
}
std::string HyperXQuadcastSController::GetDeviceLocation()
{
return location;
}
std::string HyperXQuadcastSController::GetSerialString()
{
return serial_number;
}
void HyperXQuadcastSController::SaveColors(std::vector<RGBColor> colors, unsigned int num_frames)
{
using namespace std::chrono_literals;
int res;
unsigned int num_color_packets = 0;
unsigned int frame = 0;
unsigned char color[HXQS_PACKET_SIZE] = {0};
unsigned char empty[HXQS_PACKET_SIZE] = {0};
num_color_packets = num_frames/8;
if(num_frames % 8)
{
num_color_packets++;
}
lock.lock();
/*---------------------------------------------------------*\
| Start Save Transaction |
| 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 |
\*---------------------------------------------------------*/
SendToRegister(0x53, (uint8_t)num_color_packets, 0);
while(frame < num_frames)
{
memset(color, 0, HXQS_PACKET_SIZE);
unsigned int i = 0;
while(i < 8 && frame < num_frames)
{
int index = HXQS_FRAME_SIZE * i;
RGBColor top = colors[frame*2];
RGBColor bot = colors[frame*2 + 1];
color[index + 1] = 0x81;
color[index + 2] = RGBGetRValue(top);
color[index + 3] = RGBGetGValue(top);
color[index + 4] = RGBGetBValue(top);
color[index + 5] = 0x81;
color[index + 6] = RGBGetRValue(bot);
color[index + 7] = RGBGetGValue(bot);
color[index + 8] = RGBGetBValue(bot);
i++;
frame++;
}
std::this_thread::sleep_for(15ms);
res = hidapi_wrapper.send_feature_report(dev,color,HXQS_PACKET_SIZE);
}
/*---------------------------------------------------------*\
| Post Save Transaction |
| 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 |
\*---------------------------------------------------------*/
SendToRegister(0x02, 0, 0);
/*---------------------------------------------------------*\
| Stop Save Transaction |
| 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 |
\*---------------------------------------------------------*/
SendToRegister(0x23, 1, 0);
SendEOT((uint8_t)num_frames);
/*---------------------------------------------------------*\
| Post Save Transaction |
| 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 |
\*---------------------------------------------------------*/
SendToRegister(0x02, 0, 0);
lock.unlock();
// likes to have one temporary direct packet after the save for some reason
SendDirect(colors);
}
void HyperXQuadcastSController::SendDirect(std::vector<RGBColor> colors)
{
using namespace std::chrono_literals;
if(colors.size() != 2)
{
LOG_ERROR("[HyperX Quadcast S] Unable to send direct packet, incorrect size: %d", colors.size());
return;
}
int res;
RGBColor c1 = colors[0];
RGBColor c2 = colors[1];
uint8_t buffer[HXQS_PACKET_SIZE];
// colour packet
memset(buffer,0,HXQS_PACKET_SIZE);
//buffer[0x00] = 0; //buffer requires 0 at index 0 as device does not use ReportIDs
buffer[0x01] = 0x81;
buffer[0x02] = RGBGetRValue(c1);
buffer[0x03] = RGBGetGValue(c1);
buffer[0x04] = RGBGetBValue(c1);
buffer[0x05] = 0x81;
buffer[0x06] = RGBGetRValue(c2);
buffer[0x07] = RGBGetGValue(c2);
buffer[0x08] = RGBGetBValue(c2);
lock.lock();
res = hidapi_wrapper.send_feature_report(dev, buffer, HXQS_PACKET_SIZE);
std::this_thread::sleep_for(15ms);
SendToRegister(0xF2, 0, 1);
lock.unlock();
}
void HyperXQuadcastSController::SendEOT(uint8_t frame_count)
{
using namespace std::chrono_literals;
uint8_t buffer[HXQS_PACKET_SIZE];
memset(buffer,0,HXQS_PACKET_SIZE);
//buffer[0x00] = 0; //buffer requires 0 at index 0 as device does not use ReportIDs
buffer[0x01] = 0x08;
buffer[0x3C] = 0x28;
buffer[0x3D] = frame_count;
buffer[0x3E] = 0x00;
buffer[0x3F] = 0xAA;
buffer[0x40] = 0x55;
int result = hidapi_wrapper.send_feature_report(dev,buffer,HXQS_PACKET_SIZE);
LOG_DEBUG("[HyperX Quadcast S] SendEOT with frame count %02X wrote %d bytes", frame_count, result);
std::this_thread::sleep_for(15ms);
}
void HyperXQuadcastSController::SendToRegister(uint8_t reg, uint8_t param1, uint8_t param2)
{
using namespace std::chrono_literals;
uint8_t buffer[HXQS_PACKET_SIZE];
memset(buffer,0,HXQS_PACKET_SIZE);
//buffer[0x00] = 0; //buffer requires 0 at index 0 as device does not use ReportIDs
buffer[0x01] = 0x04;
buffer[0x02] = reg; // 0xF2 Apply, 0x53 Save
buffer[0x08] = param1;
buffer[0x09] = param2;
int result = hidapi_wrapper.send_feature_report(dev, buffer, HXQS_PACKET_SIZE);
if(result < 0)
{
LOG_ERROR("[HyperX Quadcast S] SendToRegister failed: %ls", hidapi_wrapper.error(dev));
}
else
{
LOG_DEBUG("[HyperX Quadcast S] SendToRegister %02X with P1 %02X P2 %02X wrote %d bytes", reg, param1, param2, result);
}
std::this_thread::sleep_for(15ms);
}

View file

@ -0,0 +1,66 @@
/*-----------------------------------------*\
| HyperXQuadcastSController.h |
| |
| Implementation for the HyperX |
| Quadcast S RGB microphone |
| |
| Matt Silva (thesilvanator) 2022 |
\*-----------------------------------------*/
#pragma once
#include <string>
#include <hidapi/hidapi.h>
#include "LogManager.h"
#include "RGBController.h"
#define HXQS_PACKET_SIZE 64 + 1
#define HXQS_FRAME_SIZE 8
// wrapper typedefs
typedef int (*HXQS_Report_Send_t)(hid_device*, const unsigned char*, size_t);
typedef int (*HXQS_Report_Get_t)(hid_device*, unsigned char*, size_t);
typedef int (*HXQS_Get_Serial_t)(hid_device*, wchar_t*, size_t);
typedef hid_device* (*HXQS_hid_open_path_t)(const char*);
typedef hid_device_info* (*HXQS_hid_enumerate_t) (unsigned short, unsigned short);
typedef void (*HXQS_hid_free_enumeration_t)(hid_device_info*);
typedef void (*HXQS_hid_close_t)(hid_device*);
typedef const wchar_t* (*HXQS_hid_error_t) (hid_device*);
/*----------------------------------------------------*\
| See comment at top of HyperXQuadcastSDetect.cpp for |
| details about the hidapi wrapper for this device |
\*----------------------------------------------------*/
struct HXQS_HIDAPI_WRAPPER {
void* dyn_handle;
HXQS_Report_Send_t send_feature_report;
HXQS_Report_Get_t get_feature_report;
HXQS_Get_Serial_t get_serial_num_string;
HXQS_hid_open_path_t open_path;
HXQS_hid_enumerate_t enumerate;
HXQS_hid_free_enumeration_t free_enumeration;
HXQS_hid_close_t close;
HXQS_hid_error_t error;
};
class HyperXQuadcastSController
{
public:
HyperXQuadcastSController(hid_device* dev, HXQS_HIDAPI_WRAPPER wrapper, std::string path);
~HyperXQuadcastSController();
std::string GetDeviceLocation();
std::string GetSerialString();
void SendDirect(std::vector<RGBColor> color_data);
void SaveColors(std::vector<RGBColor> colors, unsigned int num_frames);
private:
hid_device* dev;
std::string location;
std::string serial_number;
std::mutex lock;
HXQS_HIDAPI_WRAPPER hidapi_wrapper;
void SendEOT(uint8_t frame_count);
void SendToRegister(uint8_t reg, uint8_t param1, uint8_t param2);
};

View file

@ -0,0 +1,172 @@
/*-----------------------------------------*\
| HyperXQuadcastSControllerDetect.cpp |
| |
| Implementation for the HyperX |
| Quadcast S RGB microphone |
| |
| Matt Silva (thesilvanator) 2022 |
\*-----------------------------------------*/
#include "Detector.h"
#include "HyperXQuadcastSController.h"
#include "RGBController.h"
#include "RGBController_HyperXQuadcastS.h"
#include <vector>
#include <LogManager.h>
#ifdef __linux__
#include <dlfcn.h>
#endif
#include <hidapi/hidapi.h>
#define HYPERX_QS_VID_NA 0x0951
#define HYPERX_QS_PID_NA 0x171F
#define HYPERX_QS_VID_EU 0x03F0
#define HYPERX_QS_PID_EU 0x0F8B
const char* name = "HyperX Quadcast S";
#ifdef __linux__
void FindAndAddHyperXQuadcastSDevice(unsigned int vid, unsigned int pid,
HXQS_HIDAPI_WRAPPER wrapper,
std::vector<RGBController*>& rgb_controllers)
{
std::string path;
hid_device_info* devs = NULL;
hid_device *dev = NULL;
HyperXQuadcastSController* controller;
RGBController_HyperXQuadcastS* rgb_controller;
/*-----------------------------------------*\
| Iterate over devices with corresponding |
| VID and PID and check if there is one |
| with interface 0 as that is what we use |
\*-----------------------------------------*/
if(!(devs = wrapper.enumerate(vid, pid)))
{
LOG_DEBUG("[%s] Dynamic call to hid_enumerate failed or couldn't find a device (Linux only)", name);
return;
}
hid_device_info *curr = devs;
while(curr)
{
if(curr->interface_number == 0)
{
path = curr->path;
LOG_DEBUG("[%s] Found device with correct 0 interface (Linux only): %s", name, path.c_str());
break;
}
curr = curr->next;
}
wrapper.free_enumeration(devs);
if(!curr)
{
LOG_ERROR("[%s] Unable to find device with 0 interface (Linux only)", name);
return;
}
if(!(dev = wrapper.open_path(path.c_str())))
{
LOG_ERROR("[%s] Dynamic call to hid_open_path failed (Linux only)", name);
return;
}
LOG_DEBUG("[%s] Sucessfully opened device as a hidapi_device (Linux only)", name);
controller = new HyperXQuadcastSController(dev, wrapper, path);
rgb_controller = new RGBController_HyperXQuadcastS(controller);
rgb_controller->name = name;
rgb_controllers.push_back(rgb_controller);
}
void DetectHyperXQuadcastSControllers(std::vector<RGBController*>& rgb_controllers)
{
/*-----------------------------------------*\
| Dynamically load hidapi-libusb and setup |
| wrapper for Linux platforms |
\*-----------------------------------------*/
void* dyn_handle = NULL;
HXQS_HIDAPI_WRAPPER wrapper;
if(!(dyn_handle = dlopen("libhidapi-libusb.so", RTLD_NOW | RTLD_NODELETE | RTLD_DEEPBIND)))
{
LOG_ERROR("[%s] Couldn't dynamically load hidapi-libusb (Linux only): %s", name, dlerror());
return;
}
wrapper =
{
.dyn_handle = dyn_handle,
.send_feature_report = (HXQS_Report_Send_t) dlsym(dyn_handle,"hid_send_feature_report"),
.get_feature_report = (HXQS_Report_Get_t) dlsym(dyn_handle,"hid_get_feature_report"),
.get_serial_num_string = (HXQS_Get_Serial_t) dlsym(dyn_handle,"hid_get_serial_number_string"),
.open_path = (HXQS_hid_open_path_t) dlsym(dyn_handle,"hid_open_path"),
.enumerate = (HXQS_hid_enumerate_t) dlsym(dyn_handle,"hid_enumerate"),
.free_enumeration = (HXQS_hid_free_enumeration_t) dlsym(dyn_handle,"hid_free_enumeration"),
.close = (HXQS_hid_close_t) dlsym(dyn_handle,"hid_close"),
.error = (HXQS_hid_error_t) dlsym(dyn_handle,"hid_free_enumeration")
};
if(!(wrapper.send_feature_report && wrapper.get_feature_report &&
wrapper.open_path && wrapper.enumerate && wrapper.free_enumeration &&
wrapper.close && wrapper.error && wrapper.get_serial_num_string))
{
LOG_ERROR("[%s] Couldn't dynamically load one of hidapi-libusb functions for the wrapper (Linux only)", name);
return;
}
FindAndAddHyperXQuadcastSDevice(HYPERX_QS_VID_NA, HYPERX_QS_PID_NA, wrapper, rgb_controllers);
FindAndAddHyperXQuadcastSDevice(HYPERX_QS_VID_EU, HYPERX_QS_PID_EU, wrapper, rgb_controllers);
}
REGISTER_DETECTOR("HyperX Quadcast S", DetectHyperXQuadcastSControllers);
#else
void DetectHyperXQuadcastSControllers(hid_device_info* info, const std::string& name)
{
hid_device *dev = hid_open_path(info->path);
if(!dev)
{
LOG_ERROR("[%s] Unable to open device via hid_open_path(%s): %ls", name.c_str(), info->path, hid_error(dev));
return;
}
/*-----------------------------------------*\
| Setup wrapper for Windows platforms just |
| using the already linked in hidapi |
| functions |
\*-----------------------------------------*/
HXQS_HIDAPI_WRAPPER wrapper =
{
NULL, // dyn_handle
hid_send_feature_report,
hid_get_feature_report,
hid_get_serial_number_string,
hid_open_path,
hid_enumerate,
hid_free_enumeration,
hid_close,
hid_error
};
HyperXQuadcastSController* controller = new HyperXQuadcastSController(dev, wrapper, info->path);
RGBController_HyperXQuadcastS *rgb_controller = new RGBController_HyperXQuadcastS(controller);
rgb_controller->name = name;
ResourceManager::get()->RegisterRGBController(rgb_controller);
}
REGISTER_HID_DETECTOR_IPU("HyperX Quadcast S", DetectHyperXQuadcastSControllers, HYPERX_QS_VID_NA, HYPERX_QS_PID_NA, 0, 0xFF90, 0xFF00);
REGISTER_HID_DETECTOR_IPU("HyperX Quadcast S", DetectHyperXQuadcastSControllers, HYPERX_QS_VID_EU, HYPERX_QS_PID_EU, 0, 0xFF90, 0xFF00);
#endif

View file

@ -0,0 +1,181 @@
/*-----------------------------------------*\
| RGBController_HyperXQuadcastS.cpp |
| |
| Implementation for the HyperX |
| Quadcast S RGB microphone |
| |
| Matt Silva (thesilvanator) 2022 |
\*-----------------------------------------*/
/**------------------------------------------------------------------*\
@name HyperX Quadcast S
@type USB
@save :white_check_mark:
@direct :white_check_mark:
@effects :x:
@detectors DetectHyperXQuadcastSControllers
@comment The HyperX Quadcast S has a manufacturer issue
with the interface it uses (0) for controlling its RGB.
HID requires that any HID interface have at least one
Interrupt IN endpoint; however, the HXQS does not,
even though its interface reports itelf as hid and
responds to hid requests. As such Linux doesn't bind
to the usbhid driver and it goes undetected by
hidapi-hidraw. Windows does detect it as hid and hidapi
finds and interacts with it just fine. To work around
the Linux issue, hidapi-libusb is loaded dynamically
using dlopen/dlsym as hidapi using a libusb backend is
able to find the device and interact with it. This
requires that you have support for dlopen/dlsym on your
Linux platform as well as hidapi-libusb (and libusb)
libraries installed in the standard dynamic library
path.
The controller for this device has a wrapper for hidapi
functions so that the controller can be the same across
all platforms, but call the correct underlying functions
that are defined in the detector under an #ifdef
for that platform.
Additionally, hidapi-libusb has an error that causes
hid_close() to hang on this device, see:
https://github.com/libusb/hidapi/issues/456
This will be fixed on newer versions of hidapi-libusb,
but until then, OpenRGB will hang/crash if you try to
rescan devices once a HXQS has been detected during
program session.
\*-------------------------------------------------------------------*/
#include "RGBController_HyperXQuadcastS.h"
#include <LogManager.h>
using namespace std::chrono_literals;
RGBController_HyperXQuadcastS::RGBController_HyperXQuadcastS(HyperXQuadcastSController* controller_ptr)
{
controller = controller_ptr;
name = "HyperX QuadcastS Device";
vendor = "HyperX";
type = DEVICE_TYPE_MICROPHONE;
description = "HyperX QuadcastS Device";
location = controller->GetDeviceLocation();
serial = controller->GetSerialString();
mode Direct;
Direct.name = "Direct";
Direct.flags = MODE_FLAG_HAS_BRIGHTNESS | MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_MANUAL_SAVE;
Direct.color_mode = MODE_COLORS_PER_LED;
Direct.brightness_min = 0;
Direct.brightness_max = 100;
Direct.colors.push_back(ToRGBColor(0xFF,0,0)); // Top LED
Direct.colors.push_back(ToRGBColor(0xFF,0,0)); // Bot LED
modes.push_back(Direct);
mode Off;
Off.name = "Off";
Off.flags = MODE_FLAG_MANUAL_SAVE;
Off.color_mode = MODE_COLORS_NONE;
Off.colors.push_back(ToRGBColor(0,0,0)); // Top LED
Off.colors.push_back(ToRGBColor(0,0,0)); // Bot LED
modes.push_back(Off);
SetupZones();
keepalive_thread_run = 1;
keepalive_thread = new std::thread(&RGBController_HyperXQuadcastS::KeepaliveThread, this);
};
RGBController_HyperXQuadcastS::~RGBController_HyperXQuadcastS()
{
keepalive_thread_run = 0;
keepalive_thread->join();
delete keepalive_thread;
delete controller;
}
void RGBController_HyperXQuadcastS::SetupZones()
{
led* Top = new led();
led* Bot = new led();
Top->name = std::string("Top");
Top->value = 0;
Bot->name = std::string("Bottom");
Bot->value = 1;
leds.push_back(*Top);
leds.push_back(*Bot);
zone* Mic = new zone();
Mic->name = std::string("Microphone");
Mic->type = ZONE_TYPE_LINEAR;
Mic->leds_min = 2;
Mic->leds_max = 2;
Mic->leds_count = 2;
Mic->matrix_map = NULL;
zones.push_back(*Mic);
SetupColors();
// make starting colors be the default starting
// colors for direct
colors = modes[HXQS_MODE_DIRECT].colors;
}
void RGBController_HyperXQuadcastS::ResizeZone(int /*zone*/, int /*new_size*/)
{
}
void RGBController_HyperXQuadcastS::DeviceUpdateLEDs()
{
last_update_time = std::chrono::steady_clock::now();
controller->SendDirect(colors);
}
void RGBController_HyperXQuadcastS::UpdateZoneLEDs(int zone)
{
}
void RGBController_HyperXQuadcastS::UpdateSingleLED(int led)
{
}
void RGBController_HyperXQuadcastS::DeviceUpdateMode()
{
switch (active_mode)
{
case HXQS_MODE_DIRECT:
// make current colors this modes colors, in this case,
// the preserved direct colors from before switching off
colors = modes[active_mode].colors;
break;
case HXQS_MODE_OFF:
modes[HXQS_MODE_DIRECT].colors = colors; // preserve directs previous colours
colors = modes[active_mode].colors; // make current colors this modes colors
break;
default:
break;
}
UpdateLEDs();
}
void RGBController_HyperXQuadcastS::DeviceSaveMode()
{
LOG_DEBUG("[%s] Saving current direct colors to device", name.c_str());
controller->SaveColors(colors,1);
}
void RGBController_HyperXQuadcastS::KeepaliveThread()
{
while(keepalive_thread_run.load())
{
if((std::chrono::steady_clock::now() - last_update_time) > std::chrono::milliseconds(50))
{
UpdateLEDs();
}
std::this_thread::sleep_for(15ms);
}
}

View file

@ -0,0 +1,46 @@
/*-----------------------------------------*\
| RGBController_HyperXQuadcastS.h |
| |
| Implementation for the HyperX |
| Quadcast S RGB microphone |
| |
| Matt Silva (thesilvanator) 2022 |
\*-----------------------------------------*/
#pragma once
#include <chrono>
#include "RGBController.h"
#include "HyperXQuadcastSController.h"
enum
{
HXQS_MODE_DIRECT = 0,
HXQS_MODE_OFF = 1,
};
class RGBController_HyperXQuadcastS : public RGBController
{
public:
RGBController_HyperXQuadcastS(HyperXQuadcastSController* controller_ptr);
~RGBController_HyperXQuadcastS();
void SetupZones();
void ResizeZone(int zone, int new_size);
void DeviceUpdateLEDs();
void UpdateZoneLEDs(int zone);
void UpdateSingleLED(int led);
void DeviceUpdateMode();
void DeviceSaveMode();
void KeepaliveThread();
private:
HyperXQuadcastSController* controller;
std::thread* keepalive_thread;
std::atomic<bool> keepalive_thread_run;
std::chrono::time_point<std::chrono::steady_clock> last_update_time;
};

View file

@ -131,6 +131,7 @@ INCLUDEPATH +=
Controllers/HyperXKeyboardController/ \
Controllers/HyperXMouseController/ \
Controllers/HyperXMousematController/ \
Controllers/HyperXQuadcastController/ \
Controllers/IntelArcA770LEController/ \
Controllers/IonicoController/ \
Controllers/LEDStripController/ \
@ -457,6 +458,8 @@ HEADERS +=
Controllers/HyperXMouseController/RGBController_HyperXPulsefireRaid.h \
Controllers/HyperXMousematController/HyperXMousematController.h \
Controllers/HyperXMousematController/RGBController_HyperXMousemat.h \
Controllers/HyperXQuadcastController/HyperXQuadcastSController.h \
Controllers/HyperXQuadcastController/RGBController_HyperXQuadcastS.h \
Controllers/IntelArcA770LEController/IntelArcA770LEController.h \
Controllers/IntelArcA770LEController/RGBController_IntelArcA770LE.h \
Controllers/IonicoController/IonicoController.h \
@ -1045,6 +1048,9 @@ SOURCES +=
Controllers/HyperXMousematController/HyperXMousematController.cpp \
Controllers/HyperXMousematController/HyperXMousematControllerDetect.cpp \
Controllers/HyperXMousematController/RGBController_HyperXMousemat.cpp \
Controllers/HyperXQuadcastController/HyperXQuadcastSController.cpp \
Controllers/HyperXQuadcastController/HyperXQuadcastSControllerDetect.cpp \
Controllers/HyperXQuadcastController/RGBController_HyperXQuadcastS.cpp \
Controllers/IntelArcA770LEController/IntelArcA770LEController.cpp \
Controllers/IntelArcA770LEController/IntelArcA770LEControllerDetect.cpp \
Controllers/IonicoController/IonicoController.cpp \
@ -1619,6 +1625,7 @@ contains(QMAKE_PLATFORM, linux) {
-lmbedx509 \
-lmbedtls \
-lmbedcrypto \
-ldl \
COMPILER_VERSION = $$system($$QMAKE_CXX " -dumpversion")
if (!versionAtLeast(COMPILER_VERSION, "9")) {