Add support for Thrustmaster Sol series joystick RGB LEDs

This commit is contained in:
Ken Sanislo
2026-05-09 05:21:40 +00:00
committed by Adam Honse
parent 29d8788e25
commit 9410d9c066
5 changed files with 944 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
/*---------------------------------------------------------*\
| ThrustmasterSolController.cpp |
| |
| Driver for Thrustmaster Sol series joysticks |
| |
| Ken Sanislo 02 Apr 2026 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include <thread>
#include <chrono>
#include "ThrustmasterSolController.h"
#include "StringUtils.h"
ThrustmasterSolController::ThrustmasterSolController(libusb_device_handle* dev_handle,
const char* path,
unsigned short pid)
{
dev = dev_handle;
this->pid = pid;
location = "USB: ";
location += path;
/*---------------------------------------------------------*\
| Get serial number string descriptor |
\*---------------------------------------------------------*/
libusb_device_descriptor desc;
libusb_get_device_descriptor(libusb_get_device(dev_handle), &desc);
if(desc.iSerialNumber != 0)
{
unsigned char serial_str[256];
int ret = libusb_get_string_descriptor_ascii(dev_handle,
desc.iSerialNumber,
serial_str,
sizeof(serial_str));
if(ret > 0)
{
serial = std::string(reinterpret_cast<char*>(serial_str), ret);
}
}
}
ThrustmasterSolController::~ThrustmasterSolController()
{
if(dev != nullptr)
{
libusb_release_interface(dev, THRUSTMASTER_SOL_INTERFACE);
libusb_close(dev);
}
}
std::string ThrustmasterSolController::GetDeviceLocation()
{
return(location);
}
std::string ThrustmasterSolController::GetSerialString()
{
return(serial);
}
unsigned short ThrustmasterSolController::GetPID()
{
return(pid);
}
void ThrustmasterSolController::SendPacket(unsigned char* packet, unsigned int size)
{
if(dev == nullptr)
{
return;
}
int actual_length = 0;
libusb_interrupt_transfer(dev,
THRUSTMASTER_SOL_ENDPOINT_OUT,
packet,
size,
&actual_length,
1000);
}
void ThrustmasterSolController::BuildAndSendPackets(unsigned int* zones,
RGBColor* colors,
unsigned int count,
bool persistent)
{
unsigned char persist_flag = persistent ? THRUSTMASTER_SOL_PERSISTENT
: THRUSTMASTER_SOL_VOLATILE;
/*---------------------------------------------------------*\
| Separate grip zones from base zones. Zone 0x00 exists on |
| both the base (logo LED) and the grip (thumbstick LED). |
| The GRIP_FLAG bit selects the grip report type. |
\*---------------------------------------------------------*/
unsigned char base_zones[THRUSTMASTER_SOL_R_ZONE_COUNT];
RGBColor base_colors[THRUSTMASTER_SOL_R_ZONE_COUNT];
unsigned int base_count = 0;
for(unsigned int i = 0; i < count; i++)
{
if(zones[i] & THRUSTMASTER_SOL_GRIP_FLAG)
{
/*-------------------------------------------------*\
| Send grip packet for thumbstick immediately |
\*-------------------------------------------------*/
unsigned char zone_id = zones[i] & 0xFF;
unsigned char packet[THRUSTMASTER_SOL_PACKET_SIZE];
memset(packet, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
packet[0] = THRUSTMASTER_SOL_REPORT_GRIP_LO;
packet[1] = THRUSTMASTER_SOL_REPORT_GRIP_HI;
packet[2] = persist_flag | 0x01;
packet[3] = THRUSTMASTER_SOL_MARKER;
packet[4] = zone_id;
packet[5] = RGBGetRValue(colors[i]);
packet[6] = RGBGetGValue(colors[i]);
packet[7] = RGBGetBValue(colors[i]);
SendPacket(packet, THRUSTMASTER_SOL_PACKET_SIZE);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
else
{
base_zones[base_count] = zones[i] & 0xFF;
base_colors[base_count] = colors[i];
base_count++;
}
}
/*---------------------------------------------------------*\
| Send base zone packets, batching up to 15 entries each |
\*---------------------------------------------------------*/
for(unsigned int offset = 0; offset < base_count; offset += THRUSTMASTER_SOL_MAX_ENTRIES)
{
unsigned int entries = base_count - offset;
if(entries > THRUSTMASTER_SOL_MAX_ENTRIES)
{
entries = THRUSTMASTER_SOL_MAX_ENTRIES;
}
unsigned char packet[THRUSTMASTER_SOL_PACKET_SIZE];
memset(packet, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
packet[0] = THRUSTMASTER_SOL_REPORT_BASE_LO;
packet[1] = THRUSTMASTER_SOL_REPORT_BASE_HI;
packet[2] = persist_flag | (unsigned char)entries;
packet[3] = THRUSTMASTER_SOL_MARKER;
for(unsigned int j = 0; j < entries; j++)
{
unsigned int idx = offset + j;
packet[4 + j * 4] = base_zones[idx];
packet[4 + j * 4 + 1] = RGBGetRValue(base_colors[idx]);
packet[4 + j * 4 + 2] = RGBGetGValue(base_colors[idx]);
packet[4 + j * 4 + 3] = RGBGetBValue(base_colors[idx]);
}
SendPacket(packet, THRUSTMASTER_SOL_PACKET_SIZE);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
}
}
void ThrustmasterSolController::SetLEDColor(unsigned int zone, RGBColor color)
{
BuildAndSendPackets(&zone, &color, 1, false);
}
void ThrustmasterSolController::SetLEDColors(unsigned int* zones,
RGBColor* colors,
unsigned int count)
{
BuildAndSendPackets(zones, colors, count, false);
}
void ThrustmasterSolController::SaveColors(unsigned int* zones,
RGBColor* colors,
unsigned int count)
{
BuildAndSendPackets(zones, colors, count, true);
}
void ThrustmasterSolController::ReadColors(std::vector<unsigned int>& zones,
std::vector<RGBColor>& colors)
{
if(dev == nullptr)
{
return;
}
zones.clear();
colors.clear();
/*---------------------------------------------------------*\
| Read base zones (report 0x0002). May require multiple |
| queries if more than 14 zones are returned per response. |
| Zone IDs are returned without the grip flag, so they |
| match the base LED values (including zone 0x00 = logo). |
\*---------------------------------------------------------*/
unsigned char start = 0;
for(int page = 0; page < 4; page++)
{
unsigned char query[THRUSTMASTER_SOL_PACKET_SIZE];
memset(query, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
query[0] = THRUSTMASTER_SOL_READ_BASE_LO;
query[1] = THRUSTMASTER_SOL_READ_BASE_HI;
query[2] = start;
SendPacket(query, THRUSTMASTER_SOL_PACKET_SIZE);
unsigned char resp[THRUSTMASTER_SOL_PACKET_SIZE];
int actual = 0;
int ret = libusb_interrupt_transfer(dev,
THRUSTMASTER_SOL_ENDPOINT_IN,
resp,
THRUSTMASTER_SOL_PACKET_SIZE,
&actual,
1000);
if(ret != LIBUSB_SUCCESS || actual < 4)
{
break;
}
unsigned int n = resp[2] & 0x0F;
if(n == 0)
{
break;
}
unsigned char last = start;
for(unsigned int i = 0; i < n; i++)
{
unsigned int off = 4 + i * 4;
if(off + 3 >= (unsigned int)actual)
{
break;
}
unsigned char zid = resp[off];
unsigned char r = resp[off + 1];
unsigned char g = resp[off + 2];
unsigned char b = resp[off + 3];
zones.push_back(zid);
colors.push_back(ToRGBColor(r, g, b));
last = zid;
}
start = last + 1;
if(n < 14 || start > 0x20)
{
break;
}
}
/*---------------------------------------------------------*\
| Read grip zones (report 0x8002). Grip zone IDs are |
| returned with the GRIP_FLAG set so they match the |
| thumbstick LED value. They are added as new entries, |
| not overwriting base entries (zone 0x00 exists on both). |
\*---------------------------------------------------------*/
unsigned char grip_query[THRUSTMASTER_SOL_PACKET_SIZE];
memset(grip_query, 0x00, THRUSTMASTER_SOL_PACKET_SIZE);
grip_query[0] = THRUSTMASTER_SOL_READ_GRIP_LO;
grip_query[1] = THRUSTMASTER_SOL_READ_GRIP_HI;
SendPacket(grip_query, THRUSTMASTER_SOL_PACKET_SIZE);
unsigned char grip_resp[THRUSTMASTER_SOL_PACKET_SIZE];
int grip_actual = 0;
int grip_ret = libusb_interrupt_transfer(dev,
THRUSTMASTER_SOL_ENDPOINT_IN,
grip_resp,
THRUSTMASTER_SOL_PACKET_SIZE,
&grip_actual,
1000);
if(grip_ret == LIBUSB_SUCCESS && grip_actual >= 8)
{
unsigned int grip_n = grip_resp[2] & 0x0F;
for(unsigned int i = 0; i < grip_n; i++)
{
unsigned int off = 4 + i * 4;
if(off + 3 >= (unsigned int)grip_actual)
{
break;
}
unsigned char zid = grip_resp[off];
unsigned char r = grip_resp[off + 1];
unsigned char g = grip_resp[off + 2];
unsigned char b = grip_resp[off + 3];
zones.push_back(THRUSTMASTER_SOL_GRIP_FLAG | zid);
colors.push_back(ToRGBColor(r, g, b));
}
}
}