Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
179 changes: 171 additions & 8 deletions src/devices/logitech_g535.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,69 @@
#include "logitech.h"

#include <math.h>
#include <stdio.h>
#include <string.h>

#define MSG_SIZE 20
#include <unistd.h>

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)
{
Expand All @@ -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;
}
26 changes: 26 additions & 0 deletions src/utility.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 24 additions & 2 deletions src/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down