Files
SalastilandClaude Opus 5 f888b1ddbc Fix Corsair K70 RGB PRO V2 direct lighting
The K70 RGB PRO V2 (1B1C:1BB3) was detected but never lit: it blanked
into software render mode and then fell back to its onboard effect.
Getting it working turned up five faults, three of which affect every
Corsair V2 device rather than just this keyboard.

Packet size is now read from the HID report descriptor before the first
write instead of being inferred from a reply. These devices come in a 64
byte and a 1024 byte flavour, and sending a short packet to a 1024 byte
endpoint stalls it until the device is power cycled, so the size has to
be known up front rather than after two short queries have gone out.

CORSAIR_V2_PACKET_SIZE was 1024 while pkt_sze reached 1025, so every
read using pkt_sze overran its stack buffer by a byte. The command
helpers now use pkt_sze against buffers of the corrected size.

The lighting resource probe ran before the device was placed in software
render mode, where the answer is meaningless, and treated any error as
"use resource 1". Devices that answer invalid or unsupported for
resource 1 want the alternate lighting resource, which takes RGB
triplets; this keyboard is one of them. The probe now runs after the
render mode switch and distinguishes a stale open handle from an
unsupported resource.

Direct lighting writes were sized from the keymap, which covers only the
keys that exist. The hardware expects its full slot count and ignores a
short write, so corsair_v2_device carries an optional hw_led_count and
the buffer map is padded to it. Set to 193 for this keyboard.

The keepalive thread wrote LEDs on its first pass, because
last_update_time was left at the clock epoch and so always read as older
than the update period. That raced detection on the same HID handle with
no locking, which both corrupted the transaction and could abort the
process. The clock is now started before the thread runs, device
transactions are serialised behind a mutex, and the thread is stopped
before Shutdown rather than after.

Also fixes an uninitialised stack buffer sent to the device during
setup, and widens the K70 RGB PRO matrix to 22 columns so the numpad
period key has a position in the layout instead of being dropped.

Verified on hardware: repeated colour changes apply and the device stays
responsive across runs.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-21 21:08:51 -04:00

183 lines
8.4 KiB
C++

