Logitech HID++ 2.0: VID-generic detection, protocol fixes and device support
This commit is contained in:
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;
|
||||
};
|
||||
+3721
-2010
File diff suppressed because it is too large
Load Diff
+751
-403
File diff suppressed because it is too large
Load Diff
+33
-375
@@ -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", ¤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;
|
||||
return name;
|
||||
}
|
||||
|
||||
+14
-252
@@ -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;
|
||||
}
|
||||
|
||||
+32
-10
@@ -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;
|
||||
|
||||
+20
-1
@@ -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;
|
||||
};
|
||||
|
||||
+391
@@ -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();
|
||||
}
|
||||
|
||||
+90
@@ -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;
|
||||
};
|
||||
+2277
-841
File diff suppressed because it is too large
Load Diff
+74
-29
@@ -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
|
||||
{
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user