diff --git a/Controllers/ThrustmasterSolController/RGBController_ThrustmasterSol.cpp b/Controllers/ThrustmasterSolController/RGBController_ThrustmasterSol.cpp new file mode 100644 index 000000000..5f8177afd --- /dev/null +++ b/Controllers/ThrustmasterSolController/RGBController_ThrustmasterSol.cpp @@ -0,0 +1,353 @@ +/*---------------------------------------------------------*\ +| RGBController_ThrustmasterSol.cpp | +| | +| RGBController 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 "RGBController_ThrustmasterSol.h" + +/**------------------------------------------------------------------*\ + @name Thrustmaster Sol + @category Gamepad + @type USB + @save :white_check_mark: + @direct :white_check_mark: + @effects :x: + @detectors DetectThrustmasterSolControllers + @comment Thrustmaster Sol series joystick RGB LED control. Supports + Sol-R, Sol F16, and Sol F18 variants. Only Sol-R has been tested. + Uses vendor-specific USB interface 1 (not HID). +\*-------------------------------------------------------------------*/ + +#define NA 0xFFFFFFFF + +static unsigned int logo_matrix_map[2][2] = +{ + { 0, 3 }, + { 1, 2 }, +}; + +static unsigned int ring_matrix_map[3][5] = +{ + { NA, 7, 0, 1, NA }, + { 6, NA, NA, NA, 2 }, + { NA, 5, 4, 3, NA }, +}; + +static unsigned int right_buttons_matrix_map[2][2] = +{ + { 0, 1 }, + { 2, 3 }, +}; + +static unsigned int left_buttons_matrix_map[2][2] = +{ + { 0, 1 }, + { 2, 3 }, +}; + +RGBController_ThrustmasterSol::RGBController_ThrustmasterSol(ThrustmasterSolController* controller_ptr) +{ + controller = controller_ptr; + + vendor = "Thrustmaster"; + type = DEVICE_TYPE_GAMEPAD; + description = "Thrustmaster Sol Series Joystick"; + location = controller->GetDeviceLocation(); + serial = controller->GetSerialString(); + + mode Direct; + Direct.name = "Direct"; + Direct.value = 0; + Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR | MODE_FLAG_MANUAL_SAVE; + Direct.color_mode = MODE_COLORS_PER_LED; + modes.push_back(Direct); + + SetupZones(); + + /*---------------------------------------------------------*\ + | Read current EEPROM colors from device so the UI starts | + | with the actual hardware state rather than all-black | + \*---------------------------------------------------------*/ + std::vector hw_zones; + std::vector hw_colors; + controller->ReadColors(hw_zones, hw_colors); + + for(unsigned int i = 0; i < hw_zones.size(); i++) + { + for(unsigned int j = 0; j < leds.size(); j++) + { + if(leds[j].value == hw_zones[i]) + { + colors[j] = hw_colors[i]; + break; + } + } + } +} + +RGBController_ThrustmasterSol::~RGBController_ThrustmasterSol() +{ + delete controller; +} + +void RGBController_ThrustmasterSol::SetupZones() +{ + /*---------------------------------------------------------*\ + | Thumbstick zone (grip, 1 LED) | + \*---------------------------------------------------------*/ + zone thumbstick_zone; + thumbstick_zone.name = "Thumbstick"; + thumbstick_zone.type = ZONE_TYPE_SINGLE; + thumbstick_zone.leds_min = 1; + thumbstick_zone.leds_max = 1; + thumbstick_zone.leds_count = 1; + thumbstick_zone.matrix_map = NULL; + zones.push_back(thumbstick_zone); + + led thumbstick_led; + thumbstick_led.name = "Thumbstick"; + thumbstick_led.value = THRUSTMASTER_SOL_GRIP_FLAG | 0x00; + leds.push_back(thumbstick_led); + + /*---------------------------------------------------------*\ + | TM Logo zone (3 LEDs) | + \*---------------------------------------------------------*/ + zone logo_zone; + logo_zone.name = "TM Logo"; + logo_zone.type = ZONE_TYPE_MATRIX; + logo_zone.leds_min = 4; + logo_zone.leds_max = 4; + logo_zone.leds_count = 4; + logo_zone.matrix_map = new matrix_map_type; + logo_zone.matrix_map->height = 2; + logo_zone.matrix_map->width = 2; + logo_zone.matrix_map->map = (unsigned int *)&logo_matrix_map; + zones.push_back(logo_zone); + + led logo_top_left; + logo_top_left.name = "TM Logo Top Left"; + logo_top_left.value = 0x01; + leds.push_back(logo_top_left); + + led logo_bottom_left; + logo_bottom_left.name = "TM Logo Bottom Left"; + logo_bottom_left.value = 0x02; + leds.push_back(logo_bottom_left); + + led logo_bottom_right; + logo_bottom_right.name = "TM Logo Bottom Right"; + logo_bottom_right.value = 0x03; + leds.push_back(logo_bottom_right); + + led logo_top_right; + logo_top_right.name = "TM Logo Top Right"; + logo_top_right.value = 0x00; + leds.push_back(logo_top_right); + + /*---------------------------------------------------------*\ + | Left Buttons zone (4 LEDs: buttons 5-8) | + \*---------------------------------------------------------*/ + zone left_buttons_zone; + left_buttons_zone.name = "Left Buttons"; + left_buttons_zone.type = ZONE_TYPE_MATRIX; + left_buttons_zone.leds_min = 4; + left_buttons_zone.leds_max = 4; + left_buttons_zone.leds_count = 4; + left_buttons_zone.matrix_map = new matrix_map_type; + left_buttons_zone.matrix_map->height = 2; + left_buttons_zone.matrix_map->width = 2; + left_buttons_zone.matrix_map->map = (unsigned int *)&left_buttons_matrix_map; + zones.push_back(left_buttons_zone); + + led btn5; + btn5.name = "Button 5"; + btn5.value = 0x11; + leds.push_back(btn5); + + led btn6; + btn6.name = "Button 6"; + btn6.value = 0x10; + leds.push_back(btn6); + + led btn7; + btn7.name = "Button 7"; + btn7.value = 0x12; + leds.push_back(btn7); + + led btn8; + btn8.name = "Button 8"; + btn8.value = 0x13; + leds.push_back(btn8); + + /*---------------------------------------------------------*\ + | Base Ring zone (8 LEDs, clockwise from top) | + \*---------------------------------------------------------*/ + zone ring_zone; + ring_zone.name = "Base Ring"; + ring_zone.type = ZONE_TYPE_MATRIX; + ring_zone.leds_min = 8; + ring_zone.leds_max = 8; + ring_zone.leds_count = 8; + ring_zone.matrix_map = new matrix_map_type; + ring_zone.matrix_map->height = 3; + ring_zone.matrix_map->width = 5; + ring_zone.matrix_map->map = (unsigned int *)&ring_matrix_map; + zones.push_back(ring_zone); + + led ring_upper; + ring_upper.name = "Upper"; + ring_upper.value = 0x04; + leds.push_back(ring_upper); + + led ring_upper_right; + ring_upper_right.name = "Upper Right"; + ring_upper_right.value = 0x05; + leds.push_back(ring_upper_right); + + led ring_right; + ring_right.name = "Right"; + ring_right.value = 0x06; + leds.push_back(ring_right); + + led ring_bottom_right; + ring_bottom_right.name = "Bottom Right"; + ring_bottom_right.value = 0x0B; + leds.push_back(ring_bottom_right); + + led ring_bottom; + ring_bottom.name = "Bottom"; + ring_bottom.value = 0x0C; + leds.push_back(ring_bottom); + + led ring_bottom_left; + ring_bottom_left.name = "Bottom Left"; + ring_bottom_left.value = 0x0D; + leds.push_back(ring_bottom_left); + + led ring_left; + ring_left.name = "Left"; + ring_left.value = 0x0E; + leds.push_back(ring_left); + + led ring_upper_left; + ring_upper_left.name = "Upper Left"; + ring_upper_left.value = 0x0F; + leds.push_back(ring_upper_left); + + /*---------------------------------------------------------*\ + | Right Buttons zone (4 LEDs: buttons 16-19) | + \*---------------------------------------------------------*/ + zone right_buttons_zone; + right_buttons_zone.name = "Right Buttons"; + right_buttons_zone.type = ZONE_TYPE_MATRIX; + right_buttons_zone.leds_min = 4; + right_buttons_zone.leds_max = 4; + right_buttons_zone.leds_count = 4; + right_buttons_zone.matrix_map = new matrix_map_type; + right_buttons_zone.matrix_map->height = 2; + right_buttons_zone.matrix_map->width = 2; + right_buttons_zone.matrix_map->map = (unsigned int *)&right_buttons_matrix_map; + zones.push_back(right_buttons_zone); + + led btn17; + btn17.name = "Button 17"; + btn17.value = 0x07; + leds.push_back(btn17); + + led btn16; + btn16.name = "Button 16"; + btn16.value = 0x08; + leds.push_back(btn16); + + led btn19; + btn19.name = "Button 19"; + btn19.value = 0x0A; + leds.push_back(btn19); + + led btn18; + btn18.name = "Button 18"; + btn18.value = 0x09; + leds.push_back(btn18); + + SetupColors(); +} + +void RGBController_ThrustmasterSol::ResizeZone(int /*zone*/, int /*new_size*/) +{ + /*---------------------------------------------------------*\ + | This device does not support resizing zones | + \*---------------------------------------------------------*/ +} + +void RGBController_ThrustmasterSol::DeviceUpdateLEDs() +{ + unsigned int led_zones[THRUSTMASTER_SOL_R_ZONE_COUNT]; + RGBColor led_colors[THRUSTMASTER_SOL_R_ZONE_COUNT]; + + for(unsigned int led_idx = 0; led_idx < leds.size(); led_idx++) + { + led_zones[led_idx] = leds[led_idx].value; + led_colors[led_idx] = colors[led_idx]; + } + + controller->SetLEDColors(led_zones, led_colors, static_cast(leds.size())); +} + +void RGBController_ThrustmasterSol::UpdateZoneLEDs(int zone) +{ + unsigned int start_idx = 0; + unsigned int zone_size = 0; + + for(unsigned int z_idx = 0; z_idx < zones.size(); z_idx++) + { + if(z_idx == (unsigned int)zone) + { + zone_size = zones[z_idx].leds_count; + break; + } + + start_idx += zones[z_idx].leds_count; + } + + unsigned int led_zones[THRUSTMASTER_SOL_R_ZONE_COUNT]; + RGBColor led_colors[THRUSTMASTER_SOL_R_ZONE_COUNT]; + + for(unsigned int led_idx = 0; led_idx < zone_size; led_idx++) + { + unsigned int current_idx = start_idx + led_idx; + led_zones[led_idx] = leds[current_idx].value; + led_colors[led_idx] = colors[current_idx]; + } + + controller->SetLEDColors(led_zones, led_colors, zone_size); +} + +void RGBController_ThrustmasterSol::UpdateSingleLED(int led) +{ + controller->SetLEDColor(leds[led].value, colors[led]); +} + +void RGBController_ThrustmasterSol::DeviceUpdateMode() +{ + DeviceUpdateLEDs(); +} + +void RGBController_ThrustmasterSol::DeviceSaveMode() +{ + unsigned int led_zones[THRUSTMASTER_SOL_R_ZONE_COUNT]; + RGBColor led_colors[THRUSTMASTER_SOL_R_ZONE_COUNT]; + + for(unsigned int led_idx = 0; led_idx < leds.size(); led_idx++) + { + led_zones[led_idx] = leds[led_idx].value; + led_colors[led_idx] = colors[led_idx]; + } + + controller->SaveColors(led_zones, led_colors, static_cast(leds.size())); +} diff --git a/Controllers/ThrustmasterSolController/RGBController_ThrustmasterSol.h b/Controllers/ThrustmasterSolController/RGBController_ThrustmasterSol.h new file mode 100644 index 000000000..97d899d11 --- /dev/null +++ b/Controllers/ThrustmasterSolController/RGBController_ThrustmasterSol.h @@ -0,0 +1,35 @@ +/*---------------------------------------------------------*\ +| RGBController_ThrustmasterSol.h | +| | +| RGBController 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 | +\*---------------------------------------------------------*/ + +#pragma once + +#include "RGBController.h" +#include "ThrustmasterSolController.h" + +class RGBController_ThrustmasterSol : public RGBController +{ +public: + RGBController_ThrustmasterSol(ThrustmasterSolController* controller_ptr); + ~RGBController_ThrustmasterSol(); + + void SetupZones(); + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); + void DeviceSaveMode(); + +private: + ThrustmasterSolController* controller; +}; diff --git a/Controllers/ThrustmasterSolController/ThrustmasterSolController.cpp b/Controllers/ThrustmasterSolController/ThrustmasterSolController.cpp new file mode 100644 index 000000000..8ea8a251b --- /dev/null +++ b/Controllers/ThrustmasterSolController/ThrustmasterSolController.cpp @@ -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 +#include +#include +#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(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& zones, + std::vector& 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)); + } + } +} diff --git a/Controllers/ThrustmasterSolController/ThrustmasterSolController.h b/Controllers/ThrustmasterSolController/ThrustmasterSolController.h new file mode 100644 index 000000000..e67485c30 --- /dev/null +++ b/Controllers/ThrustmasterSolController/ThrustmasterSolController.h @@ -0,0 +1,116 @@ +/*---------------------------------------------------------*\ +| ThrustmasterSolController.h | +| | +| 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 | +\*---------------------------------------------------------*/ + +#pragma once + +#include +#include +#include "RGBController.h" + +#ifdef _WIN32 +#include "dependencies/libusb-1.0.27/include/libusb.h" +#else +#include +#endif + +/*-----------------------------------------------------*\ +| Thrustmaster vendor ID | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_VID 0x044F + +/*-----------------------------------------------------*\ +| Thrustmaster Sol series product IDs | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_R_RIGHT_PID 0x0422 +#define THRUSTMASTER_SOL_R_LEFT_PID 0x042A +#define THRUSTMASTER_SOL_F16_RIGHT_PID 0x0420 +#define THRUSTMASTER_SOL_F18_RIGHT_PID 0x0421 +#define THRUSTMASTER_SOL_F16_LEFT_PID 0x0428 +#define THRUSTMASTER_SOL_F18_LEFT_PID 0x0429 + +/*-----------------------------------------------------*\ +| Thrustmaster Sol USB constants | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_INTERFACE 1 +#define THRUSTMASTER_SOL_ENDPOINT_OUT 0x02 +#define THRUSTMASTER_SOL_ENDPOINT_IN 0x82 +#define THRUSTMASTER_SOL_PACKET_SIZE 64 +#define THRUSTMASTER_SOL_MAX_ENTRIES 15 + +/*-----------------------------------------------------*\ +| Thrustmaster Sol report type identifiers | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_REPORT_BASE_LO 0x01 +#define THRUSTMASTER_SOL_REPORT_BASE_HI 0x08 +#define THRUSTMASTER_SOL_REPORT_GRIP_LO 0x01 +#define THRUSTMASTER_SOL_REPORT_GRIP_HI 0x88 + +/*-----------------------------------------------------*\ +| Thrustmaster Sol persistence flags | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_VOLATILE 0x00 +#define THRUSTMASTER_SOL_PERSISTENT 0x80 + +/*-----------------------------------------------------*\ +| Thrustmaster Sol read query report types | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_READ_BASE_LO 0x02 +#define THRUSTMASTER_SOL_READ_BASE_HI 0x00 +#define THRUSTMASTER_SOL_READ_GRIP_LO 0x02 +#define THRUSTMASTER_SOL_READ_GRIP_HI 0x80 + +/*-----------------------------------------------------*\ +| Thrustmaster Sol zone constants | +| | +| Zone 0x00 is shared: on the base it is a logo LED, | +| on the grip it is the thumbstick LED. The grip flag | +| (bit 8) selects the grip report type for zone 0x00. | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_ZONE_THUMBSTICK 0x00 +#define THRUSTMASTER_SOL_GRIP_FLAG 0x100 +#define THRUSTMASTER_SOL_MARKER 0xFF + +/*-----------------------------------------------------*\ +| Thrustmaster Sol zone count for Sol-R | +| 19 base zones + thumbstick + logo zone 0x00 = 21 | +\*-----------------------------------------------------*/ +#define THRUSTMASTER_SOL_R_ZONE_COUNT 21 + +class ThrustmasterSolController +{ +public: + ThrustmasterSolController(libusb_device_handle* dev_handle, + const char* path, + unsigned short pid); + ~ThrustmasterSolController(); + + std::string GetDeviceLocation(); + std::string GetSerialString(); + unsigned short GetPID(); + + void SetLEDColor(unsigned int zone, RGBColor color); + void SetLEDColors(unsigned int* zones, RGBColor* colors, + unsigned int count); + void SaveColors(unsigned int* zones, RGBColor* colors, + unsigned int count); + void ReadColors(std::vector& zones, + std::vector& colors); + +private: + libusb_device_handle* dev; + std::string location; + std::string serial; + unsigned short pid; + + void SendPacket(unsigned char* packet, unsigned int size); + void BuildAndSendPackets(unsigned int* zones, RGBColor* colors, + unsigned int count, bool persistent); +}; diff --git a/Controllers/ThrustmasterSolController/ThrustmasterSolControllerDetect.cpp b/Controllers/ThrustmasterSolController/ThrustmasterSolControllerDetect.cpp new file mode 100644 index 000000000..84bcf3f5b --- /dev/null +++ b/Controllers/ThrustmasterSolController/ThrustmasterSolControllerDetect.cpp @@ -0,0 +1,125 @@ +/*---------------------------------------------------------*\ +| ThrustmasterSolControllerDetect.cpp | +| | +| Detector 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 "Detector.h" +#include "ThrustmasterSolController.h" +#include "RGBController_ThrustmasterSol.h" + +typedef struct +{ + unsigned short usb_vid; + unsigned short usb_pid; + const char* name; +} thrustmaster_sol_device; + +#define THRUSTMASTER_SOL_NUM_DEVICES (sizeof(device_list) / sizeof(device_list[0])) + +static const thrustmaster_sol_device device_list[] = +{ + /*-----------------------------------------------------------------------------------------------------*\ + | Sol-R variants (tested) | + \*-----------------------------------------------------------------------------------------------------*/ + { THRUSTMASTER_VID, THRUSTMASTER_SOL_R_RIGHT_PID, "Thrustmaster Sol-R Right" }, + { THRUSTMASTER_VID, THRUSTMASTER_SOL_R_LEFT_PID, "Thrustmaster Sol-R Left" }, + /*-----------------------------------------------------------------------------------------------------*\ + | Sol F16/F18 variants (untested, same base protocol) | + \*-----------------------------------------------------------------------------------------------------*/ + { THRUSTMASTER_VID, THRUSTMASTER_SOL_F16_RIGHT_PID, "Thrustmaster Sol F16 Right" }, + { THRUSTMASTER_VID, THRUSTMASTER_SOL_F18_RIGHT_PID, "Thrustmaster Sol F18 Right" }, + { THRUSTMASTER_VID, THRUSTMASTER_SOL_F16_LEFT_PID, "Thrustmaster Sol F16 Left" }, + { THRUSTMASTER_VID, THRUSTMASTER_SOL_F18_LEFT_PID, "Thrustmaster Sol F18 Left" }, +}; + +/******************************************************************************************\ +* * +* DetectThrustmasterSolControllers * +* * +* Detect Thrustmaster Sol series joysticks via libusb. * +* LED control is on USB interface 1 (vendor-specific class 255, not HID). * +* Interface 0 (joystick HID) is not touched. * +* * +\******************************************************************************************/ + +void DetectThrustmasterSolControllers() +{ + libusb_init(NULL); + + #ifdef _WIN32 + libusb_set_option(NULL, LIBUSB_OPTION_USE_USBDK); + #endif + + libusb_device** devs; + ssize_t num_devs = libusb_get_device_list(NULL, &devs); + + if(num_devs <= 0) + { + return; + } + + for(ssize_t i = 0; i < num_devs; i++) + { + libusb_device_descriptor desc; + + if(libusb_get_device_descriptor(devs[i], &desc) != 0) + { + continue; + } + + for(std::size_t d = 0; d < THRUSTMASTER_SOL_NUM_DEVICES; d++) + { + if(desc.idVendor == device_list[d].usb_vid && + desc.idProduct == device_list[d].usb_pid) + { + libusb_device_handle* handle = NULL; + + if(libusb_open(devs[i], &handle) != LIBUSB_SUCCESS) + { + continue; + } + + libusb_set_auto_detach_kernel_driver(handle, 1); + + if(libusb_claim_interface(handle, THRUSTMASTER_SOL_INTERFACE) != LIBUSB_SUCCESS) + { + libusb_close(handle); + continue; + } + + uint8_t bus = libusb_get_bus_number(devs[i]); + uint8_t address = libusb_get_device_address(devs[i]); + char path[32]; + snprintf(path, sizeof(path), "%d-%d", bus, address); + + ThrustmasterSolController* controller = new ThrustmasterSolController(handle, path, desc.idProduct); + RGBController_ThrustmasterSol* rgb_controller = new RGBController_ThrustmasterSol(controller); + + rgb_controller->name = device_list[d].name; + + ResourceManager::get()->RegisterRGBController(rgb_controller); + break; + } + } + } + + libusb_free_device_list(devs, 1); +} + +REGISTER_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers); +/*---------------------------------------------------------------------------------------------------------*\ +| Entries for dynamic UDEV rules | +| | +| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0420 ) | +| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0421 ) | +| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0422 ) | +| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0428 ) | +| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x0429 ) | +| DUMMY_DEVICE_DETECTOR("Thrustmaster Sol", DetectThrustmasterSolControllers, 0x044F, 0x042A ) | +\*---------------------------------------------------------------------------------------------------------*/