Logitech HID++ 2.0: VID-generic detection, protocol fixes and device support

This commit is contained in:
Ken Sanislo
2026-08-16 23:27:46 -05:00
committed by Adam Honse
parent ae22e1e8ea
commit c852af2a2c
18 changed files with 8518 additions and 4569 deletions
File diff suppressed because it is too large Load Diff
@@ -1,154 +0,0 @@
/*---------------------------------------------------------*\
| LogitechG560Controller.cpp |
| |
| Driver for Logitech G560 |
| |
| Cheerpipe 28 Oct 2020 |
| based on TheRogueZeta 31 Aug 2020 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstring>
#include <stdio.h>
#include <stdlib.h>
#include "LogitechG560Controller.h"
using namespace std::chrono_literals;
LogitechG560Controller::LogitechG560Controller(hid_device* dev_handle, const char* path, std::string dev_name)
{
dev = dev_handle;
location = path;
name = dev_name;
}
LogitechG560Controller::~LogitechG560Controller()
{
hid_close(dev);
}
std::string LogitechG560Controller::GetDeviceLocation()
{
return("HID: " + location);
}
std::string LogitechG560Controller::GetDeviceName()
{
return(name);
}
void LogitechG560Controller::SetDirectMode(uint8_t zone)
{
unsigned char usb_buf[LOGI_G560_LED_PACKET_SIZE];
usb_buf[0x00] = 0x11;
usb_buf[0x01] = 0xFF;
usb_buf[0x02] = 0x04;
usb_buf[0x03] = 0xCA;
usb_buf[0x04] = zone;
/*-----------------------------------------------------*\
| Send packet |
\*-----------------------------------------------------*/
fail_retry_write(dev, usb_buf, LOGI_G560_LED_PACKET_SIZE);
}
void LogitechG560Controller::SetOffMode(uint8_t zone)
{
unsigned char usb_buf[LOGI_G560_LED_PACKET_SIZE];
usb_buf[0x00] = 0x11;
usb_buf[0x01] = 0xFF;
usb_buf[0x02] = 0x04;
usb_buf[0x03] = 0x3F;
usb_buf[0x04] = zone;
/*-----------------------------------------------------*\
| Send packet |
\*-----------------------------------------------------*/
fail_retry_write(dev, usb_buf, LOGI_G560_LED_PACKET_SIZE);
}
void LogitechG560Controller::SendSpeakerMode
(
unsigned char zone,
unsigned char mode,
unsigned char red,
unsigned char green,
unsigned char blue
)
{
unsigned char usb_buf[LOGI_G560_LED_PACKET_SIZE];
/*-----------------------------------------------------*\
| Zero out buffer |
\*-----------------------------------------------------*/
memset(usb_buf, 0x00, sizeof(usb_buf));
/*-----------------------------------------------------*\
| Set up Lighting Control |
\*-----------------------------------------------------*/
usb_buf[0x00] = 0x11;
usb_buf[0x01] = 0xFF;
usb_buf[0x02] = 0x04;
/*-----------------------------------------------------*\
| This packet sets speaker into direct mode. This mode |
| is used by Lightsync Ambilight and Music Visualizer |
| realtime effect. |
\*-----------------------------------------------------*/
usb_buf[0x03] = 0x3A;
/*-----------------------------------------------------*\
| Set up mode and speed |
\*-----------------------------------------------------*/
usb_buf[0x04] = zone;
usb_buf[0x05] = mode;
/*-----------------------------------------------------*\
| And set up the colors |
\*-----------------------------------------------------*/
usb_buf[0x06] = red;
usb_buf[0x07] = green;
usb_buf[0x08] = blue;
if(mode == LOGITECH_G560_MODE_DIRECT) //G560 only has Direct Mode.
{
usb_buf[0x09] = 0x02;
}
/*-----------------------------------------------------*\
| Send packet |
\*-----------------------------------------------------*/
fail_retry_write(dev, usb_buf, LOGI_G560_LED_PACKET_SIZE);
}
void LogitechG560Controller::fail_retry_write(hid_device *device, const unsigned char *data, size_t length)
{
unsigned char usb_buf_out[LOGI_G560_LED_PACKET_SIZE];
unsigned int write_max_retry = LOGI_G560_LED_COMMAND_SEND_RETRIES;
do
{
std::this_thread::sleep_for(1ms);
int ret = hid_write(device, data, length);
/*-------------------------------------------------------------------------------------*\
| HID write fails if a change led color and set volume command are sent at |
| the same time because RGB controller and volume control shares the same interface. |
\*-------------------------------------------------------------------------------------*/
if(ret == 20)
{
std::this_thread::sleep_for(1ms);
hid_read_timeout(dev, usb_buf_out, LOGI_G560_LED_PACKET_SIZE, 20);
break;
}
else
{
write_max_retry--;
std::this_thread::sleep_for(10ms);
}
}while (write_max_retry > 0);
}
@@ -1,59 +0,0 @@
/*---------------------------------------------------------*\
| LogitechG560Controller.h |
| |
| Driver for Logitech G560 |
| |
| Cheerpipe 28 Oct 2020 |
| based on TheRogueZeta 31 Aug 2020 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <hidapi.h>
#include "RGBController.h"
#define LOGI_G560_LED_PACKET_SIZE 20
#define LOGI_G560_LED_COMMAND_SEND_RETRIES 3
enum
{
LOGITECH_G560_MODE_OFF = 0x00,
LOGITECH_G560_MODE_DIRECT = 0x01,
LOGITECH_G560_MODE_CYCLE = 0x02,
LOGITECH_G560_MODE_BREATHING = 0x03,
};
class LogitechG560Controller
{
public:
LogitechG560Controller(hid_device* dev_handle, const char* path, std::string dev_name);
~LogitechG560Controller();
std::string GetDeviceLocation();
std::string GetDeviceName();
void SetDirectMode(uint8_t zone);
void SetOffMode(uint8_t zone);
void SendSpeakerMode
(
unsigned char zone,
unsigned char mode,
unsigned char red,
unsigned char green,
unsigned char blue
);
private:
hid_device* dev;
std::string location;
std::string name;
void fail_retry_write(hid_device *device, const unsigned char *data, size_t length);
};
@@ -1,160 +0,0 @@
/*---------------------------------------------------------*\
| RGBController_LogitechG560.cpp |
| |
| RGBController for Logitech G560 |
| |
| Cheerpipe 28 Oct 2020 |
| based on TheRogueZeta 31 Aug 2020 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "RGBController_LogitechG560.h"
/**------------------------------------------------------------------*\
@name Logitech G560
@category Speaker
@type USB
@save :x:
@direct :white_check_mark:
@effects :x:
@detectors DetectLogitechG560
@comment
\*-------------------------------------------------------------------*/
RGBController_LogitechG560::RGBController_LogitechG560(LogitechG560Controller* controller_ptr)
{
controller = controller_ptr;
name = controller->GetDeviceName();
vendor = "Logitech";
type = DEVICE_TYPE_SPEAKER;
description = "Logitech G560 Lightsync Speaker";
location = controller->GetDeviceLocation();
mode Off;
Off.name = "Off";
Off.value = LOGITECH_G560_MODE_OFF;
Off.flags = 0;
Off.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Off);
mode Direct;
Direct.name = "Direct";
Direct.value = LOGITECH_G560_MODE_DIRECT;
Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Direct.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Direct);
SetupZones();
}
RGBController_LogitechG560::~RGBController_LogitechG560()
{
Shutdown();
delete controller;
}
void RGBController_LogitechG560::SetupZones()
{
zone G560_left_front;
G560_left_front.name = "Left Front";
G560_left_front.type = ZONE_TYPE_SINGLE;
G560_left_front.leds_min = 1;
G560_left_front.leds_max = 1;
G560_left_front.leds_count = 1;
zones.push_back(G560_left_front);
led G560_left_front_led;
G560_left_front_led.name = "Left Front";
G560_left_front_led.value = 0x00;
leds.push_back(G560_left_front_led);
zone G560_right_front;
G560_right_front.name = "Right Front";
G560_right_front.type = ZONE_TYPE_SINGLE;
G560_right_front.leds_min = 1;
G560_right_front.leds_max = 1;
G560_right_front.leds_count = 1;
zones.push_back(G560_right_front);
led G560_right_front_led;
G560_right_front_led.name = "Right Front";
G560_right_front_led.value = 0x01;
leds.push_back(G560_right_front_led);
zone G560_left_rear;
G560_left_rear.name = "Left Rear";
G560_left_rear.type = ZONE_TYPE_SINGLE;
G560_left_rear.leds_min = 1;
G560_left_rear.leds_max = 1;
G560_left_rear.leds_count = 1;
zones.push_back(G560_left_rear);
led G560_left_read_led;
G560_left_read_led.name = "Left Rear";
G560_left_read_led.value = 0x02;
leds.push_back(G560_left_read_led);
zone G560_right_rear;
G560_right_rear.name = "Right Rear";
G560_right_rear.type = ZONE_TYPE_SINGLE;
G560_right_rear.leds_min = 1;
G560_right_rear.leds_max = 1;
G560_right_rear.leds_count = 1;
zones.push_back(G560_right_rear);
led G560_right_rear_led;
G560_right_rear_led.name = "Right Rear";
G560_right_rear_led.value = 0x03;
leds.push_back(G560_right_rear_led);
SetupColors();
}
void RGBController_LogitechG560::DeviceUpdateLEDs()
{
for(std::size_t led_idx = 0; led_idx < leds.size(); led_idx++)
{
unsigned char red = RGBGetRValue(colors[led_idx]);
unsigned char grn = RGBGetGValue(colors[led_idx]);
unsigned char blu = RGBGetBValue(colors[led_idx]);
controller->SendSpeakerMode((unsigned char)leds[led_idx].value, modes[active_mode].value, red, grn, blu);
}
}
void RGBController_LogitechG560::DeviceUpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_LogitechG560::DeviceUpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_LogitechG560::DeviceUpdateMode()
{
for(std::size_t led_idx = 0; led_idx < leds.size(); led_idx++)
{
if(modes[active_mode].value == LOGITECH_G560_MODE_OFF)
{
controller->SetOffMode(leds[led_idx].value);
}
else
{
/*---------------------------------------------------------*\
| Required to "reset" RGB controller and start receiving |
| color in direct mode |
\*---------------------------------------------------------*/
controller->SetDirectMode(leds[led_idx].value);
}
}
DeviceUpdateLEDs();
}
@@ -1,34 +0,0 @@
/*---------------------------------------------------------*\
| RGBController_LogitechG560.h |
| |
| RGBController for Logitech G560 |
| |
| Cheerpipe 28 Oct 2020 |
| based on TheRogueZeta 31 Aug 2020 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include "RGBController.h"
#include "LogitechG560Controller.h"
class RGBController_LogitechG560 : public RGBController
{
public:
RGBController_LogitechG560(LogitechG560Controller* controller_ptr);
~RGBController_LogitechG560();
void SetupZones();
void DeviceUpdateLEDs();
void DeviceUpdateZoneLEDs(int zone);
void DeviceUpdateSingleLED(int led);
void DeviceUpdateMode();
private:
LogitechG560Controller* controller;
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -28,27 +28,45 @@
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;
/*-----------------------------------------------------*\
| Prefer the USB product string (Solaar's source) |
| over HID_NAME's "Logitech " prefix: read |
| /sys/class/hidraw/<N>/device/../../product, with |
| HID_NAME from the hidraw uevent as the fallback. |
\*-----------------------------------------------------*/
std::string name;
size_t pos = path.rfind("hidraw");
if(pos == std::string::npos)
{
return sysfs_name;
return name;
}
std::string uevent_path = "/sys/class/hidraw/" + path.substr(pos) + "/device/uevent";
std::string hidraw = path.substr(pos);
std::ifstream pf("/sys/class/hidraw/" + hidraw + "/device/../../product");
if(pf)
{
std::getline(pf, name);
while(!name.empty() && (name.back() == '\n' || name.back() == '\r'))
{
name.pop_back();
}
}
if(!name.empty())
{
return name;
}
std::string uevent_path = "/sys/class/hidraw/" + hidraw + "/device/uevent";
FILE* f = fopen(uevent_path.c_str(), "r");
if(!f)
{
return sysfs_name;
return name;
}
char line[256];
@@ -57,11 +75,11 @@ std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::stri
{
if(strncmp(line, "HID_NAME=", 9) == 0)
{
sysfs_name = line + 9;
name = line + 9;
while(!sysfs_name.empty() && (sysfs_name.back() == '\n' || sysfs_name.back() == '\r'))
while(!name.empty() && (name.back() == '\n' || name.back() == '\r'))
{
sysfs_name.pop_back();
name.pop_back();
}
break;
@@ -69,365 +87,5 @@ std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::stri
}
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", &current_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;
return name;
}
@@ -25,18 +25,20 @@
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". |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| 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);
@@ -60,243 +62,3 @@ std::string LogitechHIDPP20Controller::GetCenturionSubDeviceName(const std::stri
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;
}
@@ -85,21 +85,29 @@ 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. |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| 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{};
configured = false;
force_host_mode = false;
show_unmapped = false;
on_battery = LogitechHIDPP20IdleProfile{};
plugged_in = LogitechHIDPP20IdleProfile{};
return;
}
configured = true;
force_host_mode = settings.contains("force_host_mode")
? (bool)settings["force_host_mode"] : false;
show_unmapped = settings.contains("show_unmapped_leds")
? (bool)settings["show_unmapped_leds"] : false;
if(settings.contains("on_battery"))
{
on_battery = ProfileFromJson(settings["on_battery"]);
@@ -123,8 +131,10 @@ void LogitechHIDPP20IdleSettings::save()
{
json settings;
settings["on_battery"] = ProfileToJson(on_battery);
settings["plugged_in"] = ProfileToJson(plugged_in);
settings["on_battery"] = ProfileToJson(on_battery);
settings["plugged_in"] = ProfileToJson(plugged_in);
settings["force_host_mode"] = force_host_mode;
settings["show_unmapped_leds"] = show_unmapped;
SettingsManager* mgr = ResourceManager::get()->GetSettingsManager();
mgr->SetSettings(SETTINGS_KEY, settings);
@@ -133,6 +143,18 @@ void LogitechHIDPP20IdleSettings::save()
configured = true;
}
void LogitechHIDPP20IdleSettings::setForceHostMode(bool v)
{
force_host_mode = v;
configured = true;
}
void LogitechHIDPP20IdleSettings::setShowUnmapped(bool v)
{
show_unmapped = v;
configured = true;
}
void LogitechHIDPP20IdleSettings::setOnBattery(const LogitechHIDPP20IdleProfile& p)
{
on_battery = p;
@@ -5,7 +5,7 @@
| 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. |
| 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 |
@@ -35,6 +35,23 @@ public:
const LogitechHIDPP20IdleProfile& onBattery() const { return on_battery; }
const LogitechHIDPP20IdleProfile& pluggedIn() const { return plugged_in; }
/*-----------------------------------------------------*\
| Ignore HIDPP20_QUIRK_KEEP_ONBOARD_MODE (Solaar or the |
| official app handles G-keys). Applied at device init. |
\*-----------------------------------------------------*/
bool forceHostMode() const { return force_host_mode; }
void setForceHostMode(bool v);
/*-----------------------------------------------------*\
| Surface every unclaimed zone the classification would |
| suppress (defined-but-unstuffed positions, per-board |
| phantoms) in the Unmapped zone. For boards that |
| actually stuff a position no KLM layout places (F13, |
| Keypad =). Applied at device rescan. |
\*-----------------------------------------------------*/
bool showUnmapped() const { return show_unmapped; }
void setShowUnmapped(bool v);
void setOnBattery(const LogitechHIDPP20IdleProfile& p);
void setPluggedIn(const LogitechHIDPP20IdleProfile& p);
@@ -42,6 +59,8 @@ private:
LogitechHIDPP20IdleSettings() = default;
bool configured = false;
bool force_host_mode = false;
bool show_unmapped = false;
LogitechHIDPP20IdleProfile on_battery;
LogitechHIDPP20IdleProfile plugged_in;
};
@@ -0,0 +1,391 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20ReceiverWatcher.cpp |
| |
| Persistent per-receiver-node listener |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "LogitechHIDPP20ReceiverWatcher.h"
#include "LogitechHIDPP20Controller.h"
#include "LogManager.h"
#define LOG_TAG log_tag.c_str()
/*---------------------------------------------------------*\
| Give up on the node after this many consecutive read |
| errors; the receiver was unplugged. |
\*---------------------------------------------------------*/
static const int WATCHER_READ_ERROR_LIMIT = 10;
/*---------------------------------------------------------*\
| Built sub-device registry. Keyed on node and index so one |
| watcher can poke any of the up-to-six devices paired to |
| its receiver. |
\*---------------------------------------------------------*/
static std::map<std::pair<std::string, uint8_t>, LogitechHIDPP20Controller*> subdevice_registry;
static std::mutex subdevice_registry_mutex;
/*---------------------------------------------------------*\
| How to build a device, set by the detector. |
\*---------------------------------------------------------*/
static LogitechHIDPP20ReceiverWatcher::BuilderFunction subdevice_builder;
void LogitechHIDPP20ReceiverWatcher::SetBuilder(BuilderFunction builder)
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
subdevice_builder = builder;
}
void LogitechHIDPP20ReceiverWatcher::RegisterSubDevice(const std::string& node_path, uint8_t index, LogitechHIDPP20Controller* controller)
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
subdevice_registry[std::make_pair(node_path, index)] = controller;
}
void LogitechHIDPP20ReceiverWatcher::UnregisterSubDevice(LogitechHIDPP20Controller* controller)
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
for(std::map<std::pair<std::string, uint8_t>, LogitechHIDPP20Controller*>::iterator it = subdevice_registry.begin();
it != subdevice_registry.end();)
{
if(it->second == controller)
{
it = subdevice_registry.erase(it);
}
else
{
++it;
}
}
}
bool LogitechHIDPP20ReceiverWatcher::PokeSubDevice(const std::string& node_path, uint8_t index, int direction)
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
std::map<std::pair<std::string, uint8_t>, LogitechHIDPP20Controller*>::iterator it =
subdevice_registry.find(std::make_pair(node_path, index));
if(it == subdevice_registry.end())
{
return false;
}
it->second->NudgeConnection(direction);
return true;
}
/*---------------------------------------------------------*\
| A bridge event carries the connected sub-device count, |
| not an index, so every controller on the node gets the |
| direction. |
\*---------------------------------------------------------*/
void LogitechHIDPP20ReceiverWatcher::PokeNode(const std::string& node_path, int direction)
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
for(std::map<std::pair<std::string, uint8_t>, LogitechHIDPP20Controller*>::iterator it = subdevice_registry.begin();
it != subdevice_registry.end(); ++it)
{
if(it->first.first == node_path)
{
it->second->NudgeConnection(direction);
}
}
}
bool LogitechHIDPP20ReceiverWatcher::HasSubDevice(const std::string& node_path, uint8_t index)
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
return subdevice_registry.count(std::make_pair(node_path, index)) != 0;
}
LogitechHIDPP20ReceiverWatcher::LogitechHIDPP20ReceiverWatcher(const std::string& node_path, uint8_t bridge_feat_idx,
uint8_t bridge_report_id, bool bridge_addressed)
{
this->node_path = node_path;
this->bridge_feat_idx = bridge_feat_idx;
this->bridge_report_id = bridge_report_id;
this->bridge_addressed = bridge_addressed;
this->log_tag = "[Logitech receiver watcher @ " + node_path + "]";
this->dev = nullptr;
this->reader_thread = nullptr;
this->worker_thread = nullptr;
this->running = false;
this->alive = false;
}
LogitechHIDPP20ReceiverWatcher::~LogitechHIDPP20ReceiverWatcher()
{
running.store(false);
queue_cv.notify_all();
if(reader_thread)
{
reader_thread->join();
delete reader_thread;
}
if(worker_thread)
{
worker_thread->join();
delete worker_thread;
}
if(dev)
{
hid_close(dev);
}
}
bool LogitechHIDPP20ReceiverWatcher::Start()
{
dev = hid_open_path(node_path.c_str());
if(dev == nullptr)
{
return false;
}
running.store(true);
alive.store(true);
reader_thread = new std::thread(&LogitechHIDPP20ReceiverWatcher::ReaderThreadFunc, this);
worker_thread = new std::thread(&LogitechHIDPP20ReceiverWatcher::WorkerThreadFunc, this);
LOG_INFO("%s Watching for connection events", LOG_TAG);
return true;
}
bool LogitechHIDPP20ReceiverWatcher::IsAlive()
{
return alive.load();
}
/*---------------------------------------------------------*\
| Hand a slot to the worker, once. Repeated connection |
| events for a slot that cannot answer must not queue |
| more than one build. |
\*---------------------------------------------------------*/
void LogitechHIDPP20ReceiverWatcher::QueueBuild(uint8_t index)
{
{
std::lock_guard<std::mutex> lock(queue_mutex);
for(std::deque<uint8_t>::iterator it = build_queue.begin(); it != build_queue.end(); ++it)
{
if(*it == index)
{
return;
}
}
build_queue.push_back(index);
}
queue_cv.notify_all();
}
/*---------------------------------------------------------*\
| A slot changed state. A built controller gets the nudge |
| and its own threads handle the wake; a slot with no |
| controller was unreachable when the pass ran, so build |
| it now. |
\*---------------------------------------------------------*/
void LogitechHIDPP20ReceiverWatcher::OnConnection(uint8_t index, int direction)
{
if(PokeSubDevice(node_path, index, direction))
{
LOG_DEBUG("%s index=0x%02X nudged %+d", LOG_TAG, index, direction);
return;
}
if(direction < 0)
{
return;
}
/*-----------------------------------------------------*\
| Whether this slot is one we know is the builder's |
| answer to give: it holds what every enumerated slot |
| needs to be built, and it outlives the pass. |
\*-----------------------------------------------------*/
QueueBuild(index);
}
/*---------------------------------------------------------*\
| Builds happen here, off the read loop, one at a time. |
\*---------------------------------------------------------*/
void LogitechHIDPP20ReceiverWatcher::WorkerThreadFunc()
{
while(running.load())
{
uint8_t index = 0;
bool have = false;
{
std::unique_lock<std::mutex> lock(queue_mutex);
queue_cv.wait(lock, [this]{ return !build_queue.empty() || !running.load(); });
if(!running.load())
{
return;
}
if(!build_queue.empty())
{
index = build_queue.front();
build_queue.pop_front();
have = true;
}
}
if(!have)
{
continue;
}
BuilderFunction builder;
{
std::lock_guard<std::mutex> lock(subdevice_registry_mutex);
builder = subdevice_builder;
}
if(!builder)
{
continue;
}
LOG_INFO("%s index=0x%02X connected, building it", LOG_TAG, index);
builder(node_path, index);
}
}
/*---------------------------------------------------------*\
| Only reader of the watcher's handle. Never sends: |
| decode, flag, continue. |
\*---------------------------------------------------------*/
void LogitechHIDPP20ReceiverWatcher::ReaderThreadFunc()
{
int errors = 0;
while(running.load())
{
uint8_t buf[64] = {};
int result = hid_read_timeout(dev, buf, sizeof(buf), 500);
if(result < 0)
{
if(++errors >= WATCHER_READ_ERROR_LIMIT)
{
LOG_DEBUG("%s Node is gone, going dormant", LOG_TAG);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
errors = 0;
if(result == 0)
{
continue;
}
if(bridge_feat_idx != 0)
{
/*---------------------------------------------*\
| CentPPBridge event 0: |
| ConnectionStateChangedEvent data[1] = number |
| of connected sub-devices for the whole |
| bridge, so every slot gets the direction. |
\*---------------------------------------------*/
if(buf[0] != bridge_report_id)
{
continue;
}
int hdr = bridge_addressed ? 1 : 0;
uint8_t feat = buf[3 + hdr];
uint8_t func = buf[4 + hdr];
if(feat != bridge_feat_idx || (func & 0xF0) != 0x00 || (func & 0x0F) == HIDPP20_SW_ID)
{
continue;
}
uint8_t num_devices = buf[6 + hdr];
int direction = (num_devices > 0) ? 1 : -1;
LOG_DEBUG("%s Bridge ConnectionStateChanged: %d sub-device(s)", LOG_TAG, num_devices);
PokeNode(node_path, direction);
if(direction > 0)
{
/*-----------------------------------------*\
| A bridge event carries no slot index, so |
| a sub-device with no controller is the |
| dongle's own default index. |
\*-----------------------------------------*/
if(!HasSubDevice(node_path, LOGITECH_DEFAULT_DEVICE_INDEX))
{
QueueBuild(LOGITECH_DEFAULT_DEVICE_INDEX);
}
}
continue;
}
/*-------------------------------------------------*\
| HID++ 1.0 connection notifications from the |
| receiver: 0x41 device connection (flags bit |
| 0x40 = paired but not linked), 0x40 device |
| disconnection. Disconnects are logged only, |
| built controllers detect it themselves. |
\*-------------------------------------------------*/
if(buf[0] != LOGITECH_SHORT_MESSAGE && buf[0] != LOGITECH_LONG_MESSAGE)
{
continue;
}
uint8_t index = buf[1];
uint8_t sub_id = buf[2];
uint8_t flags = buf[4];
if(sub_id == 0x41)
{
bool link_established = !(flags & 0x40);
LOG_DEBUG("%s index=0x%02X connection notification flags=0x%02X link=%d",
LOG_TAG, index, flags, link_established);
if(link_established)
{
OnConnection(index, 1);
}
}
else if(sub_id == 0x40)
{
LOG_DEBUG("%s index=0x%02X disconnection notification", LOG_TAG, index);
}
else
{
continue;
}
}
alive.store(false);
queue_cv.notify_all();
}
@@ -0,0 +1,90 @@
/*---------------------------------------------------------*\
| LogitechHIDPP20ReceiverWatcher.h |
| |
| Persistent per-receiver-node listener. Decodes the |
| dongle's connection notifications and either nudges the |
| built sub-device controller awake, or builds one for a |
| slot that has none yet. Never sends. |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <hidapi.h>
class LogitechHIDPP20Controller;
class LogitechHIDPP20ReceiverWatcher
{
public:
/*-----------------------------------------------------*\
| bridge_feat_idx == 0 reads HID++ 1.0 connection |
| notifications (standard receiver). Nonzero reads |
| CentPPBridge ConnectionStateChangedEvent in the |
| Centurion framing given by report_id/addressed. |
\*-----------------------------------------------------*/
LogitechHIDPP20ReceiverWatcher(const std::string& node_path, uint8_t bridge_feat_idx,
uint8_t bridge_report_id, bool bridge_addressed);
~LogitechHIDPP20ReceiverWatcher();
bool Start();
bool IsAlive();
/*-----------------------------------------------------*\
| How the watcher builds a device reported as |
| connected. Set once by the detector, which owns the |
| build; the watcher only decides when. |
\*-----------------------------------------------------*/
typedef std::function<bool(const std::string& node_path, uint8_t index)> BuilderFunction;
static void SetBuilder(BuilderFunction builder);
static bool HasSubDevice(const std::string& node_path, uint8_t index);
/*-----------------------------------------------------*\
| Built sub-device registry, shared by all watchers. |
| A poke reaches a built controller; a controller |
| deregisters at the top of its destructor under the |
| same lock, so a poke can never touch a dying one. |
\*-----------------------------------------------------*/
static void RegisterSubDevice(const std::string& node_path, uint8_t index, LogitechHIDPP20Controller* controller);
static void UnregisterSubDevice(LogitechHIDPP20Controller* controller);
private:
void ReaderThreadFunc();
void WorkerThreadFunc();
void OnConnection(uint8_t index, int direction);
void QueueBuild(uint8_t index);
static bool PokeSubDevice(const std::string& node_path, uint8_t index, int direction);
static void PokeNode(const std::string& node_path, int direction);
std::string node_path;
uint8_t bridge_feat_idx;
uint8_t bridge_report_id;
bool bridge_addressed;
std::string log_tag;
hid_device* dev;
std::thread* reader_thread;
std::thread* worker_thread;
std::atomic<bool> running;
std::atomic<bool> alive;
/*-----------------------------------------------------*\
| Slots reported connected with no controller yet, |
| waiting for the worker. A build is feature discovery |
| and a claim, far too long to run on the read loop. |
\*-----------------------------------------------------*/
std::deque<uint8_t> build_queue;
std::mutex queue_mutex;
std::condition_variable queue_cv;
};
@@ -19,6 +19,8 @@ public:
~RGBController_LogitechHIDPP20();
void SetupZones();
void SetupZones8080();
bool PerKey8080Capable() const;
void DeviceUpdateLEDs();
void DeviceUpdateZoneLEDs(int zone);
@@ -29,46 +31,89 @@ public:
bool ReapplyActiveMode();
LogitechHIDPP20Controller* GetController() const { return controller; }
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. |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| 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. |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| Per-key frame body. Registered with the |
| controller as the frame sender callback and |
| invoked from its sender thread with the newest |
| submitted snapshot: diffs it against sent_colors |
| and puts the differences on the wire. |
\*-----------------------------------------------------*/
void SendPerKeyFrame(std::vector<RGBColor>& snapshot);
/*-----------------------------------------------------*\
| Build one matrix (map != nullptr) or linear |
| zone from a name + geometry + (led name, wire |
| value) list. Shared by the static-table layout |
| paths (mouse, headset). |
\*-----------------------------------------------------*/
void AddLayoutZone(const std::string& zone_name,
zone_type type,
unsigned int rows,
unsigned int cols,
const unsigned int* map,
const std::vector<std::pair<std::string, uint16_t>>& zone_leds);
/*-----------------------------------------------------*\
| 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 |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| 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. |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| 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. |
\*---------------------------------------------------------*/
/*-----------------------------------------------------*\
| 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;
/*-----------------------------------------------------*\
| Rolling resync cursor (see HIDPP20_RESYNC_KEYS_MIN). |
| Walks the LED list one frame at a time so every key |
| is repainted within a bounded number of frames. |
\*-----------------------------------------------------*/
size_t resync_cursor = 0;
/*-----------------------------------------------------*\
| 0x8080 addressing: LED index -> (keyType, |
| keyId), built in SetupZones from the |
| enumeration. keyType 0 + keyId 0 marks a |
| matrix LED the device did not enumerate. |
| Parallel to led_to_zone_id; 0x8080 path only. |
\*-----------------------------------------------------*/
std::vector<std::pair<uint16_t, uint8_t>> led_to_keytype_keyid_8080;
};
@@ -35,7 +35,7 @@ static std::vector<uint16_t> logitech_RGB_pages =
LOGITECH_HIDPP_PAGE_RGB_EFFECTS2
};
int getWirelessDevice(usages device_usages, uint16_t pid, wireless_map *wireless_devices)
int getWirelessDevice(usages device_usages, uint16_t pid, wireless_map *wireless_devices, std::map<uint8_t, bool> *online_out)
{
hid_device* dev_use1;
usages::iterator find_usage = device_usages.find(1);
@@ -115,6 +115,20 @@ int getWirelessDevice(usages device_usages, uint16_t pid, wireless_map *wireless
if(devices.device_index != LOGITECH_RECEIVER_DEVICE_INDEX)
{
wireless_devices->emplace(wireless_PID, devices.device_index);
/*-------------------------------------*\
| data[0] bit 0x40 of the connection |
| notification is the link flag: set |
| means the device is paired but not |
| currently linked (off / asleep). |
| Record online state so callers can |
| tell a sleeping device apart from |
| an absent slot. |
\*-------------------------------------*/
if(online_out)
{
online_out->emplace(devices.device_index, !(devices.data[0] & 0x40));
}
}
}
}
@@ -127,6 +141,164 @@ int getWirelessDevice(usages device_usages, uint16_t pid, wireless_map *wireless
return((int)wireless_devices->size());
}
std::string getWirelessDeviceName(usages device_usages, uint8_t device_index)
{
usages::iterator find_usage = device_usages.find(1);
if(find_usage == device_usages.end() || device_index < 1)
{
return "";
}
hid_device* dev_use1 = find_usage->second;
/*-----------------------------------------------------*\
| GET_LONG_REGISTER 0xB5 (receiver info), sub 0x40 + N |
| - 1 = device name: the codename the receiver stores |
| for the paired device. Answered by the receiver |
| itself, so it works even when the device is asleep. |
\*-----------------------------------------------------*/
shortFAPrequest get_name;
get_name.init(LOGITECH_RECEIVER_DEVICE_INDEX, LOGITECH_GET_LONG_REGISTER_REQUEST);
get_name.feature_command = 0xB5;
get_name.data[0] = (uint8_t)(0x40 + device_index - 1);
hid_write(dev_use1, get_name.buffer, get_name.size());
/*-----------------------------------------------------*\
| Read until the matching response, skipping |
| unrelated frames (link notifications etc.) up to |
| a small budget. |
\*-----------------------------------------------------*/
for(int reads = 0; reads < 8; reads++)
{
blankFAPmessage response;
response.init();
int rd = hid_read_timeout(dev_use1, response.buffer, response.size(), LOGITECH_PROTOCOL_TIMEOUT);
if(rd <= 0)
{
break;
}
if(response.feature_index != LOGITECH_GET_LONG_REGISTER_REQUEST ||
response.feature_command != 0xB5 ||
response.data[0] != (uint8_t)(0x40 + device_index - 1))
{
continue;
}
unsigned int name_len = response.data[1];
LOG_DEBUG("Pairing name reply idx=%u len=%u raw=[%02X %02X %02X %02X %02X %02X %02X %02X]",
device_index, name_len,
response.data[0], response.data[1], response.data[2], response.data[3],
response.data[4], response.data[5], response.data[6], response.data[7]);
if(name_len == 0 || name_len > 14)
{
break;
}
std::string name((char*)&response.data[2], name_len);
while(!name.empty() && (name.back() == '\0' || name.back() == ' '))
{
name.pop_back();
}
/*-------------------------------------------------*\
| Reject a garbled read: right length, junk |
| bytes. Callers read an empty return as |
| "no pairing name". |
\*-------------------------------------------------*/
for(unsigned char c : name)
{
if(c < 0x20 || c > 0x7E)
{
LOG_DEBUG("Pairing name for idx=%u is not printable, discarding", device_index);
return "";
}
}
return name;
}
return "";
}
/*---------------------------------------------------------*\
| Receiver-stored serial for a paired slot: |
| GET_LONG_REGISTER 0xB5 sub 0x30+N-1 (extended |
| pairing info), bytes 1..4, the same value the device |
| reports as its HID++ 2.0 unit id. Answered from the |
| receiver's own registers, so it works while the |
| device is asleep, off, or away on its cable: a |
| device identity that needs no reachable device. |
\*---------------------------------------------------------*/
std::string getWirelessDeviceSerial(usages device_usages, uint8_t device_index)
{
usages::iterator find_usage = device_usages.find(1);
if(find_usage == device_usages.end() || device_index < 1)
{
return "";
}
hid_device* dev_use1 = find_usage->second;
shortFAPrequest get_serial;
get_serial.init(LOGITECH_RECEIVER_DEVICE_INDEX, LOGITECH_GET_LONG_REGISTER_REQUEST);
get_serial.feature_command = 0xB5;
get_serial.data[0] = (uint8_t)(0x30 + device_index - 1);
hid_write(dev_use1, get_serial.buffer, get_serial.size());
for(int reads = 0; reads < 8; reads++)
{
blankFAPmessage response;
response.init();
int rd = hid_read_timeout(dev_use1, response.buffer, response.size(), LOGITECH_PROTOCOL_TIMEOUT);
if(rd <= 0)
{
break;
}
if(response.feature_index != LOGITECH_GET_LONG_REGISTER_REQUEST ||
response.feature_command != 0xB5 ||
response.data[0] != (uint8_t)(0x30 + device_index - 1))
{
continue;
}
char serial[9];
snprintf(serial, sizeof(serial), "%02X%02X%02X%02X",
response.data[1], response.data[2], response.data[3], response.data[4]);
std::string serial_str(serial);
LOG_DEBUG("Pairing serial reply idx=%u serial=%s", device_index, serial_str.c_str());
/*-------------------------------------------------*\
| An all-zero serial is the receiver saying |
| it has none. Useless as an identity, |
| callers must not treat it as one. |
\*-------------------------------------------------*/
if(serial_str == "00000000")
{
return "";
}
return serial_str;
}
return "";
}
logitech_device::logitech_device(char *path, usages _usages, uint8_t _device_index, bool _wireless)
{
device_index = _device_index;
@@ -22,12 +22,15 @@
#define LOGITECH_SHORT_MESSAGE_LEN 7
#define LOGITECH_LONG_MESSAGE 0x11
#define LOGITECH_LONG_MESSAGE_LEN 20
#define LOGITECH_VERY_LONG_MESSAGE 0x12
#define LOGITECH_VERY_LONG_MESSAGE_LEN 64
#define LOGITECH_FAP_RESPONSE_LEN 64 //Define a universal response buffer and allow the hidapi to determine the size
#define LOGITECH_DEFAULT_DEVICE_INDEX 0xFF
#define LOGITECH_RECEIVER_DEVICE_INDEX 0xFF //The Unifying receiver uses RAP or register access protocol
#define LOGITECH_SET_REGISTER_REQUEST 0x80
#define LOGITECH_GET_REGISTER_REQUEST 0x81
#define LOGITECH_GET_LONG_REGISTER_REQUEST 0x83
#define LOGITECH_HIDPP_PAGE_ROOT_IDX 0x00 //Used for querying the feature index
#define LOGITECH_CMD_ROOT_GET_FEATURE 0x01
@@ -63,7 +66,18 @@ enum LOGITECH_DEVICE_TYPE
LOGITECH_DEVICE_TYPE_TRACKBALL = 5,
LOGITECH_DEVICE_TYPE_PRESENTER = 6,
LOGITECH_DEVICE_TYPE_RECEIVER = 7,
LOGITECH_DEVICE_TYPE_HEADSET = 8
LOGITECH_DEVICE_TYPE_HEADSET = 8,
LOGITECH_DEVICE_TYPE_WEBCAM = 9,
LOGITECH_DEVICE_TYPE_STEERINGWHEEL = 10,
LOGITECH_DEVICE_TYPE_JOYSTICK = 11,
LOGITECH_DEVICE_TYPE_GAMEPAD = 12,
LOGITECH_DEVICE_TYPE_DOCK = 13,
LOGITECH_DEVICE_TYPE_SPEAKER = 14,
LOGITECH_DEVICE_TYPE_MICROPHONE = 15,
LOGITECH_DEVICE_TYPE_LIGHT = 16,
LOGITECH_DEVICE_TYPE_PROGRAMMABLE = 17,
LOGITECH_DEVICE_TYPE_CARSIMPEDALS = 18,
LOGITECH_DEVICE_TYPE_ADAPTER = 19
};
enum LOGITECH_DEVICE_MODE
@@ -270,7 +284,9 @@ struct logitech_led
leds_fx fx;
};
int getWirelessDevice(usages _usages, uint16_t pid, wireless_map *wireless_devices); //Helper function needed outside of class
int getWirelessDevice(usages _usages, uint16_t pid, wireless_map *wireless_devices, std::map<uint8_t, bool> *online_out = nullptr); //Helper function needed outside of class; online_out (optional) reports per-index link state
std::string getWirelessDeviceName(usages _usages, uint8_t device_index); //Receiver-stored device codename (reg 0xB5 sub 0x4N); works while the device is asleep
std::string getWirelessDeviceSerial(usages _usages, uint8_t device_index); //Receiver-stored device serial (extended pairing info reg 0xB5 sub 0x3N); works while the device is asleep, and matches the device's own HID++ unit id
class logitech_device
{
+1
View File
@@ -25,6 +25,7 @@ echo -e "Adding Static Headers"
UDEV_HEADER=${UDEV_LINE}'# OpenRGB udev rules - Git Commit: '${GIT_SHORT_HASH:0:8}' #\n'${UDEV_LINE}'\n'
UDEV_HEADER+=${UDEV_LINE}'# User I2C/SMBus Access #\n'${UDEV_LINE}'KERNEL=="i2c-[0-99]*", TAG+="uaccess"\n\n'
UDEV_HEADER+=${UDEV_LINE}'# Super I/O Access #\n'${UDEV_LINE}'KERNEL=="port", TAG+="uaccess"\n\n'
UDEV_HEADER+=${UDEV_LINE}'# Logitech HID++ 2.0 - VID-generic detection #\n'${UDEV_LINE}'SUBSYSTEM=="hidraw", ATTRS{idVendor}=="046d", TAG+="uaccess", TAG+="Logitech_HID_20"\nSUBSYSTEM=="usb", ATTR{idVendor}=="046d", TAG+="uaccess", TAG+="Logitech_HID_20"\n\n'
# Faustus rules
ASUS_TUF_DEVICES=('blue' 'flags' 'green' 'mode' 'red' 'set' 'speed')