Publish curated LumaOps source from c85ee9ef8f1454f5c305398902afbb9e6738de40
Managed validation / full (push) Failing after 8s
Managed validation / full (push) Failing after 8s
This commit is contained in:
1 parent
91f8b3e3c1
commit
def7920856
2271 files changed
+501225
-501178
No files matched your search
+5709
-5709
File diff suppressed because it is too large.
Load diff
+874
-874
File diff suppressed because it is too large.
Load diff
+433
-433
@@ -1,433 +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;
|
||||
}
|
||||
/*---------------------------------------------------------*\
|
||||
| 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;
|
||||
}
|
||||
+302
-302
@@ -1,302 +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;
|
||||
}
|
||||
/*---------------------------------------------------------*\
|
||||
| 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;
|
||||
}
|
||||
+146
-146
@@ -1,146 +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;
|
||||
}
|
||||
/*---------------------------------------------------------*\
|
||||
| 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;
|
||||
}
|
||||
+47
-47
@@ -1,47 +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;
|
||||
};
|
||||
/*---------------------------------------------------------*\
|
||||
| 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;
|
||||
};
|
||||
+1796
-1796
File diff suppressed because it is too large.
Load diff
+72
-72
@@ -1,72 +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;
|
||||
};
|
||||
/*---------------------------------------------------------*\
|
||||
| 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;
|
||||
};
|
||||
Reference in new issue
Block a user