/*---------------------------------------------------------*\
| CorsairPeripheralV2Controller.h |
| |
| Driver for Corsair V2 peripherals |
| |
| Chris M (Dr_No) 07 Aug 2022 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <mutex>
#include <vector>
#include <hidapi.h>
#include "LogManager.h"
#include "RGBController.h"
#include "CorsairPeripheralV2Devices.h"
#define NA 0xFFFFFFFF
#define HID_MAX_STR 255
/*---------------------------------------------------------*\
| A jumbo packet device answers a 1KB command in well over |
| 50ms. Missing the reply leaves it queued and the next |
| write to the device then fails outright, so this has to |
| be generous. A reply that does arrive is returned as |
| soon as it lands, so this only costs time on failure. |
\*---------------------------------------------------------*/
#define CORSAIR_V2_TIMEOUT 500
#define CORSAIR_V2_TIMEOUT_SHORT 3
#define CORSAIR_V2_VALUE_MODE 3
#define CORSAIR_V2_WRITE_WIRED_ID 8
#define CORSAIR_V2_WRITE_WIRELESS_ID 9
#define CORSAIR_V2_WRITE_SIZE 65
/*---------------------------------------------------------*\
| Largest packet the protocol uses: 1024 payload bytes |
| plus the leading report ID. pkt_sze is measured |
| against this, and every stack buffer handed to hidapi |
| with pkt_sze is this size, so the two must agree. |
\*---------------------------------------------------------*/
#define CORSAIR_V2_PACKET_SIZE 1025
/*---------------------------------------------------------*\
| Error codes the device returns in byte 2 of a reply. |
\*---------------------------------------------------------*/
#define CORSAIR_V2_ERR_INVALID 0x01
#define CORSAIR_V2_ERR_FAILED 0x03
#define CORSAIR_V2_ERR_NOT_SUPPORTED 0x06
#define CORSAIR_V2_LIGHT_CTRL1 1
#define CORSAIR_V2_LIGHT_CTRL2 34 /* 0x22 */
#define CORSAIR_V2_UPDATE_PERIOD 30000
#define CORSAIR_V2_SLEEP_PERIOD 12500ms
#define CORSAIR_V2_BRIGHTNESS_MIN 0
#define CORSAIR_V2_BRIGHTNESS_MAX 0xFF
/*---------------------------------------------------------*\
| A device that has just enumerated does not answer the PID |
| query straight away, so retry before giving up on it. |
\*---------------------------------------------------------*/
#define CORSAIR_V2_PID_RETRIES 8
#define CORSAIR_V2_PID_RETRY_DELAY 120
/*---------------------------------------------------------*\
| device_index when the PID is not in the device list. |
| Never index the device list with it. |
\*---------------------------------------------------------*/
#define CORSAIR_V2_DEVICE_NOT_FOUND 0xFFFF
enum corsair_v2_cmd
{
CORSAIR_V2_CMD_SET = 0x01, /* Command for setting values */
CORSAIR_V2_CMD_GET = 0x02, /* Command for getting values */
CORSAIR_V2_CMD_STOP_TX = 0x05, /* Finish Transaction */
CORSAIR_V2_CMD_BLK_W1 = 0x06, /* Block write packet 1 */
CORSAIR_V2_CMD_BLK_WN = 0x07, /* Block write remaining packets */
CORSAIR_V2_CMD_START_TX = 0x0D, /* Start Transaction */
};
enum corsair_v2_mode
{
CORSAIR_V2_MODE_DIRECT = 0x0012,
CORSAIR_V2_MODE_STATIC = 0x207E,
CORSAIR_V2_MODE_FLASHING = 0xAD4F,
CORSAIR_V2_MODE_BREATHING = 0xA5FA,
CORSAIR_V2_MODE_SPECTRUM = 0x7BFF,
CORSAIR_V2_MODE_RAINBOW = 0xB94C,
CORSAIR_V2_MODE_RAIN = 0xA07E,
CORSAIR_V2_MODE_SPIRAL = 0xAB87,
CORSAIR_V2_MODE_WATERCOLOR = 0x0022,
CORSAIR_V2_MODE_REACTIVE = 0xB1F9,
CORSAIR_V2_MODE_RIPPLE = 0x09A2,
CORSAIR_V2_MODE_VISOR = 0x90C0
};
enum corsair_v2_color
{
CORSAIR_V2_COLOR_NONE = 0x00,
CORSAIR_V2_COLOR_SPECIFIC = 0x01,
CORSAIR_V2_COLOR_RANDOM = 0x02,
CORSAIR_V2_COLOR_UNKNOWN = 0x03
};
class CorsairPeripheralV2Controller
{
public:
CorsairPeripheralV2Controller(hid_device* dev_handle, const char* path, std::string name);
virtual ~CorsairPeripheralV2Controller();
std::string GetDeviceLocation();
std::string GetErrorString(uint8_t err);
std::string GetFirmwareString();
std::string GetName();
std::string GetSerialString();
const corsair_v2_device* GetDeviceData();
bool IsDeviceSupported();
unsigned int GetKeyboardLayout();
void SetRenderMode(corsair_v2_device_mode mode);
void LightingControl(uint8_t opt1);
void SetLEDs(uint8_t *data, uint16_t data_size);
void UpdateHWMode(uint16_t mode, corsair_v2_color color_mode, uint8_t speed,
uint8_t direction, uint8_t brightness, std::vector<RGBColor> colors);
virtual void SetLedsDirect(std::vector<RGBColor *> colors) = 0;
protected:
uint16_t device_index;
std::string device_name;
uint8_t light_ctrl = CORSAIR_V2_LIGHT_CTRL2;
/*---------------------------------------------------------*\
| Works out which lighting control endpoint the device |
| accepts. Must be called only once the device has been |
| put into software render mode: endpoint 2 rejects the |
| transaction while the device is still driving its own |
| lighting, which would pick endpoint 1 for a device that |
| does not support it. |
\*---------------------------------------------------------*/
void DetectLightingEndpoint();
/*---------------------------------------------------------*\
| Size of one output report, plus the leading report ID. |
| Read from the HID report descriptor before anything is |
| sent: these devices come in a 64 byte and a 1024 byte |
| flavour, and sending a short packet to a 1024 byte |
| endpoint wedges it until the device is reset, so the |
| size has to be known before the first write rather than |
| inferred from a reply. Returns 0 if it cannot be read. |
\*---------------------------------------------------------*/
uint16_t GetOutputReportSize();
private:
void ClearPacketBuffer();
unsigned int GetAddress(uint8_t address);
unsigned int GetAddressRetry(uint8_t address);
unsigned char StartTransaction(uint8_t opt1);
void StopTransaction(uint8_t opt1);
hid_device* dev;
/*---------------------------------------------------------*\
| Serialises device transactions. |
| The RGB controllers run a keepalive thread that writes |
| LEDs independently of the caller, and a Corsair block |
| write spans several packets. Letting two of those |
| interleave on one HID handle corrupts the transaction |
| and leaves the device unresponsive until it is reset. |
\*---------------------------------------------------------*/
std::mutex device_mutex;
uint8_t write_cmd = CORSAIR_V2_WRITE_WIRED_ID;
uint16_t pkt_sze = CORSAIR_V2_WRITE_SIZE;
bool skip_reads = false;
std::string firmware_version;
std::string location;
};