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..81e00370 100644 --- a/src/devices/logitech_g535.c +++ b/src/devices/logitech_g535.c @@ -3,15 +3,69 @@ #include "logitech.h" #include +#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 +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); +static int g535_send_inactive_time(hid_device* device_handle, uint8_t num); void g535_init(struct device** device) { @@ -21,21 +75,130 @@ 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 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) +{ + // Accepted values are 0 (never), 1, 2, 5, 10, 15, 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; + + 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; } diff --git a/src/utility.c b/src/utility.c index a2bc4bbd..502fb357 100644 --- a/src/utility.c +++ b/src/utility.c @@ -12,6 +12,32 @@ 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; + + 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..1ad214cc 100644 --- a/src/utility.h +++ b/src/utility.h @@ -9,14 +9,36 @@ */ 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. + * + * @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 */