mirror of
https://github.com/CalcProgrammer1/OpenRGB.git
synced 2026-06-24 13:48:45 -04:00
G515 LS TKL and G502 X PLUS support, via Logitech HID++ 2.0 controller with feature discovery
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <thread>
|
||||
#include <hidapi.h>
|
||||
#include "Detector.h"
|
||||
#include "ResourceManager.h"
|
||||
#include "LogManager.h"
|
||||
#include "LogitechProtocolCommon.h"
|
||||
#include "LogitechG203LController.h"
|
||||
@@ -40,6 +41,8 @@
|
||||
#include "RGBController_LogitechLightspeed.h"
|
||||
#include "RGBController_LogitechGPowerPlay.h" // Linux-only
|
||||
#include "RGBController_LogitechX56.h"
|
||||
#include "LogitechHIDPP20Controller.h"
|
||||
#include "RGBController_LogitechHIDPP20.h"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
@@ -84,6 +87,9 @@ using namespace std::chrono_literals;
|
||||
#define LOGITECH_G502_HERO_PID 0xC08B
|
||||
#define LOGITECH_G502_LIGHTSPEED_PID 0xC08D
|
||||
#define LOGITECH_G502_X_PLUS_PID 0xC095
|
||||
#define LOGITECH_G515_LS_TKL_PID 0xC355
|
||||
#define LOGITECH_G522_LIGHTSPEED_USB_PID 0x0B19
|
||||
#define LOGITECH_G522_LIGHTSPEED_DONGLE_PID 0x0B18
|
||||
#define LOGITECH_G600_PID 0xC24A
|
||||
#define LOGITECH_G703_LIGHTSPEED_PID 0xC087
|
||||
#define LOGITECH_G703_HERO_LIGHTSPEED_PID 0xC090
|
||||
@@ -135,6 +141,8 @@ using namespace std::chrono_literals;
|
||||
#define LOGITECH_G903_LIGHTSPEED_VIRTUAL_HERO_PID 0x4087
|
||||
#define LOGITECH_G_PRO_WIRELESS_VIRTUAL_PID 0x4079
|
||||
#define LOGITECH_POWERPLAY_MAT_VIRTUAL_PID 0x405F
|
||||
#define LOGITECH_G502_X_PLUS_LIGHTSPEED_VIRTUAL_PID 0x4099
|
||||
#define LOGITECH_G515_LS_TKL_LIGHTSPEED_VIRTUAL_PID 0x40B4
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Joystick product IDs |
|
||||
@@ -650,6 +658,220 @@ void DetectLogitechX56(hid_device_info* info, const std::string& name)
|
||||
}
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------------*\
|
||||
| Unified HID++ 2.0 Detection |
|
||||
| Probes IRoot (feature 0x0000) to determine if the device speaks HID++ 2.0. |
|
||||
| If it does and has RGB features, the unified controller handles it. |
|
||||
| If not, the device is released for legacy controllers. |
|
||||
\*------------------------------------------------------------------------------*/
|
||||
void DetectLogitechHIDPP20(hid_device_info* info, const std::string& /*name*/)
|
||||
{
|
||||
hid_device* dev = hid_open_path(info->path);
|
||||
|
||||
if(!dev)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogitechHIDPP20Controller* controller = new LogitechHIDPP20Controller(
|
||||
dev, info->path, LOGITECH_DEFAULT_DEVICE_INDEX, false, nullptr,
|
||||
info->usage_page);
|
||||
|
||||
if(controller->Probe())
|
||||
{
|
||||
controller->Initialize();
|
||||
|
||||
const HIDPP20DeviceCapabilities& caps = controller->GetCapabilities();
|
||||
|
||||
if(caps.has_zone_effects || caps.has_perkey)
|
||||
{
|
||||
/*-------------------------------------------------*\
|
||||
| Device has RGB features — create and register |
|
||||
| RGBController for the UI. |
|
||||
\*-------------------------------------------------*/
|
||||
RGBController_LogitechHIDPP20* rgb_controller = new RGBController_LogitechHIDPP20(controller);
|
||||
|
||||
LOG_INFO("[%s] Registering RGB controller", caps.device_name.c_str());
|
||||
|
||||
ResourceManager::get()->RegisterRGBController(rgb_controller);
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Start reader + power threads immediately so we |
|
||||
| detect connection events and handle power mgmt |
|
||||
| from the start — not deferred to DeviceUpdateMode. |
|
||||
\*--------------------------------------------------*/
|
||||
if(caps.has_power_mgmt || caps.idx_wireless_status != 0)
|
||||
{
|
||||
controller->StartPowerManager();
|
||||
|
||||
if(!caps.has_power_mgmt && caps.idx_wireless_status != 0)
|
||||
{
|
||||
controller->StartEventWatcher();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(controller->HasBridge())
|
||||
{
|
||||
/*--------------------------------------------------*\
|
||||
| Centurion dongle with no sub-device — keep the |
|
||||
| controller alive and start reader thread to watch |
|
||||
| for sub-device connection events. |
|
||||
\*--------------------------------------------------*/
|
||||
LOG_INFO("[%s] Dongle registered, watching for sub-device",
|
||||
caps.device_name.c_str());
|
||||
|
||||
controller->SetRegisterCallback([](RGBController* rgb)
|
||||
{
|
||||
ResourceManager::get()->RegisterRGBController(rgb);
|
||||
});
|
||||
|
||||
controller->StartEventWatcher();
|
||||
}
|
||||
else
|
||||
{
|
||||
/*--------------------------------------------------*\
|
||||
| Device probed successfully but has no RGB and no |
|
||||
| bridge — nothing to do (e.g., headset without RGB) |
|
||||
\*--------------------------------------------------*/
|
||||
LOG_INFO("[%s] No RGB features, skipping", caps.device_name.c_str());
|
||||
delete controller;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/*--------------------------------------------------*\
|
||||
| Probe failed. Could be an offline paired device, |
|
||||
| a stale pairing slot, or a receiver itself. |
|
||||
| |
|
||||
| Only skip if the name explicitly says "Receiver". |
|
||||
| Everything else gets a watcher — devices can come |
|
||||
| back at any time (power cycle, dongle swap, etc.) |
|
||||
\*--------------------------------------------------*/
|
||||
std::string hid_name;
|
||||
|
||||
if(info->product_string)
|
||||
{
|
||||
std::wstring ws(info->product_string);
|
||||
hid_name = std::string(ws.begin(), ws.end());
|
||||
}
|
||||
|
||||
if(hid_name.find("Receiver") != std::string::npos ||
|
||||
hid_name.find("receiver") != std::string::npos)
|
||||
{
|
||||
delete controller;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO("[HID++2.0 %s] Probe failed — watching for device (name='%s')",
|
||||
info->path, hid_name.c_str());
|
||||
|
||||
controller->SetRegisterCallback([](RGBController* rgb)
|
||||
{
|
||||
ResourceManager::get()->RegisterRGBController(rgb);
|
||||
});
|
||||
|
||||
controller->StartProbeWatcher();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Unified HID++ 2.0 Lightspeed Receiver Detection (Windows / macOS) |
|
||||
| |
|
||||
| On Linux, hid-logitech-dj splits receiver traffic into per-slot virtual child hidraw nodes with their own 0x40XX PIDs, so Linux detection can |
|
||||
| use DetectLogitechHIDPP20 directly against the virtual PIDs. Windows and macOS have no DJ driver — the receiver appears as a single HID device |
|
||||
| and we must probe each paired slot by hand, addressing it via the HID++ device_index header byte. |
|
||||
| |
|
||||
| Iterates device indices 0x01..0x06. c547 is dual-pair, but the loop covers Unifying-style receivers and any future wider-pair variants. Each |
|
||||
| responding slot gets its own hid_device handle and a shared std::mutex so sibling slots serialize HID writes — matching the pattern in the |
|
||||
| legacy LogitechLightspeedController (see CreateLogitechLightspeedDevice around line 860). |
|
||||
| |
|
||||
| TODO (untested on Windows): runtime reader-thread coordination. Each slot controller starts its own reader thread; on a shared receiver both |
|
||||
| threads will see both slots' incoming packets. The reader needs to drop frames whose device_index doesn't match its own, or dispatch across |
|
||||
| sibling controllers. Safe during probe (mutex serializes writes, reads are direct); becomes an issue post-StartPowerManager. |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
void DetectLogitechHIDPP20LightspeedReceiver(hid_device_info* info, const std::string& /*name*/)
|
||||
{
|
||||
std::shared_ptr<std::mutex> receiver_mutex = std::make_shared<std::mutex>();
|
||||
|
||||
for(uint8_t idx = 0x01; idx <= 0x06; idx++)
|
||||
{
|
||||
hid_device* dev = hid_open_path(info->path);
|
||||
|
||||
if(!dev)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LogitechHIDPP20Controller* controller = new LogitechHIDPP20Controller(
|
||||
dev, info->path, idx, true, receiver_mutex, info->usage_page);
|
||||
|
||||
if(!controller->Probe())
|
||||
{
|
||||
/*--------------------------------------------------*\
|
||||
| Slot is empty, stale, or not HID++ 2.0. Destructor |
|
||||
| closes the per-slot handle we opened above. |
|
||||
\*--------------------------------------------------*/
|
||||
delete controller;
|
||||
continue;
|
||||
}
|
||||
|
||||
controller->Initialize();
|
||||
|
||||
const HIDPP20DeviceCapabilities& caps = controller->GetCapabilities();
|
||||
|
||||
if(caps.has_zone_effects || caps.has_perkey)
|
||||
{
|
||||
RGBController_LogitechHIDPP20* rgb_controller = new RGBController_LogitechHIDPP20(controller);
|
||||
|
||||
LOG_INFO("[%s slot=%u] Registering RGB controller", caps.device_name.c_str(), idx);
|
||||
|
||||
ResourceManager::get()->RegisterRGBController(rgb_controller);
|
||||
|
||||
if(caps.has_power_mgmt || caps.idx_wireless_status != 0)
|
||||
{
|
||||
controller->StartPowerManager();
|
||||
|
||||
if(!caps.has_power_mgmt && caps.idx_wireless_status != 0)
|
||||
{
|
||||
controller->StartEventWatcher();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO("[%s slot=%u] No RGB features, skipping", caps.device_name.c_str(), idx);
|
||||
delete controller;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Unified HID++ 2.0 Devices |
|
||||
| PID-specific registrations for devices tested with the unified controller. |
|
||||
| Wired paths use the device's own USB PID; wireless paths on Linux use the hid-logitech-dj virtual child PIDs (0x40XX range). |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G502 X Plus (wired)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G502_X_PLUS_PID, 2, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G515 LS TKL (wired)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G515_LS_TKL_PID, 2, 0xFF00, 2);
|
||||
#ifdef __linux__
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G502 X Plus (wireless)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G502_X_PLUS_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 G515 LS TKL (wireless)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G515_LS_TKL_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
|
||||
#endif
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech HID++ 2.0 Lightspeed Receiver (C547)", DetectLogitechHIDPP20LightspeedReceiver, LOGITECH_VID, 0xC547, 2, 0xFF00, 2);
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Centurion-transport devices (63-byte reports on usage page 0xFFA0, 0x50 addressed or 0x51 direct). |
|
||||
| Centurion receivers are not DJ-style Lightspeed receivers and are not split by hid-logitech-dj — the dongle PID enumerates as a single hidraw |
|
||||
| on all platforms. The controller's DiscoverTransport parses the report descriptor to pick the 0x50/0x51 variant and runs a 0x00..0xFF address |
|
||||
| sweep for the 0x50 (addressed) variant, so the detector only needs VID/PID + usage page 0xFFA0. |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
REGISTER_HID_DETECTOR_P("Logitech HID++ 2.0 G522 Lightspeed (wired)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G522_LIGHTSPEED_USB_PID, 0xFFA0);
|
||||
REGISTER_HID_DETECTOR_P("Logitech HID++ 2.0 G522 Lightspeed (dongle)", DetectLogitechHIDPP20, LOGITECH_VID, LOGITECH_G522_LIGHTSPEED_DONGLE_PID, 0xFFA0);
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Keyboards |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
@@ -683,11 +905,11 @@ REGISTER_HID_DETECTOR_IP ("Logitech G600 Gaming Mouse", Dete
|
||||
REGISTER_HID_DETECTOR_IP ("Logitech G Pro Gaming Mouse", DetectLogitechMouseGPRO, LOGITECH_VID, LOGITECH_G_PRO_PID, 1, 0xFF00);
|
||||
REGISTER_HID_DETECTOR_IP ("Logitech G Pro HERO Gaming Mouse", DetectLogitechMouseGPRO, LOGITECH_VID, LOGITECH_G_PRO_HERO_PID, 1, 0xFF00);
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Speakers |
|
||||
| Speakers |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G560 Lightsync Speaker", DetectLogitechG560, LOGITECH_VID, LOGITECH_G560_PID, 2, 0xFF43, 514);
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Headsets |
|
||||
| Headsets |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G933 Lightsync Headset", DetectLogitechG933, LOGITECH_VID, LOGITECH_G933_PID, 3, 0xFF43, 514);
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
@@ -890,12 +1112,12 @@ void DetectLogitechWireless(hid_device_info* info, const std::string& /*name*/)
|
||||
}
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Lightspeed Devices (Linux Wireless) |
|
||||
| |
|
||||
| DUMMY_DEVICE_DETECTOR("Logitech G Lightspeed Receiver", DetectLogitechWireless, 0x046D, 0xC539 ) |
|
||||
/*--------------------------------------------------------------------------------------------------------------------------------------------------*\
|
||||
| Lightspeed Devices (Linux Wireless) |
|
||||
| |
|
||||
| DUMMY_DEVICE_DETECTOR("Logitech G Lightspeed Receiver", DetectLogitechWireless, 0x046D, 0xC539 ) |
|
||||
| DUMMY_DEVICE_DETECTOR("Logitech Powerplay Mat Receiver", DetectLogitechWireless, 0x046D, 0xC53A ) |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
\*--------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G403 Wireless Gaming Mouse", DetectLogitechWireless, LOGITECH_VID, LOGITECH_G403_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G502 Wireless Gaming Mouse", DetectLogitechWireless, LOGITECH_VID, LOGITECH_G502_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G703 Wireless Gaming Mouse", DetectLogitechWireless, LOGITECH_VID, LOGITECH_G703_LIGHTSPEED_VIRTUAL_PID, 2, 0xFF00, 2);
|
||||
@@ -913,7 +1135,6 @@ REGISTER_HID_DETECTOR_IPU("Logitech Powerplay Mat",
|
||||
| G502 changed to PU to accomodate old and new firmware. Other devices may require similar update #4627 |
|
||||
\*-------------------------------------------------------------------------------------------------------------------------------------------------*/
|
||||
REGISTER_HID_DETECTOR_PU("Logitech G502 Wireless Gaming Mouse (wired)", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_LIGHTSPEED_PID, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G502 X Plus (wired)", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_X_PLUS_PID, 2, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G502 Proteus Spectrum Gaming Mouse", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_PROTEUS_SPECTRUM_PID, 1, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G502 HERO Gaming Mouse", DetectLogitechWired, LOGITECH_VID, LOGITECH_G502_HERO_PID, 1, 0xFF00, 2);
|
||||
REGISTER_HID_DETECTOR_IPU("Logitech G403 Prodigy Gaming Mouse", DetectLogitechWired, LOGITECH_VID, LOGITECH_G403_PID, 1, 0xFF00, 2);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,874 @@
|
||||
/*---------------------------------------------------------*\
|
||||
| LogitechHIDPP20Controller.h |
|
||||
| |
|
||||
| Unified Logitech HID++ 2.0 controller |
|
||||
| |
|
||||
| Uses feature discovery (IRoot 0x0000) to dynamically |
|
||||
| determine device capabilities and adapt to any HID++ |
|
||||
| 2.0 device with RGB lighting support. |
|
||||
| |
|
||||
| This file is part of the OpenRGB project |
|
||||
| SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
\*---------------------------------------------------------*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <condition_variable>
|
||||
#include <hidapi.h>
|
||||
#include "RGBController.h"
|
||||
#include "LogitechProtocolCommon.h"
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| HID++ 2.0 Feature Page IDs |
|
||||
\*-----------------------------------------------------*/
|
||||
#define HIDPP20_FEAT_IROOT 0x0000
|
||||
#define HIDPP20_FEAT_FEATURE_SET 0x0001
|
||||
#define HIDPP20_FEAT_DEVICE_NAME_TYPE 0x0005
|
||||
#define HIDPP20_FEAT_ONBOARD_PROFILES 0x8100
|
||||
#define HIDPP20_FEAT_PROFILE_MANAGEMENT 0x8101
|
||||
#define HIDPP20_FEAT_FIRMWARE_INFO 0x0003
|
||||
#define HIDPP20_FEAT_CENTPPBRIDGE 0x0003 /* same ID, different meaning on Centurion */
|
||||
#define HIDPP20_FEAT_COLOR_LED_EFFECTS 0x8070
|
||||
#define HIDPP20_FEAT_RGB_EFFECTS 0x8071
|
||||
#define HIDPP20_FEAT_PER_KEY_LIGHTING_V1 0x8080
|
||||
#define HIDPP20_FEAT_PER_KEY_LIGHTING_V2 0x8081
|
||||
#define HIDPP20_FEAT_KEYBOARD_LAYOUT 0x4540
|
||||
#define HIDPP20_FEAT_DISABLE_KEYS_BY_USAGE 0x4522
|
||||
#define HIDPP20_FEAT_CENTURION_RGB 0x0600
|
||||
#define HIDPP20_FEAT_HEADSET_RGB_HOSTMODE 0x0620
|
||||
#define HIDPP20_FEAT_CENTURION_DEVICE_INFO 0x0100
|
||||
#define HIDPP20_FEAT_CENTURION_DEVICE_NAME 0x0101
|
||||
#define HIDPP20_FEAT_UNIFIED_BATTERY 0x1004
|
||||
#define HIDPP20_FEAT_WIRELESS_STATUS 0x1D4B
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| HID++ 2.0 Function IDs (byte 3 high nibble) |
|
||||
| Function ID is shifted left 4 bits, low nibble = swID |
|
||||
\*-----------------------------------------------------*/
|
||||
/*-----------------------------------------------------*\
|
||||
| HID++ Software ID — identifies our responses. |
|
||||
| Must avoid: 0x00 (firmware), 0x01 (vendor app), |
|
||||
| 0x02-0x0F (Solaar cycles these). |
|
||||
| There are only 16 values (4-bit field), and all are |
|
||||
| claimed. We pick a fixed value and will coordinate |
|
||||
| with Solaar to exclude it from its cycle. |
|
||||
\*-----------------------------------------------------*/
|
||||
#define HIDPP20_SW_ID 0x07
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Feature 0x8071 functions |
|
||||
\*-----------------------------------------------------*/
|
||||
#define FN_8071_GET_INFO 0x00
|
||||
#define FN_8071_SET_EFFECT 0x10
|
||||
#define FN_8071_SET_PATTERN 0x20
|
||||
#define FN_8071_NV_CONFIG 0x30
|
||||
#define FN_8071_BIN_INFO 0x40
|
||||
#define FN_8071_SW_CONTROL 0x50
|
||||
#define FN_8071_SYNC 0x60
|
||||
#define FN_8071_PWR_CONFIG 0x70
|
||||
#define FN_8071_PWR_MODE 0x80
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Feature 0x8081 functions |
|
||||
\*-----------------------------------------------------*/
|
||||
#define FN_8081_GET_INFO 0x00
|
||||
#define FN_8081_SET_INDIVIDUAL 0x10
|
||||
#define FN_8081_SET_CONSECUTIVE 0x20
|
||||
#define FN_8081_SET_DELTA_5BIT 0x30
|
||||
#define FN_8081_SET_DELTA_4BIT 0x40
|
||||
#define FN_8081_SET_RANGE 0x50
|
||||
#define FN_8081_SET_SINGLE_VALUE 0x60
|
||||
#define FN_8081_FRAME_END 0x70
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Feature 0x0620 functions (headset RGB hostmode) |
|
||||
\*-----------------------------------------------------*/
|
||||
#define FN_0620_GET_INFO 0x00
|
||||
#define FN_0620_GET_RGB_ZONE_INFO 0x10
|
||||
#define FN_0620_SET_INDIVIDUAL_RGB_ZONES 0x20
|
||||
#define FN_0620_SET_CONSECUTIVE_RGB_ZONES 0x30
|
||||
#define FN_0620_SET_RANGE_RGB_ZONES 0x40
|
||||
#define FN_0620_SET_RGB_ZONES_SINGLE_VALUE 0x50
|
||||
#define FN_0620_FRAME_END 0x60
|
||||
#define FN_0620_GET_HOST_MODE_STATE 0x70
|
||||
#define FN_0620_SET_HOST_MODE_STATE 0x80
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Feature 0x8100 functions |
|
||||
\*-----------------------------------------------------*/
|
||||
#define FN_8100_SET_ONBOARD_MODE 0x10
|
||||
#define FN_8100_GET_ONBOARD_MODE 0x20
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Feature 0x8101 functions |
|
||||
\*-----------------------------------------------------*/
|
||||
#define FN_8101_GET_SET_MODE 0x60
|
||||
#define FN_8101_LOAD 0x80 // load(partition, sector, size)
|
||||
#define FN_8101_READBUFFER 0xC0 // readBuffer(offset)
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Zone cluster effect entry |
|
||||
\*-----------------------------------------------------*/
|
||||
struct HIDPP20Effect
|
||||
{
|
||||
uint8_t index;
|
||||
uint16_t effect_id;
|
||||
uint16_t capabilities;
|
||||
uint16_t default_period;
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Zone cluster info from 0x8071 GetRgbClusterInfo |
|
||||
\*-----------------------------------------------------*/
|
||||
struct HIDPP20ZoneCluster
|
||||
{
|
||||
uint8_t index;
|
||||
uint16_t location;
|
||||
uint8_t effect_count;
|
||||
std::vector<HIDPP20Effect> effects;
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Per-model device quirks — behavioral differences that |
|
||||
| can't be detected via feature probing. |
|
||||
\*-----------------------------------------------------*/
|
||||
enum HIDPP20DeviceQuirks : uint32_t
|
||||
{
|
||||
HIDPP20_QUIRK_NONE = 0,
|
||||
HIDPP20_QUIRK_FADE_ACCEPTS_WRITES = (1 << 0), // firmware accepts host frames during sleep fade without waking
|
||||
};
|
||||
|
||||
struct HIDPP20DeviceQuirkEntry
|
||||
{
|
||||
uint16_t pid_wireless;
|
||||
uint16_t pid_wired;
|
||||
uint32_t quirks;
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Default: suppress frames while SLEEPING. Safe everywhere |
|
||||
| — it cannot wake a device that treats writes as activity. |
|
||||
| Devices listed here opt out of suppression because their |
|
||||
| firmware accepts writes during the fade without |
|
||||
| cancelling sleep. |
|
||||
\*---------------------------------------------------------*/
|
||||
static constexpr HIDPP20DeviceQuirkEntry HIDPP20_DEVICE_QUIRK_TABLE[] =
|
||||
{
|
||||
{ 0x40B4, 0xC355, HIDPP20_QUIRK_FADE_ACCEPTS_WRITES }, // G515 LS TKL
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Device capabilities discovered via feature probing |
|
||||
\*-----------------------------------------------------*/
|
||||
struct HIDPP20DeviceCapabilities
|
||||
{
|
||||
std::string device_name;
|
||||
uint8_t device_type;
|
||||
std::string firmware_version;
|
||||
std::string serial_number;
|
||||
std::string unit_id; // stable hardware ID (from FirmwareInfo fn0)
|
||||
uint16_t pid_wireless; // wireless virtual PID (from FirmwareInfo fn0)
|
||||
uint16_t pid_wired; // wired USB PID (from FirmwareInfo fn0)
|
||||
uint32_t quirks; // resolved from HIDPP20_DEVICE_QUIRK_TABLE after PID discovery
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Complete feature map (feature_id → runtime index) |
|
||||
| Built once by EnumerateFeatures, used by all |
|
||||
| subsequent GetFeatureIndex lookups (no wire). |
|
||||
\*--------------------------------------------------*/
|
||||
std::map<uint16_t, uint8_t> feature_map;
|
||||
std::map<uint16_t, uint8_t> feature_versions; /* feature_id -> version byte */
|
||||
bool feature_map_complete; // true after bulk enumeration
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Feature indices (0 = not supported) |
|
||||
\*-------------------------------------------------*/
|
||||
uint8_t idx_onboard_profiles;
|
||||
uint8_t idx_profile_management;
|
||||
uint8_t idx_rgb_effects;
|
||||
uint8_t idx_headset_rgb_hostmode; /* 0x0620 — Centurion headset RGB */
|
||||
uint8_t idx_perkey_v2;
|
||||
uint8_t idx_perkey_v1;
|
||||
uint8_t idx_wireless_status;
|
||||
uint8_t idx_disable_keys_by_usage; /* 0x4522 — keyboard-family handshake */
|
||||
uint16_t rgb_feature_page;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Resolved function IDs for the RGB effects feature |
|
||||
| Varies between 0x8070, 0x8071, 0x0600 |
|
||||
\*--------------------------------------------------*/
|
||||
uint8_t fn_set_effect;
|
||||
uint8_t fn_sw_control;
|
||||
uint8_t fn_pwr_config;
|
||||
uint8_t fn_pwr_mode;
|
||||
bool has_power_mgmt;
|
||||
bool sw_control_simple;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Persistent NV settings read from RGBEffects fn3 |
|
||||
| (FN_8071_NV_CONFIG). Capability 0x0020 is the |
|
||||
| sleep ramp / off-ramp transition (enabled + |
|
||||
| ramp_seconds). |
|
||||
\*--------------------------------------------------*/
|
||||
bool nv_sleep_ramp_known;
|
||||
bool nv_sleep_ramp_enabled;
|
||||
uint8_t nv_sleep_ramp_seconds;
|
||||
|
||||
/*---------------------------------------------------*\
|
||||
| Device-firmware effect cards (0x8071 fn0 |
|
||||
| GetEffectSpecificInfo). Populated by |
|
||||
| DiscoverEffectCards at feature-discovery time. |
|
||||
| |
|
||||
| has_effect_cards — probe returned a valid response |
|
||||
| for firmware card 0 page 1 (no InvalidArgument). |
|
||||
| effect_card_template[0..1] — device-wide constant |
|
||||
| bytes read from that response at data[10..11]. |
|
||||
| Echoed back into prep1 of DoObservedPerKeyPrep |
|
||||
| at SetEffectByIndex params[6..7]. |
|
||||
| |
|
||||
| Gate for the observed per-key prep is now |
|
||||
| has_effect_cards — replaces the earlier |
|
||||
| "effects.size() < 5" heuristic, which was a proxy |
|
||||
| that correlated with "has cards" on the devices we |
|
||||
| happened to know but had no principled meaning. |
|
||||
\*---------------------------------------------------*/
|
||||
bool has_effect_cards;
|
||||
uint8_t effect_card_template[2];
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Discovered zone and LED data |
|
||||
\*-------------------------------------------------*/
|
||||
std::vector<HIDPP20ZoneCluster> zone_clusters;
|
||||
std::vector<uint16_t> perkey_zone_ids;
|
||||
std::vector<uint8_t> headset_rgb_hostmode_zone_ids; /* 0x0620 fn1 result */
|
||||
bool has_perkey;
|
||||
bool has_zone_effects;
|
||||
bool is_headset_rgb_hostmode; /* 0x0620 path selected */
|
||||
bool has_numpad;
|
||||
uint8_t keyboard_layout_code;
|
||||
};
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Transport type — determines wire framing |
|
||||
\*------------------------------------------------------*/
|
||||
enum HIDPP20TransportType
|
||||
{
|
||||
HIDPP20_TRANSPORT_STANDARD, // 0xFF00/0xFF43: report IDs 0x10/0x11, 7/20 bytes
|
||||
HIDPP20_TRANSPORT_CENTURION // 0xFFA0: report ID 0x51 or 0x50, 64 bytes,
|
||||
// with CPL framing and CentPPBridge sub-device routing
|
||||
};
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Transport layer — abstracts wire format differences |
|
||||
| |
|
||||
| Standard HID++ and Centurion both carry the same |
|
||||
| feature/function/data payload, but with different |
|
||||
| report framing. This struct holds transport state |
|
||||
| so SendMessage/ReadMessage can adapt. |
|
||||
\*------------------------------------------------------*/
|
||||
struct HIDPP20Transport
|
||||
{
|
||||
HIDPP20TransportType type;
|
||||
uint16_t usage_page; // 0xFF00, 0xFF43, or 0xFFA0
|
||||
uint8_t report_id; // 0x10/0x11 for standard, 0x51/0x50 for Centurion
|
||||
bool addressed; // Centurion 0x50 has device address byte
|
||||
uint8_t device_address; // Centurion 0x50: device address (e.g., 0x23)
|
||||
uint8_t bridge_feat_idx; // CentPPBridge feature index on parent (0 if N/A)
|
||||
uint8_t sub_device_id; // CentPPBridge sub-device ID (typically 0)
|
||||
uint16_t bridge_mtu; // CentPPBridge MTU from getConnectionInfo:
|
||||
// 0 = no sub-device, sendFragment will fail
|
||||
// >0 = sub-device present, payload size in bytes
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Power management state machine |
|
||||
| Matches Solaar's RGBPowerManager states |
|
||||
\*-----------------------------------------------------*/
|
||||
enum HIDPP20PowerState
|
||||
{
|
||||
HIDPP20_POWER_ACTIVE = 0,
|
||||
HIDPP20_POWER_DIMMING = 1,
|
||||
HIDPP20_POWER_IDLE = 2,
|
||||
HIDPP20_POWER_SLEEPING = 3,
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Parsed HID++ message for the response queue |
|
||||
\*-----------------------------------------------------*/
|
||||
struct HIDPP20RawMessage
|
||||
{
|
||||
uint8_t feat;
|
||||
uint8_t func;
|
||||
uint8_t data[60];
|
||||
int result;
|
||||
};
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Per-key write tracking. SendPerKeyData is fire-and- |
|
||||
| forget at the wire layer; we track which zones each |
|
||||
| outstanding write covers so PerKeyFrameEnd can match |
|
||||
| ACKs back by FIFO order and report which zones the |
|
||||
| firmware actually committed. |
|
||||
\*------------------------------------------------------*/
|
||||
struct OutstandingPerKeyWrite
|
||||
{
|
||||
uint8_t function; // FN_8081_* (high nibble carries the type)
|
||||
std::vector<uint8_t> zone_ids; // zones covered by this packet
|
||||
};
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Result of a per-key frame commit. The caller uses |
|
||||
| these to update its delta-tracking state: |
|
||||
| - frame_end_acked: did the FrameEnd packet ACK? |
|
||||
| - acked_zones: zones whose write packet ACKed |
|
||||
| - attempted_zones: every zone written this frame |
|
||||
\*-----------------------------------------------------*/
|
||||
struct PerKeyFrameResult
|
||||
{
|
||||
bool frame_end_acked;
|
||||
std::vector<uint8_t> acked_zones;
|
||||
std::vector<uint8_t> attempted_zones;
|
||||
};
|
||||
|
||||
/*-------------------------------------------------------*\
|
||||
| Retry policy for SendAcked. |
|
||||
| |
|
||||
| Controls the send/read/retry loop for a single HID++ |
|
||||
| request. Three canned policies cover all use cases: |
|
||||
| |
|
||||
| Reliable: probe/discovery/set/get (~6s worst case) |
|
||||
| FrameEnd: per-key commit gate (~230ms worst) |
|
||||
| Streaming: per-key write inside the animation loop |
|
||||
| (~80ms worst) |
|
||||
| |
|
||||
| backoff_ms[i] is the delay applied BEFORE the i-th |
|
||||
| attempt. backoff_ms[0] is normally 0 (no delay before |
|
||||
| the first send). The schedule mirrors the firmware's |
|
||||
| own event burst pattern (63→125→250→500→1000→2000ms, |
|
||||
| "catch at least one of N"). |
|
||||
\*-------------------------------------------------------*/
|
||||
struct HIDPP20RetryPolicy
|
||||
{
|
||||
const uint16_t* backoff_ms; // schedule[i] = delay before attempt i
|
||||
uint8_t attempts; // length of backoff_ms (>=1)
|
||||
uint16_t read_window_ms; // per-attempt read budget
|
||||
bool flush_before; // flush response queue at call start
|
||||
bool retry_on_busy; // BUSY (0x08) -> retry the send
|
||||
const char* name; // for logging ("reliable", etc.)
|
||||
};
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Canned backoff schedules. |
|
||||
\*------------------------------------------------------*/
|
||||
static constexpr uint16_t HIDPP20_BACKOFF_RELIABLE[] =
|
||||
{ 0, 63, 125, 250, 500, 1000, 2000 };
|
||||
static constexpr uint16_t HIDPP20_BACKOFF_PROBE[] =
|
||||
{ 0, 100 };
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| SW-control reclaim backoff. Used by ReconnectDevice |
|
||||
| to retry the claim+push sequence after a wireless |
|
||||
| reconnect, racing the firmware boot animation. The |
|
||||
| vendor app typically lands control in ~50ms; this |
|
||||
| schedule fits |
|
||||
| 6 attempts inside ~620ms so the animation never gets |
|
||||
| a chance to become visible. |
|
||||
\*------------------------------------------------------*/
|
||||
static constexpr uint16_t HIDPP20_RECLAIM_BACKOFF_MS[] =
|
||||
{ 0, 20, 40, 80, 160, 320 };
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| FrameEnd BUSY retry backoff. Used by PerKeyFrameEnd |
|
||||
| when the firmware returns HID++ error 0x08 (BUSY) |
|
||||
| because it's still draining the per-key write queue. |
|
||||
| First retry is fast (~2 USB round trips) for the |
|
||||
| common case where BUSY was transient; subsequent |
|
||||
| retries give actual drain headroom. Total worst case |
|
||||
| ~180ms, fits inside the PerKeyFrameEnd 300ms deadline |
|
||||
| with margin for the eventual ACK to land. |
|
||||
\*------------------------------------------------------*/
|
||||
static constexpr uint16_t HIDPP20_FRAME_END_BUSY_BACKOFF_MS[] =
|
||||
{ 30, 60, 90 };
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Deep-sleep detection threshold. After StartSleep() |
|
||||
| commands the firmware fade, the device eventually |
|
||||
| enters deep sleep and returns BUSY to every FrameEnd. |
|
||||
| Once this many consecutive FrameEnd attempts exhaust |
|
||||
| all BUSY retries while power_state == SLEEPING, we |
|
||||
| suppress further frame sends until Wake() fires. |
|
||||
\*-----------------------------------------------------*/
|
||||
static constexpr int HIDPP20_DEEP_SLEEP_FAILURE_THRESHOLD = 5;
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Per-key frame retry backoff. Used when a full |
|
||||
| DeviceUpdateLEDs pass completes with some zones |
|
||||
| unacked (partial commit). The retry re-runs a whole |
|
||||
| frame from the power thread, so the backoff is |
|
||||
| between full frames, not individual packets. |
|
||||
| |
|
||||
| First value aligned to the power thread's 50ms poll |
|
||||
| cadence — anything shorter rounds up anyway, and |
|
||||
| matching the tick makes latency predictable. |
|
||||
| |
|
||||
| Worst case: 5 retries, cumulative ~1550ms. This |
|
||||
| covers the reconnect-transient window where the G502 |
|
||||
| firmware silently drops per-key writes for several |
|
||||
| hundred ms after the wireless link re-establishes. |
|
||||
\*------------------------------------------------------*/
|
||||
static constexpr uint16_t HIDPP20_REPAINT_RETRY_BACKOFF_MS[] =
|
||||
{ 50, 100, 200, 400, 800 };
|
||||
|
||||
static constexpr HIDPP20RetryPolicy HIDPP20_POLICY_RELIABLE = {
|
||||
HIDPP20_BACKOFF_RELIABLE,
|
||||
sizeof(HIDPP20_BACKOFF_RELIABLE) / sizeof(uint16_t),
|
||||
300, // read window
|
||||
true, // flush_before
|
||||
true, // retry_on_busy
|
||||
"reliable"
|
||||
};
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Probe policy: tight budget for is-this-HID++ checks |
|
||||
| during initial discovery. ~500ms worst case for dead |
|
||||
| devices, vs ~6s for reliable. One retry handles a |
|
||||
| transient hiccup on the first IRoot call (e.g. on a |
|
||||
| busy mouse), but we bail fast on truly non-responsive |
|
||||
| or non-HID++ hidraws so probe latency stays bounded. |
|
||||
\*------------------------------------------------------*/
|
||||
static constexpr HIDPP20RetryPolicy HIDPP20_POLICY_PROBE = {
|
||||
HIDPP20_BACKOFF_PROBE,
|
||||
sizeof(HIDPP20_BACKOFF_PROBE) / sizeof(uint16_t),
|
||||
200, // read window
|
||||
true, // flush_before
|
||||
true, // retry_on_busy
|
||||
"probe"
|
||||
};
|
||||
|
||||
class LogitechHIDPP20Controller
|
||||
{
|
||||
public:
|
||||
LogitechHIDPP20Controller(hid_device* dev, const char* path,
|
||||
uint8_t device_index, bool wireless,
|
||||
std::shared_ptr<std::mutex> mutex_ptr,
|
||||
uint16_t usage_page = 0xFF00);
|
||||
~LogitechHIDPP20Controller();
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Lifecycle |
|
||||
\*-------------------------------------------------*/
|
||||
bool Probe();
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Accessors |
|
||||
\*-------------------------------------------------*/
|
||||
const HIDPP20DeviceCapabilities& GetCapabilities() const;
|
||||
std::string GetDeviceLocation();
|
||||
std::string GetSerialString();
|
||||
uint32_t GetInitGeneration() const;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Per-key lighting (0x8081) |
|
||||
| |
|
||||
| Per-key writes are fire-and-forget at the wire |
|
||||
| layer. SendPerKeyData enqueues an outstanding |
|
||||
| write entry; PerKeyFrameEnd drains the response |
|
||||
| queue, matches ACKs by FIFO, and returns which |
|
||||
| zones the firmware actually committed plus |
|
||||
| whether the FrameEnd itself ACKed. |
|
||||
\*--------------------------------------------------*/
|
||||
void SetPerKeyColors(const std::vector<std::pair<uint16_t, RGBColor>>& zone_colors);
|
||||
void SetAllPerKeyColor(RGBColor color);
|
||||
void SendPerKeyData(uint8_t perkey_idx, uint8_t function,
|
||||
const uint8_t* data, size_t len,
|
||||
const std::vector<uint8_t>& zone_ids);
|
||||
PerKeyFrameResult PerKeyFrameEnd();
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Zone effects (0x8071 / 0x8070) |
|
||||
\*-------------------------------------------------*/
|
||||
void SetZoneEffect(uint8_t cluster_idx, uint8_t effect_idx,
|
||||
uint16_t effect_id,
|
||||
unsigned char r, unsigned char g, unsigned char b,
|
||||
uint16_t period, unsigned char brightness,
|
||||
unsigned char direction, bool persist);
|
||||
|
||||
/*---------------------------------------------------*\
|
||||
| Headset RGB hostmode (0x0620). |
|
||||
| Sticky-claim model: SetHostMode() claims once via |
|
||||
| fn8, then each write is fn5 (single-value) or fn2 |
|
||||
| (individual) + fn6 FrameEnd[0x01]. 0x02 persist was |
|
||||
| tested and does not work on G522 firmware. |
|
||||
\*---------------------------------------------------*/
|
||||
void SetHeadsetRGBHostmodeColors(const std::vector<RGBColor>& zone_colors);
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| SW control management |
|
||||
\*-------------------------------------------------*/
|
||||
int SetSWControl(uint8_t mode, uint8_t flags);
|
||||
void SetRGBPowerMode(uint8_t mode);
|
||||
void SetHostMode();
|
||||
bool ClaimSWControlIfNeeded();
|
||||
void UpgradeSwControlAfterFirstPaint();
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Keyboard-family handshake (0x4522 fn3 + fn1). |
|
||||
| G815 / G915 / G Pro send this before any mode |
|
||||
| write. Feature-gated no-op on devices (G502 / |
|
||||
| G515) that don't enumerate 0x4522. |
|
||||
\*-------------------------------------------------*/
|
||||
void DoDisableKeysByUsageHandshake();
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Keyboard-family per-key takeover prep. |
|
||||
| Per-cluster SetEffectByIndex(effectIdx=0=Off, |
|
||||
| persist=1) + primer key via SetIndividualRgbZones |
|
||||
| + FrameEnd. Matches G815 / G915 InitializeDirect. |
|
||||
| Gated on 0x4522 + per-key V2 presence. |
|
||||
\*--------------------------------------------------*/
|
||||
void DoKeyboardFamilyPerKeyPrep();
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Wake-repaint flag. Set by Wake() before calling |
|
||||
| request_repaint_fn so the repaint callback knows |
|
||||
| to invalidate sent_colors (force a full per-key |
|
||||
| push) without triggering the claim/prep sequence. |
|
||||
\*--------------------------------------------------*/
|
||||
bool ConsumeWakeFullRepaint();
|
||||
bool NeedsPrepSequence() const { return sw_control_needs_upgrade_to_5; }
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Per-key retry scheduling. Called by the RGB |
|
||||
| controller's DeviceUpdateLEDs on partial-commit |
|
||||
| frames; the power thread polls and fires |
|
||||
| request_repaint_fn when a retry deadline expires. |
|
||||
\*--------------------------------------------------*/
|
||||
void ScheduleRetryPaint();
|
||||
void CancelRetryPaint();
|
||||
void TickRetryPaintIfPending();
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Observed per-key prep sequence. Two |
|
||||
| SetEffectByIndex calls cloned byte-for-byte from a |
|
||||
| wire capture on a G502 X PLUS. |
|
||||
| Used in place of the Static-pass-through prep when |
|
||||
| the device's RGBEffects enumeration matches the |
|
||||
| G502 shape — see DeviceUpdateLEDs for gating. |
|
||||
\*--------------------------------------------------*/
|
||||
void DoObservedPerKeyPrep();
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Power management (idle/dim/sleep/wake) |
|
||||
\*--------------------------------------------------*/
|
||||
void StartPowerManager();
|
||||
void StopPowerManager();
|
||||
void StartEventWatcher();
|
||||
void StartProbeWatcher();
|
||||
bool HasBridge() const;
|
||||
void SetRepaintCallback(std::function<void()> repaint);
|
||||
void SetReapplyActiveModeCallback(std::function<bool()> cb);
|
||||
void SetRegisterCallback(std::function<void(RGBController*)> cb);
|
||||
HIDPP20PowerState GetPowerState() const;
|
||||
int GetDimBrightness() const;
|
||||
bool IsOnline() const;
|
||||
bool IsDeepSleep() const;
|
||||
void SetWireless(bool w) { wireless = w; }
|
||||
bool QueryWirelessStatus();
|
||||
bool QueryExternalPower();
|
||||
void FlushResponseQueue();
|
||||
|
||||
private:
|
||||
hid_device* dev;
|
||||
std::string location;
|
||||
uint8_t device_index;
|
||||
bool wireless;
|
||||
std::shared_ptr<std::mutex> mutex;
|
||||
HIDPP20DeviceCapabilities caps;
|
||||
HIDPP20Transport transport;
|
||||
bool initialized;
|
||||
bool sw_control_claimed;
|
||||
bool sw_control_needs_upgrade_to_5;
|
||||
uint32_t frame_counter;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Retry-paint state (partial-commit recovery). |
|
||||
| retry_paint_deadline_ zero = no retry pending. |
|
||||
| retry_paint_attempt_ indexes into |
|
||||
| HIDPP20_REPAINT_RETRY_BACKOFF_MS; once it reaches |
|
||||
| the array length, we give up for this sequence. |
|
||||
| Atomic so both the paint thread (RGB controller) |
|
||||
| and the power thread can access without locks. |
|
||||
\*--------------------------------------------------*/
|
||||
std::atomic<std::chrono::steady_clock::time_point> retry_paint_deadline_;
|
||||
std::atomic<uint8_t> retry_paint_attempt_;
|
||||
std::atomic<bool> wake_full_repaint_pending_;
|
||||
uint32_t init_generation;
|
||||
std::string log_tag;
|
||||
|
||||
/*---------------------------------------------------*\
|
||||
| Transport-layer I/O |
|
||||
| |
|
||||
| SendMessage/ReadMessage handle wire framing based |
|
||||
| on transport.type. Upper layers pass feature index, |
|
||||
| function ID, and payload — the transport layer |
|
||||
| wraps them in the correct report format. |
|
||||
\*---------------------------------------------------*/
|
||||
int SendMessage(uint8_t feat_idx, uint8_t function,
|
||||
const uint8_t* data, size_t len);
|
||||
int ReadMessage(uint8_t* feat_idx_out, uint8_t* function_out,
|
||||
uint8_t* data_out, size_t data_max,
|
||||
int timeout_ms = LOGITECH_PROTOCOL_TIMEOUT);
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Standard HID++ transport (0xFF00/0xFF43) |
|
||||
\*--------------------------------------------------*/
|
||||
int SendStandard(uint8_t feat_idx, uint8_t function,
|
||||
const uint8_t* data, size_t len);
|
||||
int ReadStandardDirect(uint8_t* feat_idx_out, uint8_t* function_out,
|
||||
uint8_t* data_out, size_t data_max,
|
||||
int timeout_ms);
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Centurion transport (0xFFA0) |
|
||||
| Wraps messages in CPL framing, routes through |
|
||||
| CentPPBridge for sub-device access. |
|
||||
\*--------------------------------------------------*/
|
||||
int SendCenturion(uint8_t feat_idx, uint8_t function,
|
||||
const uint8_t* data, size_t len);
|
||||
int ReadCenturionDirect(uint8_t* feat_idx_out, uint8_t* function_out,
|
||||
uint8_t* data_out, size_t data_max,
|
||||
int timeout_ms);
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Reader thread dispatch layer |
|
||||
| ReadHIDDirect: raw HID read (used by reader thread |
|
||||
| and during Probe before reader starts). |
|
||||
| ReadFromQueue: waits on response queue filled by |
|
||||
| the reader thread. |
|
||||
\*--------------------------------------------------*/
|
||||
int ReadHIDDirect(uint8_t* feat_idx_out, uint8_t* function_out,
|
||||
uint8_t* data_out, size_t data_max,
|
||||
int timeout_ms);
|
||||
int ReadFromQueue(uint8_t* feat_idx_out, uint8_t* function_out,
|
||||
uint8_t* data_out, size_t data_max,
|
||||
int timeout_ms);
|
||||
|
||||
/*---------------------------------------------------*\
|
||||
| High-level helpers |
|
||||
| SendAndReceive is retained as a thin wrapper |
|
||||
| around SendAcked with the reliable policy, for |
|
||||
| call-site stability. |
|
||||
\*---------------------------------------------------*/
|
||||
int SendAndReceive(uint8_t feat_idx, uint8_t function,
|
||||
const uint8_t* send_data, size_t send_len,
|
||||
uint8_t* recv_data, size_t recv_max);
|
||||
|
||||
/*---------------------------------------------------*\
|
||||
| Unified send-and-ack primitive with retry policy. |
|
||||
| All command paths converge here. Returns: |
|
||||
| >0 : bytes copied into recv_data |
|
||||
| 0 : timeout / BUSY exhaustion |
|
||||
| -1 : non-BUSY HID++ error |
|
||||
| -2 : wire error (SendMessage failed) |
|
||||
| If hidpp20_error_out is non-null and return is -1, |
|
||||
| the HID++ error code is stored there. |
|
||||
\*---------------------------------------------------*/
|
||||
int SendAcked(uint8_t feat_idx, uint8_t function,
|
||||
const uint8_t* send_data, size_t send_len,
|
||||
uint8_t* recv_data, size_t recv_max,
|
||||
const HIDPP20RetryPolicy& policy = HIDPP20_POLICY_RELIABLE,
|
||||
uint8_t* hidpp20_error_out = nullptr);
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Compatibility shim: same as SendAcked but writes |
|
||||
| the response into a blankFAPmessage. Used by |
|
||||
| callers that inherited the SendLong+ReadResponse |
|
||||
| interface and inspect response.data[] downstream. |
|
||||
\*--------------------------------------------------*/
|
||||
int SendAckedIntoFAP(uint8_t feat_idx, uint8_t function,
|
||||
const uint8_t* send_data, size_t send_len,
|
||||
blankFAPmessage& response,
|
||||
const HIDPP20RetryPolicy& policy = HIDPP20_POLICY_RELIABLE);
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Feature discovery |
|
||||
\*-------------------------------------------------*/
|
||||
uint8_t GetFeatureIndex(uint16_t feature_page,
|
||||
const HIDPP20RetryPolicy& policy = HIDPP20_POLICY_RELIABLE);
|
||||
uint8_t GetFeatureVersion(uint16_t feature_page) const;
|
||||
void DiscoverTransport();
|
||||
void DiscoverDeviceName();
|
||||
void DiscoverDeviceType();
|
||||
void EnumerateFeatures(uint8_t feature_set_idx);
|
||||
void DiscoverFirmwareInfo();
|
||||
void DiscoverRGBEffects();
|
||||
void DiscoverEffectCards();
|
||||
void DiscoverHeadsetRGBHostmode();
|
||||
void DiscoverPerKeyZones();
|
||||
void DiscoverKeyboardLayout();
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Power management internals |
|
||||
\*-------------------------------------------------*/
|
||||
void ReaderThreadFunc();
|
||||
void PowerThreadFunc();
|
||||
void DispatchEvent(uint8_t feat, uint8_t func, const uint8_t* data);
|
||||
void OnUserActivity(uint8_t activity_type);
|
||||
void StartDimRamp();
|
||||
void DimRampStep();
|
||||
void StartSleep();
|
||||
void Wake();
|
||||
void ReadFirmwareTimers();
|
||||
void ReadNvSleepRampConfig();
|
||||
void WritePowerConfig(uint16_t idle_s, uint16_t sleep_s);
|
||||
void ReadActiveProfileSector();
|
||||
void ReprobeSubDevice();
|
||||
void ReconnectDevice();
|
||||
void FullReprobe();
|
||||
void RediscoverFeatures();
|
||||
/*----------------------------------------------------*\
|
||||
| Platform-specific. ScanForDevice walks the OS-level |
|
||||
| HID enumeration to find the same physical device on |
|
||||
| a new path (USB<->wireless transitions). Linux uses |
|
||||
| sysfs; Windows and macOS use hidapi + serial_number |
|
||||
| matching. Bodies live in |
|
||||
| LogitechHIDPP20Controller_Linux.cpp and |
|
||||
| LogitechHIDPP20Controller_Windows_MacOS.cpp. |
|
||||
\*----------------------------------------------------*/
|
||||
bool ScanForDevice(bool force = false);
|
||||
|
||||
/*----------------------------------------------------*\
|
||||
| Platform-specific. Returns the friendly name for a |
|
||||
| Centurion sub-device at the given hidapi path, or "" |
|
||||
| if no name is available. Linux reads HID_NAME from |
|
||||
| sysfs; Windows uses hid_device_info::product_string. |
|
||||
\*----------------------------------------------------*/
|
||||
std::string GetCenturionSubDeviceName(const std::string& path);
|
||||
|
||||
void SwapHIDHandle(hid_device* new_dev, const std::string& new_path);
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Reader thread + response queue |
|
||||
\*-------------------------------------------------*/
|
||||
std::thread* reader_thread;
|
||||
std::atomic<bool> reader_running;
|
||||
std::mutex response_mutex;
|
||||
std::condition_variable response_cv;
|
||||
std::deque<HIDPP20RawMessage> response_queue;
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Power thread (state machine + command sender) |
|
||||
\*-------------------------------------------------*/
|
||||
std::thread* power_thread;
|
||||
std::atomic<bool> power_thread_running;
|
||||
std::atomic<int> pending_activity; // -1=none, 0=idle, 1+=active
|
||||
std::atomic<int> pending_connection; // 0=none, +1=connected, -1=disconnected
|
||||
std::atomic<int> pending_path_check; // HID++1.0 DJ notification → force-scan retries remaining (0=idle)
|
||||
std::atomic<bool> device_online; // false when device is unreachable
|
||||
std::atomic<int> consecutive_timeouts; // reset on successful response
|
||||
std::atomic<bool> watcher_mode; // true when retrying failed probe
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Power management state |
|
||||
\*-------------------------------------------------*/
|
||||
HIDPP20PowerState power_state;
|
||||
std::mutex power_mutex;
|
||||
std::atomic<bool> deep_sleep; // true once device stops responding after StartSleep()
|
||||
std::atomic<int> consecutive_frame_end_failures; // FrameEnd BUSY exhaustions while SLEEPING
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Dim ramp state |
|
||||
| dim_brightness_pct is applied by DeviceUpdateLEDs |
|
||||
| to scale colors before pushing to device. |
|
||||
| This is our own host-side animation, independent |
|
||||
| of any firmware dim/sleep timers. |
|
||||
\*-------------------------------------------------*/
|
||||
static const int DIM_STEPS = 25;
|
||||
static const int DIM_INTERVAL_MS = 200;
|
||||
static const int DIM_TARGET_PCT = 50;
|
||||
std::atomic<int> dim_brightness_pct; // 100=full, 50=dimmed
|
||||
int dim_step;
|
||||
std::chrono::steady_clock::time_point next_dim_time;
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Sleep timer |
|
||||
\*-------------------------------------------------*/
|
||||
std::chrono::steady_clock::time_point sleep_deadline;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Last idle-settings re-read timestamp. Drives the |
|
||||
| 500ms poll in PowerThreadFunc that re-reads the |
|
||||
| LogitechHIDPP20IdleSettings JSON key so updates |
|
||||
| from the plugin (or manual edits) apply within |
|
||||
| about half a second without any callback plumbing. |
|
||||
\*--------------------------------------------------*/
|
||||
std::chrono::steady_clock::time_point last_idle_poll;
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Effective idle/sleep timers used by the state |
|
||||
| machine. Populated from the firmware snapshot by |
|
||||
| default, then possibly overridden by profile |
|
||||
| values in ApplyPowerSavingProfile. |
|
||||
\*-------------------------------------------------*/
|
||||
uint16_t idle_timeout_s;
|
||||
uint16_t sleep_timeout_s;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Firmware-configured timer snapshot. Read at init |
|
||||
| (and on reconnect) by ReadFirmwareTimers and |
|
||||
| never overwritten by profile application, so |
|
||||
| that the unconfigured fallback path and transition |
|
||||
| back from a user profile both have a clean set |
|
||||
| of defaults to return to. |
|
||||
\*--------------------------------------------------*/
|
||||
uint16_t fw_idle_timeout_s = 60;
|
||||
uint16_t fw_sleep_timeout_s = 300;
|
||||
uint16_t written_idle_s = 0; // last value written to device RAM (0 = not written yet)
|
||||
uint16_t written_sleep_s = 0;
|
||||
|
||||
/*--------------------------------------------------*\
|
||||
| Host-side idle/dim/sleep state. |
|
||||
| Populated from LogitechHIDPP20IdleSettings on each |
|
||||
| ApplyPowerSavingProfile() invocation. |
|
||||
\*--------------------------------------------------*/
|
||||
bool ps_dim_enabled = false;
|
||||
int ps_dim_target_pct = DIM_TARGET_PCT;
|
||||
bool ps_sleep_enabled = false;
|
||||
bool ps_on_external_power = false;
|
||||
bool ps_last_logged_external = false;
|
||||
int ps_last_logged_pct = -1;
|
||||
int ps_last_logged_idle = -1;
|
||||
int ps_last_logged_sleep = -1;
|
||||
uint16_t last_power_raw = 0xFFFF; // dedup for QueryExternalPower TRACE
|
||||
uint8_t idx_unified_battery = 0;
|
||||
|
||||
void ApplyPowerSavingProfile();
|
||||
bool IsCurrentlyWireless() const;
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Per-key write tracking (per active frame) |
|
||||
| Populated by SendPerKeyData, drained by |
|
||||
| PerKeyFrameEnd. Single-threaded — only the RGB |
|
||||
| controller thread touches per-key state. |
|
||||
\*-------------------------------------------------*/
|
||||
std::vector<OutstandingPerKeyWrite> outstanding_writes;
|
||||
|
||||
/*-------------------------------------------------*\
|
||||
| Callbacks |
|
||||
\*-------------------------------------------------*/
|
||||
std::function<void()> request_repaint_fn;
|
||||
std::function<bool()> reapply_active_mode_fn;
|
||||
std::function<void(RGBController*)> register_controller_fn;
|
||||
};
|
||||
@@ -0,0 +1,433 @@
|
||||
/*---------------------------------------------------------*\
|
||||
| LogitechHIDPP20Controller_Linux.cpp |
|
||||
| |
|
||||
| Linux-specific path-migration and device-name lookup |
|
||||
| for the unified Logitech HID++ 2.0 controller. |
|
||||
| |
|
||||
| Uses sysfs (/sys/class/hidraw) to find the same |
|
||||
| physical device on a new hidraw path after a USB <-> |
|
||||
| wireless transition, and to read Centurion sub-device |
|
||||
| friendly names from HID_NAME uevent fields. |
|
||||
| |
|
||||
| This file is part of the OpenRGB project |
|
||||
| SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
\*---------------------------------------------------------*/
|
||||
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <dirent.h>
|
||||
#include "LogitechHIDPP20Controller.h"
|
||||
#include "LogManager.h"
|
||||
|
||||
#define LOG_TAG log_tag.c_str()
|
||||
|
||||
std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::string& path)
|
||||
{
|
||||
/*--------------------------------------------------------*\
|
||||
| Centurion sub-device friendly name comes from sysfs |
|
||||
| HID_NAME=, same field Solaar reads. The `path` arg is a |
|
||||
| hidraw dev path like /dev/hidraw5; extract the hidrawN |
|
||||
| basename and read /sys/class/hidraw/<basename>/device/ |
|
||||
| uevent. |
|
||||
\*--------------------------------------------------------*/
|
||||
std::string sysfs_name;
|
||||
size_t pos = path.rfind("hidraw");
|
||||
|
||||
if(pos == std::string::npos)
|
||||
{
|
||||
return sysfs_name;
|
||||
}
|
||||
|
||||
std::string uevent_path = "/sys/class/hidraw/" + path.substr(pos) + "/device/uevent";
|
||||
FILE* f = fopen(uevent_path.c_str(), "r");
|
||||
|
||||
if(!f)
|
||||
{
|
||||
return sysfs_name;
|
||||
}
|
||||
|
||||
char line[256];
|
||||
|
||||
while(fgets(line, sizeof(line), f))
|
||||
{
|
||||
if(strncmp(line, "HID_NAME=", 9) == 0)
|
||||
{
|
||||
sysfs_name = line + 9;
|
||||
|
||||
while(!sysfs_name.empty() && (sysfs_name.back() == '\n' || sysfs_name.back() == '\r'))
|
||||
{
|
||||
sysfs_name.pop_back();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return sysfs_name;
|
||||
}
|
||||
|
||||
bool LogitechHIDPP20Controller::ScanForDevice(bool force)
|
||||
{
|
||||
/*---------------------------------------------------------*\
|
||||
| Scan sysfs for a hidraw with matching unitId. Called by |
|
||||
| the power thread either periodically (normal reactive |
|
||||
| mode — only when device_online==false, USB/wireless |
|
||||
| transition or physical unplug) or on demand from the |
|
||||
| reader thread after a HID++1.0 Device Connection |
|
||||
| notification arrives (force=true, bypasses the online |
|
||||
| gate). Finds the device on its new connection path. |
|
||||
| |
|
||||
| Reactive-only: we never migrate while the current path |
|
||||
| still works. On devices like the G502 X PLUS that expose |
|
||||
| both a USB-direct hidraw AND a wireless-via-dongle hidraw |
|
||||
| simultaneously (when paired to the receiver and plugged |
|
||||
| in at the same time), eager migration would bounce us off |
|
||||
| a working path onto one the firmware has actively muted, |
|
||||
| breaking control. The device signals which path is |
|
||||
| active by returning errors/going silent on the inactive |
|
||||
| path — our reader thread picks that up as a hid_read |
|
||||
| failure and flips device_online=false, at which point the |
|
||||
| power thread calls this function to pick the new path. |
|
||||
| |
|
||||
| Linux implementation: walks /sys/class/hidraw and matches |
|
||||
| on HID_UNIQ. The Windows counterpart (same class method, |
|
||||
| different .cpp file) uses hid_enumerate + serial_number. |
|
||||
| |
|
||||
| Match criteria: |
|
||||
| - device_online == false (caller should already ensure) |
|
||||
| - HID_UNIQ matches our unitId |
|
||||
| - Logitech VID (046D) |
|
||||
| - HID++ interface (usage_page 0xFF00, usage 2) |
|
||||
| - Different from our current path (device moved) |
|
||||
\*---------------------------------------------------------*/
|
||||
if(caps.unit_id.empty() || caps.unit_id == "00000000")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Online guard: never migrate off a working path unless |
|
||||
| the caller explicitly forces a re-check. The normal |
|
||||
| periodic scan path stays gated on device_online==false |
|
||||
| so it's a no-op when everything is healthy. |
|
||||
| |
|
||||
| The reader thread bypasses this guard (force=true) after |
|
||||
| seeing a HID++1.0 Device Connection Status notification |
|
||||
| from the Lightspeed receiver, which fires BEFORE the |
|
||||
| firmware fully switches its data flow from wireless to |
|
||||
| USB. At that moment device_online is still true (the |
|
||||
| current path hasn't failed yet), but we want the scan to |
|
||||
| run so we can migrate to the new path proactively. |
|
||||
\*---------------------------------------------------------*/
|
||||
if(!force && device_online.load())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Normalize our unitId to lowercase hex without dashes for |
|
||||
| comparison. Sysfs HID_UNIQ varies between drivers: |
|
||||
| - Lightspeed virtual: "0d-12-5d-47" (dashes) |
|
||||
| - USB direct: "0D125D47" (no dashes) |
|
||||
| We strip dashes and lowercase both sides for matching. |
|
||||
\*---------------------------------------------------------*/
|
||||
std::string target_norm;
|
||||
|
||||
for(char c : caps.unit_id)
|
||||
{
|
||||
if(c != '-')
|
||||
{
|
||||
target_norm += (char)tolower(c);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Read our current PID from sysfs. Only migrate to paths |
|
||||
| with a DIFFERENT PID — same PID means same receiver, |
|
||||
| just a different pairing slot (e.g., stale pairing). |
|
||||
\*---------------------------------------------------------*/
|
||||
unsigned int current_pid = 0;
|
||||
{
|
||||
std::string cur_hidraw = location.substr(location.rfind('/') + 1);
|
||||
std::string cur_uevent = "/sys/class/hidraw/" + cur_hidraw + "/device/uevent";
|
||||
std::ifstream cur_file(cur_uevent);
|
||||
std::string line;
|
||||
|
||||
while(std::getline(cur_file, line))
|
||||
{
|
||||
if(line.compare(0, 7, "HID_ID=") == 0)
|
||||
{
|
||||
size_t lc = line.rfind(':');
|
||||
|
||||
if(lc != std::string::npos)
|
||||
{
|
||||
sscanf(line.c_str() + lc + 1, "%x", ¤t_pid);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------*\
|
||||
| Collect ALL sysfs hidraws with matching unit_id + Logitech |
|
||||
| VID + different PID. With multiple Lightspeed dongles in |
|
||||
| the system, several hidraws can share the same HID_UNIQ: |
|
||||
| one is the live virt-slot on the connected dongle, others |
|
||||
| are stale virt-slots on dongles where our device is not |
|
||||
| actually present. We have to probe each one to find out |
|
||||
| which slot is real — sysfs alone can't tell them apart. |
|
||||
\*----------------------------------------------------------*/
|
||||
struct Candidate
|
||||
{
|
||||
std::string dev_path;
|
||||
unsigned int pid;
|
||||
};
|
||||
|
||||
std::vector<Candidate> candidates;
|
||||
|
||||
DIR* dir = opendir("/sys/class/hidraw");
|
||||
|
||||
if(!dir)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
struct dirent* entry;
|
||||
|
||||
while((entry = readdir(dir)) != nullptr)
|
||||
{
|
||||
if(strncmp(entry->d_name, "hidraw", 6) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string uevent_path = "/sys/class/hidraw/" +
|
||||
std::string(entry->d_name) + "/device/uevent";
|
||||
std::ifstream uevent(uevent_path);
|
||||
|
||||
if(!uevent.is_open())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::string hid_uniq;
|
||||
std::string hid_id;
|
||||
|
||||
while(std::getline(uevent, line))
|
||||
{
|
||||
if(line.compare(0, 9, "HID_UNIQ=") == 0)
|
||||
{
|
||||
hid_uniq = line.substr(9);
|
||||
}
|
||||
else if(line.compare(0, 7, "HID_ID=") == 0)
|
||||
{
|
||||
hid_id = line.substr(7);
|
||||
}
|
||||
}
|
||||
|
||||
std::string uniq_norm;
|
||||
|
||||
for(char c : hid_uniq)
|
||||
{
|
||||
if(c != '-')
|
||||
{
|
||||
uniq_norm += (char)tolower(c);
|
||||
}
|
||||
}
|
||||
|
||||
if(uniq_norm != target_norm)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(hid_id.find("0000046D") == std::string::npos &&
|
||||
hid_id.find("0000046d") == std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string dev_path = "/dev/" + std::string(entry->d_name);
|
||||
|
||||
if(dev_path == location)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t last_colon = hid_id.rfind(':');
|
||||
|
||||
if(last_colon == std::string::npos)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned int pid = 0;
|
||||
sscanf(hid_id.c_str() + last_colon + 1, "%x", &pid);
|
||||
|
||||
if(pid == current_pid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push_back({dev_path, pid});
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
if(candidates.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------*\
|
||||
| Probe each candidate before committing. Two-dongle systems |
|
||||
| can expose multiple sysfs hidraws with the same HID_UNIQ: |
|
||||
| one is the live slot, others are stale pairings that |
|
||||
| respond with short-format UNKNOWN_DEVICE errors. Issue a |
|
||||
| cheap IRoot GetFeature (feat 0x0001) to distinguish them. |
|
||||
| A live slot returns a long-form response with feat_idx=0 |
|
||||
| and feat_byte matching the sub-index we asked for. A stale |
|
||||
| slot returns r[10 xx 8F 00 00 08 ...] (short error, code |
|
||||
| 0x08 UNKNOWN_DEVICE). We accept the first candidate that |
|
||||
| passes; the overall pending_path_check retry loop will |
|
||||
| naturally re-probe all candidates on subsequent scans if |
|
||||
| none pass on the current pass (e.g., mid-transition). |
|
||||
\*----------------------------------------------------------*/
|
||||
std::string found_path;
|
||||
hid_device* found_dev = nullptr;
|
||||
|
||||
for(size_t c = 0; c < candidates.size(); c++)
|
||||
{
|
||||
const Candidate& cand = candidates[c];
|
||||
|
||||
LOG_DEBUG("%s Scan: migration candidate at %s (pid=0x%04X, current=%s pid=0x%04X)",
|
||||
LOG_TAG, cand.dev_path.c_str(), cand.pid,
|
||||
location.c_str(), current_pid);
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Multi-dongle caveat: hid_enumerate returns entries |
|
||||
| for ALL dongles with this PID (046D:4099 Lightspeed |
|
||||
| can appear several times in a single machine). Match |
|
||||
| strictly on the sysfs candidate's dev_path so each |
|
||||
| candidate resolves to its OWN dongle's HID++ |
|
||||
| interface — not the first match we stumble across. |
|
||||
\*-----------------------------------------------------*/
|
||||
hid_device_info* devs = hid_enumerate(0x046D, (uint16_t)cand.pid);
|
||||
std::string hidpp20_path;
|
||||
|
||||
for(hid_device_info* d = devs; d != nullptr; d = d->next)
|
||||
{
|
||||
LOG_TRACE("%s Scan: enumerate PID=0x%04X path=%s page=0x%04X usage=%d",
|
||||
LOG_TAG, cand.pid, d->path, d->usage_page, d->usage);
|
||||
|
||||
if(std::string(d->path) == cand.dev_path &&
|
||||
d->usage_page == 0xFF00 && d->usage == 2 &&
|
||||
std::string(d->path) != location)
|
||||
{
|
||||
hidpp20_path = d->path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hid_free_enumeration(devs);
|
||||
|
||||
if(hidpp20_path.empty())
|
||||
{
|
||||
LOG_DEBUG("%s Scan: %s no matching LogitechHID++ interface in enum",
|
||||
LOG_TAG, cand.dev_path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
hid_device* test_dev = hid_open_path(hidpp20_path.c_str());
|
||||
|
||||
if(!test_dev)
|
||||
{
|
||||
LOG_DEBUG("%s Scan: %s failed to open", LOG_TAG, hidpp20_path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
/*------------------------------------------------------*\
|
||||
| Probe: HID++2.0 IRoot GetFeature for feat 0x0001 |
|
||||
| (IFeatureSet). Wire: w[10 FF 0000 000100] |
|
||||
| - report_id 0x10 (short) |
|
||||
| - device_index 0xFF |
|
||||
| - feat_idx 0x00 (IRoot) |
|
||||
| - address 0x00 (func 0 GetFeature, sw_id 0) |
|
||||
| - payload 00 01 00 (feat_id 0x0001) |
|
||||
| |
|
||||
| A live device returns r[11 xx 00 0X ...] — long form, |
|
||||
| feat_idx 0x00, the address byte we sent back, and the |
|
||||
| feature index in the payload. |
|
||||
| A stale slot returns r[10 xx 8F 00 00 08 ...] — short |
|
||||
| error form. Reject anything that starts with 0x10. |
|
||||
\*------------------------------------------------------*/
|
||||
uint8_t probe[7] = {0x10, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00};
|
||||
uint8_t reply[20] = {};
|
||||
int write_rc = hid_write(test_dev, probe, sizeof(probe));
|
||||
bool probe_ok = false;
|
||||
|
||||
if(write_rc >= 0)
|
||||
{
|
||||
/*-----------------------------------------------------*\
|
||||
| Drain up to ~100ms, looking for a long-form response |
|
||||
| matching our probe. Skip stray reports from unrelated |
|
||||
| firmware events that might be queued on the hidraw. |
|
||||
\*-----------------------------------------------------*/
|
||||
std::chrono::steady_clock::time_point deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::milliseconds(100);
|
||||
|
||||
while(std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
int read_rc = hid_read_timeout(test_dev, reply, sizeof(reply), 50);
|
||||
|
||||
if(read_rc <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(reply[0] == 0x11 && reply[2] == 0x00 && reply[3] == 0x00)
|
||||
{
|
||||
probe_ok = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(reply[0] == 0x10 && reply[2] == 0x8F)
|
||||
{
|
||||
LOG_DEBUG("%s Scan: %s probe rejected — err=0x%02X (stale slot)",
|
||||
LOG_TAG, hidpp20_path.c_str(), reply[5]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!probe_ok)
|
||||
{
|
||||
hid_close(test_dev);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s Scan: %s probe accepted (feat_idx=0x%02X)",
|
||||
LOG_TAG, hidpp20_path.c_str(), reply[4]);
|
||||
|
||||
found_path = hidpp20_path;
|
||||
found_dev = test_dev;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!found_dev)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s Device migrated: %s -> %s", LOG_TAG, location.c_str(), found_path.c_str());
|
||||
|
||||
SwapHIDHandle(found_dev, found_path);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*---------------------------------------------------------*\
|
||||
| LogitechHIDPP20Controller_Windows_MacOS.cpp |
|
||||
| |
|
||||
| Path-migration and device-name lookup for the unified |
|
||||
| Logitech HID++ 2.0 controller on Windows and macOS. |
|
||||
| |
|
||||
| Uses hidapi's hid_enumerate + hid_device_info fields |
|
||||
| (serial_number, product_string, product_id, usage_page) |
|
||||
| on platforms with no /sys/class/hidraw equivalent. |
|
||||
| |
|
||||
| This file is part of the OpenRGB project |
|
||||
| SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
\*---------------------------------------------------------*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "LogitechHIDPP20Controller.h"
|
||||
#include "LogManager.h"
|
||||
#include "StringUtils.h"
|
||||
|
||||
#define LOG_TAG log_tag.c_str()
|
||||
|
||||
std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::string& path)
|
||||
{
|
||||
/*---------------------------------------------------------*\
|
||||
| On Windows, hidapi's hid_device_info carries a |
|
||||
| product_string field (wchar_t*). Enumerate all Logitech |
|
||||
| devices and find the one whose path matches `path`. |
|
||||
| |
|
||||
| Caveat: Windows hidapi typically returns the parent |
|
||||
| product string on every (interface, usage_page, usage) |
|
||||
| entry that maps to the same USB device, so Centurion |
|
||||
| sub-devices may share a name with the parent dongle. |
|
||||
| That's a less specific name than Linux's HID_NAME, but |
|
||||
| still better than "Logitech Centurion Device". |
|
||||
\*---------------------------------------------------------*/
|
||||
std::string friendly;
|
||||
|
||||
hid_device_info* devs = hid_enumerate(0x046D, 0x0000);
|
||||
|
||||
for(hid_device_info* d = devs; d != nullptr; d = d->next)
|
||||
{
|
||||
if(d->path == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(std::string(d->path) != path)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
friendly = StringUtils::wchar_to_string(d->product_string);
|
||||
break;
|
||||
}
|
||||
|
||||
hid_free_enumeration(devs);
|
||||
return friendly;
|
||||
}
|
||||
|
||||
bool LogitechHIDPP20Controller::ScanForDevice(bool force)
|
||||
{
|
||||
/*---------------------------------------------------------*\
|
||||
| Scan hidapi for a Logitech device with matching unitId. |
|
||||
| Called by the power thread either periodically (normal |
|
||||
| reactive mode — only when device_online==false, USB/ |
|
||||
| wireless transition or physical unplug) or on demand |
|
||||
| from the reader thread after a HID++1.0 Device |
|
||||
| Connection notification arrives (force=true, bypasses |
|
||||
| the online gate). Finds the device on its new |
|
||||
| connection path. |
|
||||
| |
|
||||
| Reactive-only: we never migrate while the current path |
|
||||
| still works (same rationale as Linux — see the Linux |
|
||||
| companion file for the full explanation). |
|
||||
| |
|
||||
| Windows implementation: walks hid_enumerate(046D, *) and |
|
||||
| matches on hid_device_info::serial_number, which for |
|
||||
| Logitech devices generally corresponds to the same |
|
||||
| stable identity as the HID++-reported unit_id. If |
|
||||
| serial_number matching yields nothing (hidapi may not |
|
||||
| populate it for some Logitech devices on Windows), we |
|
||||
| fall through to IRoot probing every candidate on the |
|
||||
| matching usage_page/usage so we can still find the |
|
||||
| device albeit more slowly. |
|
||||
\*---------------------------------------------------------*/
|
||||
if(caps.unit_id.empty() || caps.unit_id == "00000000")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!force && device_online.load())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string target_norm = StringUtils::normalize_hex_id(caps.unit_id);
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Candidate = {path, pid, has_serial_match}. Serial matches |
|
||||
| sort first so we try the cheapest/most-likely candidate. |
|
||||
\*---------------------------------------------------------*/
|
||||
struct Candidate
|
||||
{
|
||||
std::string dev_path;
|
||||
unsigned int pid;
|
||||
bool serial_match;
|
||||
};
|
||||
|
||||
std::vector<Candidate> candidates;
|
||||
unsigned int current_pid = 0;
|
||||
|
||||
hid_device_info* devs = hid_enumerate(0x046D, 0x0000);
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| First pass: find the entry matching our current path and |
|
||||
| record its product_id so we can skip same-PID candidates. |
|
||||
\*---------------------------------------------------------*/
|
||||
for(hid_device_info* d = devs; d != nullptr; d = d->next)
|
||||
{
|
||||
if(d->path != nullptr && std::string(d->path) == location)
|
||||
{
|
||||
current_pid = d->product_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Second pass: collect candidates that (a) aren't our |
|
||||
| current path, (b) speak the HID++ interface, and (c) have |
|
||||
| a plausible identity match — either the serial_number |
|
||||
| matches our unit_id, or at minimum their PID differs from |
|
||||
| ours so they can't be another slot on the same dongle. |
|
||||
\*---------------------------------------------------------*/
|
||||
for(hid_device_info* d = devs; d != nullptr; d = d->next)
|
||||
{
|
||||
if(d->path == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(std::string(d->path) == location)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Only HID++ interface (usage_page 0xFF00, usage 2). |
|
||||
| Note: on Windows hidapi may or may not populate |
|
||||
| usage / usage_page consistently. If either is zero, |
|
||||
| fall through — we'll probe unconditionally in that |
|
||||
| case rather than dropping a potential candidate. |
|
||||
\*-----------------------------------------------------*/
|
||||
bool usage_known = (d->usage_page != 0 || d->usage != 0);
|
||||
|
||||
if(usage_known && (d->usage_page != 0xFF00 || d->usage != 2))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| Compare serial_number (wchar_t*) against our unit_id. |
|
||||
| Normalize both and do an exact-match comparison. |
|
||||
| Empty/missing serial is allowed — falls through as a |
|
||||
| serial_match=false candidate so the probe path can |
|
||||
| still find it via a different-PID filter. |
|
||||
\*-----------------------------------------------------*/
|
||||
bool serial_match = false;
|
||||
|
||||
if(d->serial_number != nullptr && d->serial_number[0] != L'\0')
|
||||
{
|
||||
std::string sn = StringUtils::wchar_to_string(d->serial_number);
|
||||
std::string sn_norm = StringUtils::normalize_hex_id(sn);
|
||||
|
||||
if(!sn_norm.empty() && sn_norm == target_norm)
|
||||
{
|
||||
serial_match = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------------------------------------------------*\
|
||||
| If we have no serial match AND this path shares the |
|
||||
| current PID, skip. Same PID with no identity evidence |
|
||||
| is probably a sibling slot on the same dongle, not a |
|
||||
| valid migration target. |
|
||||
\*-----------------------------------------------------*/
|
||||
if(!serial_match && d->product_id == current_pid && current_pid != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Candidate c;
|
||||
c.dev_path = d->path;
|
||||
c.pid = d->product_id;
|
||||
c.serial_match = serial_match;
|
||||
candidates.push_back(c);
|
||||
}
|
||||
|
||||
hid_free_enumeration(devs);
|
||||
|
||||
if(candidates.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Sort so serial-matched candidates get probed first. |
|
||||
\*---------------------------------------------------------*/
|
||||
std::stable_sort(candidates.begin(), candidates.end(),
|
||||
[](const Candidate& a, const Candidate& b)
|
||||
{
|
||||
return a.serial_match && !b.serial_match;
|
||||
});
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Probe each candidate with IRoot GetFeature feat 0x0001. |
|
||||
| A live slot returns a long-form (0x11) response; a stale |
|
||||
| slot returns a short-form (0x10) error. See the Linux |
|
||||
| companion file for the wire-level explanation. |
|
||||
\*---------------------------------------------------------*/
|
||||
std::string found_path;
|
||||
hid_device* found_dev = nullptr;
|
||||
|
||||
for(size_t c = 0; c < candidates.size(); c++)
|
||||
{
|
||||
const Candidate& cand = candidates[c];
|
||||
|
||||
LOG_DEBUG("%s Scan: migration candidate at %s (pid=0x%04X, serial_match=%d)",
|
||||
LOG_TAG, cand.dev_path.c_str(), cand.pid,
|
||||
cand.serial_match ? 1 : 0);
|
||||
|
||||
hid_device* test_dev = hid_open_path(cand.dev_path.c_str());
|
||||
|
||||
if(!test_dev)
|
||||
{
|
||||
LOG_DEBUG("%s Scan: %s failed to open",
|
||||
LOG_TAG, cand.dev_path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t probe[7] = {0x10, 0xFF, 0x00, 0x00, 0x00, 0x01, 0x00};
|
||||
uint8_t reply[20] = {};
|
||||
int write_rc = hid_write(test_dev, probe, sizeof(probe));
|
||||
bool probe_ok = false;
|
||||
|
||||
if(write_rc >= 0)
|
||||
{
|
||||
std::chrono::steady_clock::time_point deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::milliseconds(100);
|
||||
|
||||
while(std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
int read_rc = hid_read_timeout(test_dev, reply, sizeof(reply), 50);
|
||||
|
||||
if(read_rc <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(reply[0] == 0x11 && reply[2] == 0x00 && reply[3] == 0x00)
|
||||
{
|
||||
probe_ok = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if(reply[0] == 0x10 && reply[2] == 0x8F)
|
||||
{
|
||||
LOG_DEBUG("%s Scan: %s probe rejected — err=0x%02X (stale slot)",
|
||||
LOG_TAG, cand.dev_path.c_str(), reply[5]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!probe_ok)
|
||||
{
|
||||
hid_close(test_dev);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_DEBUG("%s Scan: %s probe accepted (feat_idx=0x%02X)",
|
||||
LOG_TAG, cand.dev_path.c_str(), reply[4]);
|
||||
|
||||
found_path = cand.dev_path;
|
||||
found_dev = test_dev;
|
||||
break;
|
||||
}
|
||||
|
||||
if(!found_dev)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("%s Device migrated: %s -> %s",
|
||||
LOG_TAG, location.c_str(), found_path.c_str());
|
||||
|
||||
SwapHIDHandle(found_dev, found_path);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------*\
|
||||
| LogitechHIDPP20IdleSettings.cpp |
|
||||
| |
|
||||
| Host-side idle/dim/sleep settings storage helper for |
|
||||
| Logitech HID++ 2.0 devices. Loads the configuration |
|
||||
| block from SettingsManager JSON, caches it, and writes |
|
||||
| changes back through SettingsManager::SetSettings(). |
|
||||
| |
|
||||
| This file is part of the OpenRGB project |
|
||||
| SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
\*---------------------------------------------------------*/
|
||||
|
||||
#include "LogitechHIDPP20IdleSettings.h"
|
||||
#include "ResourceManager.h"
|
||||
#include "SettingsManager.h"
|
||||
|
||||
static const char* SETTINGS_KEY = "LogitechHIDPP20IdleSettings";
|
||||
|
||||
LogitechHIDPP20IdleSettings* LogitechHIDPP20IdleSettings::instance()
|
||||
{
|
||||
static LogitechHIDPP20IdleSettings inst;
|
||||
return &inst;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Minimum idle / sleep values the controller will honor. |
|
||||
| Matches what Logitech firmware typically clamps to on |
|
||||
| HID++ 2.0 devices, and guarantees the skip-dim path's |
|
||||
| sleep_delay math always produces a positive window. |
|
||||
\*---------------------------------------------------------*/
|
||||
static const int MIN_IDLE_S = 15;
|
||||
static const int MIN_SLEEP_S = 45;
|
||||
static const int MIN_DIM_WINDOW = 30; // sleep must exceed idle by at least this
|
||||
|
||||
static void ClampProfile(LogitechHIDPP20IdleProfile& p)
|
||||
{
|
||||
if(p.dim_brightness < 0) p.dim_brightness = 0;
|
||||
if(p.dim_brightness > 100) p.dim_brightness = 100;
|
||||
|
||||
if(p.idle_timeout_s < MIN_IDLE_S)
|
||||
{
|
||||
p.idle_timeout_s = MIN_IDLE_S;
|
||||
}
|
||||
|
||||
if(p.sleep_timeout_s < MIN_SLEEP_S)
|
||||
{
|
||||
p.sleep_timeout_s = MIN_SLEEP_S;
|
||||
}
|
||||
|
||||
if(p.sleep_timeout_s < p.idle_timeout_s + MIN_DIM_WINDOW)
|
||||
{
|
||||
p.sleep_timeout_s = p.idle_timeout_s + MIN_DIM_WINDOW;
|
||||
}
|
||||
}
|
||||
|
||||
static LogitechHIDPP20IdleProfile ProfileFromJson(const json& j)
|
||||
{
|
||||
LogitechHIDPP20IdleProfile p;
|
||||
|
||||
if(j.contains("dim_when_idle")) p.dim_when_idle = j["dim_when_idle"];
|
||||
if(j.contains("dim_brightness")) p.dim_brightness = j["dim_brightness"];
|
||||
if(j.contains("idle_timeout_s")) p.idle_timeout_s = j["idle_timeout_s"];
|
||||
if(j.contains("allow_sleep")) p.allow_sleep = j["allow_sleep"];
|
||||
if(j.contains("sleep_timeout_s")) p.sleep_timeout_s = j["sleep_timeout_s"];
|
||||
|
||||
ClampProfile(p);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
static json ProfileToJson(const LogitechHIDPP20IdleProfile& p)
|
||||
{
|
||||
json j;
|
||||
|
||||
j["dim_when_idle"] = p.dim_when_idle;
|
||||
j["dim_brightness"] = p.dim_brightness;
|
||||
j["idle_timeout_s"] = p.idle_timeout_s;
|
||||
j["allow_sleep"] = p.allow_sleep;
|
||||
j["sleep_timeout_s"] = p.sleep_timeout_s;
|
||||
|
||||
return j;
|
||||
}
|
||||
|
||||
void LogitechHIDPP20IdleSettings::load()
|
||||
{
|
||||
json settings = ResourceManager::get()->GetSettingsManager()->GetSettings(SETTINGS_KEY);
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Empty / missing key means the plugin is not in use. |
|
||||
| Reset both profiles to defaults with configured=false so |
|
||||
| the controller defers to firmware. |
|
||||
\*---------------------------------------------------------*/
|
||||
if(!settings.is_object() || settings.empty())
|
||||
{
|
||||
configured = false;
|
||||
on_battery = LogitechHIDPP20IdleProfile{};
|
||||
plugged_in = LogitechHIDPP20IdleProfile{};
|
||||
return;
|
||||
}
|
||||
|
||||
configured = true;
|
||||
|
||||
if(settings.contains("on_battery"))
|
||||
{
|
||||
on_battery = ProfileFromJson(settings["on_battery"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
on_battery = LogitechHIDPP20IdleProfile{};
|
||||
}
|
||||
|
||||
if(settings.contains("plugged_in"))
|
||||
{
|
||||
plugged_in = ProfileFromJson(settings["plugged_in"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
plugged_in = LogitechHIDPP20IdleProfile{};
|
||||
}
|
||||
}
|
||||
|
||||
void LogitechHIDPP20IdleSettings::save()
|
||||
{
|
||||
json settings;
|
||||
|
||||
settings["on_battery"] = ProfileToJson(on_battery);
|
||||
settings["plugged_in"] = ProfileToJson(plugged_in);
|
||||
|
||||
SettingsManager* mgr = ResourceManager::get()->GetSettingsManager();
|
||||
mgr->SetSettings(SETTINGS_KEY, settings);
|
||||
mgr->SaveSettings();
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void LogitechHIDPP20IdleSettings::setOnBattery(const LogitechHIDPP20IdleProfile& p)
|
||||
{
|
||||
on_battery = p;
|
||||
configured = true;
|
||||
}
|
||||
|
||||
void LogitechHIDPP20IdleSettings::setPluggedIn(const LogitechHIDPP20IdleProfile& p)
|
||||
{
|
||||
plugged_in = p;
|
||||
configured = true;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*---------------------------------------------------------*\
|
||||
| LogitechHIDPP20IdleSettings.h |
|
||||
| |
|
||||
| Host-side idle/dim/sleep configuration for Logitech |
|
||||
| HID++ 2.0 devices. Two profiles (on_battery, plugged_in)|
|
||||
| selected at runtime based on the device's external- |
|
||||
| power flag. `configured == false` means the JSON key |
|
||||
| is absent entirely — the controller defers to firmware. |
|
||||
| Qt-free so the controller can consume it directly. |
|
||||
| |
|
||||
| This file is part of the OpenRGB project |
|
||||
| SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
\*---------------------------------------------------------*/
|
||||
|
||||
#pragma once
|
||||
|
||||
struct LogitechHIDPP20IdleProfile
|
||||
{
|
||||
bool dim_when_idle = false;
|
||||
int dim_brightness = 50;
|
||||
int idle_timeout_s = 60;
|
||||
bool allow_sleep = false;
|
||||
int sleep_timeout_s = 300;
|
||||
};
|
||||
|
||||
class LogitechHIDPP20IdleSettings
|
||||
{
|
||||
public:
|
||||
static LogitechHIDPP20IdleSettings* instance();
|
||||
|
||||
void load();
|
||||
void save();
|
||||
|
||||
bool isConfigured() const { return configured; }
|
||||
const LogitechHIDPP20IdleProfile& onBattery() const { return on_battery; }
|
||||
const LogitechHIDPP20IdleProfile& pluggedIn() const { return plugged_in; }
|
||||
|
||||
void setOnBattery(const LogitechHIDPP20IdleProfile& p);
|
||||
void setPluggedIn(const LogitechHIDPP20IdleProfile& p);
|
||||
|
||||
private:
|
||||
LogitechHIDPP20IdleSettings() = default;
|
||||
|
||||
bool configured = false;
|
||||
LogitechHIDPP20IdleProfile on_battery;
|
||||
LogitechHIDPP20IdleProfile plugged_in;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------*\
|
||||
| RGBController_LogitechHIDPP20.h |
|
||||
| |
|
||||
| RGBController for unified Logitech HID++ 2.0 devices |
|
||||
| |
|
||||
| This file is part of the OpenRGB project |
|
||||
| SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
\*---------------------------------------------------------*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RGBController.h"
|
||||
#include "LogitechHIDPP20Controller.h"
|
||||
|
||||
class RGBController_LogitechHIDPP20 : public RGBController
|
||||
{
|
||||
public:
|
||||
RGBController_LogitechHIDPP20(LogitechHIDPP20Controller* controller_ptr);
|
||||
~RGBController_LogitechHIDPP20();
|
||||
|
||||
void SetupZones();
|
||||
void ResizeZone(int zone, int new_size);
|
||||
void DeviceUpdateLEDs();
|
||||
void UpdateZoneLEDs(int zone);
|
||||
void UpdateSingleLED(int led);
|
||||
void DeviceUpdateMode();
|
||||
void DeviceSaveMode();
|
||||
bool ReapplyActiveMode();
|
||||
|
||||
private:
|
||||
LogitechHIDPP20Controller* controller;
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Repaint callback handler. Registered with the controller |
|
||||
| as request_repaint_fn and invoked from the power thread |
|
||||
| for dim/wake when no animation is driving updates. |
|
||||
\*---------------------------------------------------------*/
|
||||
void OnRepaintRequest();
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| When true, the next DeviceUpdateMode cycle sends its |
|
||||
| SetZoneEffect calls with persist=true instead of the |
|
||||
| default ephemeral write. Used by DeviceSaveMode to replay |
|
||||
| the active mode as a NVM-committed effect on 0x8070 |
|
||||
| devices, which default to non-persistent live writes. |
|
||||
\*---------------------------------------------------------*/
|
||||
bool save_pending = false;
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Maps OpenRGB LED index -> HID++ per-key zone ID |
|
||||
\*---------------------------------------------------------*/
|
||||
std::vector<uint16_t> led_to_zone_id;
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Reverse map: zone_id -> LED index (-1 if no LED). |
|
||||
| Indexed 0..255 (zone IDs are bytes). Built once in |
|
||||
| SetupZones to avoid scanning led_to_zone_id at commit |
|
||||
| time, which would be O(N) per acked zone. |
|
||||
\*---------------------------------------------------------*/
|
||||
std::vector<int> zone_id_to_led_idx;
|
||||
|
||||
/*---------------------------------------------------------*\
|
||||
| Last successfully committed colors for delta updates. |
|
||||
| An entry of HIDPP20_UNCOMMITTED (0xFF000000) marks an LED |
|
||||
| whose last write didn't ACK and which therefore needs to |
|
||||
| be re-pushed in the next frame regardless of color delta. |
|
||||
| The high byte (0xFF) is impossible for any value produced |
|
||||
| by ToRGBColor() so it never collides with a real color. |
|
||||
\*---------------------------------------------------------*/
|
||||
std::vector<RGBColor> sent_colors;
|
||||
uint32_t last_init_gen = 0;
|
||||
};
|
||||
@@ -19,6 +19,7 @@
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
|
||||
#include <cctype>
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
#include <string>
|
||||
@@ -71,6 +72,16 @@ const char* StringUtils::wchar_to_char(const wchar_t* pwchar)
|
||||
return(filePathC);
|
||||
}
|
||||
|
||||
std::string StringUtils::wchar_to_string(const wchar_t* pwchar)
|
||||
{
|
||||
if(pwchar == nullptr)
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
|
||||
return wstring_to_string(std::wstring(pwchar));
|
||||
}
|
||||
|
||||
std::string StringUtils::wstring_to_string(const std::wstring wstring)
|
||||
{
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
|
||||
@@ -100,4 +111,20 @@ std::string StringUtils::u32int_to_hexString(unsigned int value)
|
||||
char hex_str[20] = {0};
|
||||
snprintf(hex_str, sizeof(hex_str), "%X", value);
|
||||
return std::string(hex_str);
|
||||
}
|
||||
}
|
||||
|
||||
std::string StringUtils::normalize_hex_id(const std::string& id)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(id.size());
|
||||
|
||||
for(char c : id)
|
||||
{
|
||||
if(c != '-')
|
||||
{
|
||||
out += (char)tolower((unsigned char)c);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ class StringUtils
|
||||
{
|
||||
public:
|
||||
static const char* wchar_to_char(const wchar_t* pwchar);
|
||||
static std::string wchar_to_string(const wchar_t* pwchar);
|
||||
static std::string wstring_to_string(const std::wstring wstring);
|
||||
static std::string u16string_to_string(const std::u16string wstring);
|
||||
static const std::string remove_null_terminating_chars(std::string input);
|
||||
static std::string u32int_to_hexString(unsigned int value);
|
||||
static std::string normalize_hex_id(const std::string& id);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user