From 057bfb6974a2d39f70733130305cfc8c4de7ae8f Mon Sep 17 00:00:00 2001 From: Genesis Date: Tue, 17 Jan 2023 16:11:41 +0100 Subject: [PATCH 1/5] Improve Logitech G535 support --- README.md | 2 +- src/devices/logitech_g535.c | 181 ++++++++++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e3c49f39..0fc45eff 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ talking. This differs from a simple loopback via PulseAudio as you won't have an - Logitech G533 - Sidetone, Battery (for Wireless) - Logitech G535 - - Sidetone (only tested on Linux) + - Sidetone, Battery, Inactive time (only tested on Linux) - Logitech G633 / G635 / G733 / G933 / G935 - Sidetone, Battery (for Wireless), LED on/off - Logitech G930 diff --git a/src/devices/logitech_g535.c b/src/devices/logitech_g535.c index 79999366..a1bf2b3b 100644 --- a/src/devices/logitech_g535.c +++ b/src/devices/logitech_g535.c @@ -4,14 +4,79 @@ #include #include - -#define MSG_SIZE 20 +#include static struct device device_g535; static const uint16_t PRODUCT_ID = 0x0ac4; +// Based on manual measurements so the discharge curve used to generate these values aren't always +// right, but it's good enough. +// Based on the following measured values on a brand new headset (voltage, percentage) : +// - 4175, 100 +// - 4135, 98 +// - 4124, 97 +// - 4109, 96 +// - 4106, 95 +// - 4066, 90 +// - 4055, 87 +// - 4047, 86 +// - 4036, 85 +// - 4025, 84 +// - 4000, 83 +// - 3985, 81 +// - 3974, 80 +// - 3971, 79 +// - 3963, 78 +// - 3945, 72 +// - 3934, 71 +// - 3916, 67 +// - 3894, 64 +// - 3887, 63 +// - 3872, 61 +// - 3839, 56 +// - 3817, 50 +// - 3806, 48 +// - 3788, 39 +// - 3774, 34 +// - 3766, 30 +// - 3752, 26 +// - 3741, 22 +// - 3730, 20 +// - 3719, 17 +// - 3701, 13 +// - 3688, 10 +// - 3679, 8 +// - 3675, 6 +// - 3664, 5 +// - 3640, 4 +// - 3600, 3 +// - 3540, 2 +// - 3485, 1 +// - 3445, 1 +// - 3405, 1 +// - 3339, 0 +// - 3325, 0 +// - 3310, 0 + +// Generated using https://arachnoid.com/polysolve/ +static const double battery_estimate_terms[] = { + 5.2745819811026892e+006, + -7.2266407634766219e+003, + 3.8767801227503904e+000, + -1.0542664248696289e-003, + 1.8582262783020881e-007, + -3.4500263384672060e-011, + 4.7875908463303429e-015, + 3.9178868212079191e-019, + -2.1037922411043121e-022, + 1.7519707367951941e-026 +}; +static const size_t num_terms = 10; + static int g535_send_sidetone(hid_device* device_handle, uint8_t num); +static int g535_request_battery(hid_device* device_handle); +static int g535_send_inactive_time(hid_device* device_handle, uint8_t num); void g535_init(struct device** device) { @@ -21,21 +86,121 @@ void g535_init(struct device** device) strncpy(device_g535.device_name, "Logitech G535", sizeof(device_g535.device_name)); - device_g535.capabilities = B(CAP_SIDETONE); + device_g535.capabilities = B(CAP_SIDETONE) | B(CAP_BATTERY_STATUS) | B(CAP_INACTIVE_TIME); device_g535.capability_details[CAP_SIDETONE] = (struct capability_detail) { .usagepage = 0xc, .usageid = 0x1, .interface = 3 }; - device_g535.send_sidetone = &g535_send_sidetone; + /// TODO: usagepage and id may not be correct for battery status and inactive timer + device_g535.capability_details[CAP_BATTERY_STATUS] = (struct capability_detail) { .usagepage = 0xc, .usageid = 0x1, .interface = 3 }; + device_g535.capability_details[CAP_INACTIVE_TIME] = (struct capability_detail) { .usagepage = 0xc, .usageid = 0x1, .interface = 3 }; + + device_g535.send_sidetone = &g535_send_sidetone; + device_g535.request_battery = &g535_request_battery; + device_g535.send_inactive_time = &g535_send_inactive_time; *device = &device_g535; } static int g535_send_sidetone(hid_device* device_handle, uint8_t num) { + int ret = 0; + num = map(num, 0, 128, 0, 100); - uint8_t set_sidetone_level[MSG_SIZE] = { 0x11, 0xff, 0x04, 0x1e, num }; + uint8_t buf[HIDPP_LONG_MESSAGE_LENGTH] = { HIDPP_LONG_MESSAGE, HIDPP_DEVICE_RECEIVER, 0x04, 0x1d, num, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + ret = hid_send_feature_report(device_handle, buf, sizeof(buf) / sizeof(buf[0])); + if (ret < 0) { + return ret; + } + + ret = hid_read_timeout(device_handle, buf, HIDPP_LONG_MESSAGE_LENGTH, hsc_device_timeout); + if (ret < 0) { + return ret; + } + + if (ret == 0) { + return HSC_READ_TIMEOUT; + } + + // Headset offline + if (buf[2] == 0xFF) { + return BATTERY_UNAVAILABLE; + } + + if (buf[4] != num) { + return HSC_ERROR; + } + + return ret; +} + +// inspired by logitech_g533.c +static int g535_request_battery(hid_device* device_handle) +{ + int ret = 0; + + // request battery voltage + uint8_t buf[HIDPP_LONG_MESSAGE_LENGTH] = { HIDPP_LONG_MESSAGE, HIDPP_DEVICE_RECEIVER, 0x05, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + ret = hid_send_feature_report(device_handle, buf, sizeof(buf) / sizeof(buf[0])); + if (ret < 0) { + return ret; + } + + ret = hid_read_timeout(device_handle, buf, HIDPP_LONG_MESSAGE_LENGTH, hsc_device_timeout); + if (ret < 0) { + return ret; + } + + if (ret == 0) { + return HSC_READ_TIMEOUT; + } + + // Headset offline + if (buf[2] == 0xFF) { + return BATTERY_UNAVAILABLE; + } + + // 7th byte is state; 0x01 for idle, 0x03 for charging + uint8_t state = buf[6]; + if (state == 0x03) { + return BATTERY_CHARGING; + } + + // actual voltage is byte 4 and byte 5 combined together + const uint16_t voltage = (buf[4] << 8) | buf[5]; + + return (int)(roundf(poly_battery_level(battery_estimate_terms, num_terms, voltage))); +} + +static int g535_send_inactive_time(hid_device* device_handle, uint8_t num) +{ + // Accepted values are 0 (never), 1, 2, 5, 10, 15, 30 + int ret = 0; + + uint8_t buf[HIDPP_LONG_MESSAGE_LENGTH] = { HIDPP_LONG_MESSAGE, HIDPP_DEVICE_RECEIVER, 0x05, 0x2d, num, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + ret = hid_send_feature_report(device_handle, buf, sizeof(buf) / sizeof(buf[0])); + if (ret < 0) { + return ret; + } + + ret = hid_read_timeout(device_handle, buf, HIDPP_LONG_MESSAGE_LENGTH, hsc_device_timeout); + if (ret < 0) { + return ret; + } + + if (ret == 0) { + return HSC_READ_TIMEOUT; + } + + // Headset offline + if (buf[2] == 0xFF) { + return BATTERY_UNAVAILABLE; + } - for (int i = 16; i < MSG_SIZE; i++) - set_sidetone_level[i] = 0; + if (buf[4] != num) { + return HSC_ERROR; + } - return hid_send_feature_report(device_handle, set_sidetone_level, MSG_SIZE); + return ret; } From 2791cb916de3b07625cd3746e5cba6ad7f1677ff Mon Sep 17 00:00:00 2001 From: Genesis Date: Sat, 18 Feb 2023 12:41:07 +0100 Subject: [PATCH 2/5] Add a utility function to calculate battery percentage using splines --- src/devices/logitech_g535.c | 20 ++++---------------- src/utility.c | 21 +++++++++++++++++++++ src/utility.h | 15 +++++++++++++-- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/devices/logitech_g535.c b/src/devices/logitech_g535.c index a1bf2b3b..0ce48814 100644 --- a/src/devices/logitech_g535.c +++ b/src/devices/logitech_g535.c @@ -58,21 +58,9 @@ static const uint16_t PRODUCT_ID = 0x0ac4; // - 3339, 0 // - 3325, 0 // - 3310, 0 - -// Generated using https://arachnoid.com/polysolve/ -static const double battery_estimate_terms[] = { - 5.2745819811026892e+006, - -7.2266407634766219e+003, - 3.8767801227503904e+000, - -1.0542664248696289e-003, - 1.8582262783020881e-007, - -3.4500263384672060e-011, - 4.7875908463303429e-015, - 3.9178868212079191e-019, - -2.1037922411043121e-022, - 1.7519707367951941e-026 -}; -static const size_t num_terms = 10; +static const int battery_estimate_percentages[] = { 100, 50, 30, 20, 5, 0 }; +static const int battery_estimate_voltages[] = { 4175, 3817, 3766, 3730, 3664, 3310 }; +static const size_t battery_estimate_size = 6; static int g535_send_sidetone(hid_device* device_handle, uint8_t num); static int g535_request_battery(hid_device* device_handle); @@ -169,7 +157,7 @@ static int g535_request_battery(hid_device* device_handle) // actual voltage is byte 4 and byte 5 combined together const uint16_t voltage = (buf[4] << 8) | buf[5]; - return (int)(roundf(poly_battery_level(battery_estimate_terms, num_terms, voltage))); + return spline_battery_level(battery_estimate_percentages, battery_estimate_voltages, battery_estimate_size, voltage); } static int g535_send_inactive_time(hid_device* device_handle, uint8_t num) diff --git a/src/utility.c b/src/utility.c index a2bc4bbd..38e706e7 100644 --- a/src/utility.c +++ b/src/utility.c @@ -12,6 +12,27 @@ int map(int x, int in_min, int in_max, int out_min, int out_max) return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } +int spline_battery_level(const int p[], const int v[], const size_t size, uint16_t voltage) +{ + int percent = 0; + + for (int i = 0; i < size; ++i) { + // if >= then 100% + if (voltage >= v[i]) { + percent = p[i]; + break; + } + + // if not last + if (i < size - 1 && voltage >= v[i + 1]) { + percent = p[i + 1] + (voltage - v[i + 1]) / (v[i] - v[i + 1]) * (p[i] - p[i + 1]); + break; + } + } + + return percent; +} + float poly_battery_level(const double terms[], const size_t numterms, uint16_t voltage) { double t = 1; diff --git a/src/utility.h b/src/utility.h index f6a25c0f..4e2c3357 100644 --- a/src/utility.h +++ b/src/utility.h @@ -9,14 +9,25 @@ */ int map(int x, int in_min, int in_max, int out_min, int out_max); +/** + * @brief This function calculates the estimate batttery level in percent using splines. + * + * @param p percentage values to be associated with voltage values + * @param v voltage values associated with percentage values + * @param size number of percentage and voltage associations + * @param voltage readings + * @return battery level in percent + */ +int spline_battery_level(const int p[], const int v[], const size_t size, uint16_t voltage); + /** * @brief This function calculates the estimate batttery level in percent. * * To find the terms representing the polynominal discarge curve of the * battery an solver like https://arachnoid.com/polysolve/ can be used. * - * @param array polynominal terms for the battery discharge curve - * @param number of terms + * @param terms polynominal terms for the battery discharge curve + * @param numterms number of terms * @param voltage readings * @return battery level in percent */ From 8ffa034992eabfd8dbe8b8b66fc664bb3672b442 Mon Sep 17 00:00:00 2001 From: Genesis Date: Sat, 18 Feb 2023 12:41:31 +0100 Subject: [PATCH 3/5] Bounds checking when setting inactive time --- src/devices/logitech_g535.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/devices/logitech_g535.c b/src/devices/logitech_g535.c index 0ce48814..3c33c9b3 100644 --- a/src/devices/logitech_g535.c +++ b/src/devices/logitech_g535.c @@ -163,6 +163,10 @@ static int g535_request_battery(hid_device* device_handle) static int g535_send_inactive_time(hid_device* device_handle, uint8_t num) { // Accepted values are 0 (never), 1, 2, 5, 10, 15, 30 + if (num != 0 && num != 1 && num != 2 && num != 5 && num != 10 && num != 15 && num != 30) { + return HSC_OUT_OF_BOUNDS; + } + int ret = 0; uint8_t buf[HIDPP_LONG_MESSAGE_LENGTH] = { HIDPP_LONG_MESSAGE, HIDPP_DEVICE_RECEIVER, 0x05, 0x2d, num, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; From 3f60b7c86b957a1820a7bc873dc601ec6d884283 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Fri, 3 Mar 2023 17:05:26 +0100 Subject: [PATCH 4/5] Logitech G535: Improve oo-bounds check --- src/devices/logitech_g535.c | 7 ++++++- src/utility.c | 5 +++++ src/utility.h | 11 +++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/devices/logitech_g535.c b/src/devices/logitech_g535.c index 3c33c9b3..d4f3b0f5 100644 --- a/src/devices/logitech_g535.c +++ b/src/devices/logitech_g535.c @@ -163,8 +163,13 @@ static int g535_request_battery(hid_device* device_handle) static int g535_send_inactive_time(hid_device* device_handle, uint8_t num) { // Accepted values are 0 (never), 1, 2, 5, 10, 15, 30 - if (num != 0 && num != 1 && num != 2 && num != 5 && num != 10 && num != 15 && num != 30) { + if (num > 30) { + printf("Device only accepts 0 (never) and numbers up to 30 for inactive time\n"); return HSC_OUT_OF_BOUNDS; + } else if (num > 2 && num < 5) { // let numbers smaller-inclusive 2 through, set numbers smaller than 5 to 5, and round the rest up to 30 + num = 5; + } else if (num > 5) { + num = round_to_multiples(num, 5); } int ret = 0; diff --git a/src/utility.c b/src/utility.c index 38e706e7..502fb357 100644 --- a/src/utility.c +++ b/src/utility.c @@ -12,6 +12,11 @@ int map(int x, int in_min, int in_max, int out_min, int out_max) return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } +unsigned int round_to_multiples(unsigned int number, unsigned int multiple) +{ + return ((number + (multiple / 2)) / multiple) * multiple; +} + int spline_battery_level(const int p[], const int v[], const size_t size, uint16_t voltage) { int percent = 0; diff --git a/src/utility.h b/src/utility.h index 4e2c3357..1ad214cc 100644 --- a/src/utility.h +++ b/src/utility.h @@ -9,6 +9,17 @@ */ int map(int x, int in_min, int in_max, int out_min, int out_max); +/** + * @brief Rounds a given positive number to the nearest given multiple + * + * I.e. A number of 17 would be rounded to 15 if multiple is 5 + * + * @param number A number to round + * @param multiple A multiple + * @return unsigned int the result rounded number + */ +unsigned int round_to_multiples(unsigned int number, unsigned int multiple); + /** * @brief This function calculates the estimate batttery level in percent using splines. * From a1ca5e1c85b65b2c56fccee9597cb5f2723f22b8 Mon Sep 17 00:00:00 2001 From: Denis Arnst Date: Fri, 3 Mar 2023 17:19:02 +0100 Subject: [PATCH 5/5] Logitech G535: Add stdio.h --- src/devices/logitech_g535.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/devices/logitech_g535.c b/src/devices/logitech_g535.c index d4f3b0f5..81e00370 100644 --- a/src/devices/logitech_g535.c +++ b/src/devices/logitech_g535.c @@ -3,6 +3,7 @@ #include "logitech.h" #include +#include #include #include