Files
libratbag/src/driver-logitech-g600.c
Peter Hutterer d6bbc33dda Drop the switchable-profile device capability
If we have more than one profile, we can switch between them. Because
otherwise having more than one profile is just showing off and we don't like
that type of behaviour, no no.

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2019-02-22 16:21:00 +10:00

670 lines
19 KiB
C

/*
* Copyright © 2018 Thomas Hindoe Paaboel Andersen.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include "config.h"
#include <assert.h>
#include <errno.h>
#include <libevdev/libevdev.h>
#include <linux/input.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include "libratbag-private.h"
#include "libratbag-hidraw.h"
#define LOGITECH_G600_NUM_PROFILES 3
#define LOGITECH_G600_NUM_BUTTONS 20
#define LOGITECH_G600_NUM_DPI 4
#define LOGITECH_G600_NUM_LED 1
#define LOGITECH_G600_DPI_MIN 200
#define LOGITECH_G600_DPI_MAX 8200
#define LOGITECH_G600_REPORT_ID_GET_ACTIVE 0xF0
#define LOGITECH_G600_REPORT_ID_SET_ACTIVE 0xF0
#define LOGITECH_G600_REPORT_ID_PROFILE_0 0xF3
#define LOGITECH_G600_REPORT_ID_PROFILE_1 0xF4
#define LOGITECH_G600_REPORT_ID_PROFILE_2 0xF5
#define LOGITECH_G600_REPORT_SIZE_PROFILE 154
#define LOGITECH_G600_LED_SOLID 0x00
#define LOGITECH_G600_LED_BREATHE 0x01
#define LOGITECH_G600_LED_CYCLE 0x02
struct logitech_g600_button {
uint8_t code;
uint8_t modifier;
uint8_t key;
} __attribute__((packed));
struct logitech_g600_profile_report {
uint8_t id;
uint8_t led_red;
uint8_t led_green;
uint8_t led_blue;
uint8_t led_effect;
uint8_t led_duration;
uint8_t unknown1[5];
uint8_t frequency; /* frequency = 1000 / (value + 1) */
uint8_t dpi_shift; /* value is a linear range between 200->0x04, 8200->0xa4, so value * 50, 0x00 is disabled */
uint8_t dpi_default; /* between 1 and 4*/
uint8_t dpi[4]; /* value is a linear range between 200->0x04, 8200->0xa4, so value * 50, 0x00 is disabled */
uint8_t unknown2[13];
struct logitech_g600_button buttons[20];
uint8_t g_shift_color[3]; /* can't assign it in LGS, but the 3rd profile has one that shows the feature :) */
struct logitech_g600_button g_shift_buttons[20];
} __attribute__((packed));
struct logitech_g600_active_profile_report {
uint8_t id;
uint8_t unknown1 :1;
uint8_t resolution :2;
uint8_t unknown2 :1;
uint8_t profile :4;
uint8_t unknown3;
uint8_t unknown4;
} __attribute__((packed));
struct logitech_g600_profile_data {
struct logitech_g600_profile_report report;
};
struct logitech_g600_data {
struct logitech_g600_profile_data profile_data[LOGITECH_G600_NUM_PROFILES];
};
_Static_assert(sizeof(struct logitech_g600_profile_report) == LOGITECH_G600_REPORT_SIZE_PROFILE,
"Size of logitech_g600_profile_report is wrong");
struct logitech_g600_button_type_mapping {
uint8_t raw;
enum ratbag_button_type type;
};
static const struct logitech_g600_button_type_mapping logitech_g600_button_type_mapping[] = {
{ 0, RATBAG_BUTTON_TYPE_LEFT },
{ 1, RATBAG_BUTTON_TYPE_RIGHT },
{ 2, RATBAG_BUTTON_TYPE_MIDDLE },
{ 3, RATBAG_BUTTON_TYPE_WHEEL_LEFT },
{ 4, RATBAG_BUTTON_TYPE_WHEEL_RIGHT },
{ 5, RATBAG_BUTTON_TYPE_PINKIE },
{ 6, RATBAG_BUTTON_TYPE_PROFILE_CYCLE_UP },
{ 7, RATBAG_BUTTON_TYPE_RESOLUTION_CYCLE_UP },
{ 8, RATBAG_BUTTON_TYPE_SIDE }, // 8 = G9 ... 19 = G20
{ 9, RATBAG_BUTTON_TYPE_SIDE },
{ 10, RATBAG_BUTTON_TYPE_SIDE },
{ 11, RATBAG_BUTTON_TYPE_SIDE },
{ 12, RATBAG_BUTTON_TYPE_SIDE },
{ 13, RATBAG_BUTTON_TYPE_SIDE },
{ 14, RATBAG_BUTTON_TYPE_SIDE },
{ 15, RATBAG_BUTTON_TYPE_SIDE },
{ 16, RATBAG_BUTTON_TYPE_SIDE },
{ 17, RATBAG_BUTTON_TYPE_SIDE },
{ 18, RATBAG_BUTTON_TYPE_SIDE },
{ 19, RATBAG_BUTTON_TYPE_SIDE },
};
static enum ratbag_button_type
logitech_g600_raw_to_button_type(uint8_t data)
{
const struct logitech_g600_button_type_mapping *mapping;
ARRAY_FOR_EACH(logitech_g600_button_type_mapping, mapping) {
if (mapping->raw == data)
return mapping->type;
}
return RATBAG_BUTTON_TYPE_UNKNOWN;
}
struct logitech_g600_button_mapping {
uint8_t raw;
struct ratbag_button_action action;
};
static struct logitech_g600_button_mapping logitech_g600_button_mapping[] = {
/* 0x00 is either key or unassigned. Must be handled separatly */
{ 0x01, BUTTON_ACTION_BUTTON(1) },
{ 0x02, BUTTON_ACTION_BUTTON(2) },
{ 0x03, BUTTON_ACTION_BUTTON(3) },
{ 0x04, BUTTON_ACTION_BUTTON(4) },
{ 0x05, BUTTON_ACTION_BUTTON(5) },
{ 0x11, BUTTON_ACTION_SPECIAL(RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_UP) },
{ 0x12, BUTTON_ACTION_SPECIAL(RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_DOWN) },
{ 0x13, BUTTON_ACTION_SPECIAL(RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_CYCLE_UP) },
{ 0x14, BUTTON_ACTION_SPECIAL(RATBAG_BUTTON_ACTION_SPECIAL_PROFILE_CYCLE_UP) },
{ 0x15, BUTTON_ACTION_SPECIAL(RATBAG_BUTTON_ACTION_SPECIAL_RESOLUTION_ALTERNATE) },
{ 0x17, BUTTON_ACTION_SPECIAL(RATBAG_BUTTON_ACTION_SPECIAL_SECOND_MODE) },
};
static const struct ratbag_button_action*
logitech_g600_raw_to_button_action(uint8_t data)
{
struct logitech_g600_button_mapping *mapping;
ARRAY_FOR_EACH(logitech_g600_button_mapping, mapping) {
if (mapping->raw == data)
return &mapping->action;
}
return NULL;
}
static uint8_t logitech_g600_modifier_to_raw(int modifier_flags)
{
uint8_t modifiers = 0x00;
if (modifier_flags & MODIFIER_LEFTCTRL)
modifiers |= 0x01;
if (modifier_flags & MODIFIER_LEFTSHIFT)
modifiers |= 0x02;
if (modifier_flags & MODIFIER_LEFTALT)
modifiers |= 0x04;
if (modifier_flags & MODIFIER_LEFTMETA)
modifiers |= 0x08;
if (modifier_flags & MODIFIER_RIGHTCTRL)
modifiers |= 0x10;
if (modifier_flags & MODIFIER_RIGHTSHIFT)
modifiers |= 0x20;
if (modifier_flags & MODIFIER_RIGHTALT)
modifiers |= 0x40;
if (modifier_flags & MODIFIER_RIGHTMETA)
modifiers |= 0x80;
return modifiers;
}
static int logitech_g600_raw_to_modifiers(uint8_t data)
{
int modifiers = 0;
if (data & 0x01)
modifiers |= MODIFIER_LEFTCTRL;
if (data & 0x02)
modifiers |= MODIFIER_LEFTSHIFT;
if (data & 0x04)
modifiers |= MODIFIER_LEFTALT;
if (data & 0x08)
modifiers |= MODIFIER_LEFTMETA;
if (data & 0x10)
modifiers |= MODIFIER_RIGHTCTRL;
if (data & 0x20)
modifiers |= MODIFIER_RIGHTSHIFT;
if (data & 0x40)
modifiers |= MODIFIER_RIGHTALT;
if (data & 0x80)
modifiers |= MODIFIER_RIGHTMETA;
return modifiers;
}
static uint8_t
logitech_g600_button_action_to_raw(const struct ratbag_button_action *action)
{
struct logitech_g600_button_mapping *mapping;
ARRAY_FOR_EACH(logitech_g600_button_mapping, mapping) {
if (ratbag_button_action_match(&mapping->action, action))
return mapping->raw;
}
return 0;
}
static int
logitech_g600_get_active_profile_and_resolution(struct ratbag_device *device)
{
struct ratbag_profile *profile;
struct logitech_g600_active_profile_report buf;
int ret;
ret = ratbag_hidraw_raw_request(device, LOGITECH_G600_REPORT_ID_GET_ACTIVE,
(uint8_t*)&buf, sizeof(buf),
HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
if (ret < 0)
return ret;
if (ret != sizeof(buf))
return -EIO;
list_for_each(profile, &device->profiles, link) {
struct ratbag_resolution *resolution;
if (profile->index != buf.profile)
continue;
profile->is_active = true;
ratbag_profile_for_each_resolution(profile, resolution) {
resolution->is_active = resolution->index == buf.resolution;
}
}
return 0;
}
static int
logitech_g600_set_current_resolution(struct ratbag_device *device, unsigned int index)
{
uint8_t buf[] = {LOGITECH_G600_REPORT_ID_SET_ACTIVE, 0x40 | (index << 1), 0x00, 0x00};
int ret;
log_debug(device->ratbag, "Setting active resolution to %d\n", index);
if (index >= LOGITECH_G600_NUM_DPI)
return -EINVAL;
ret = ratbag_hidraw_raw_request(device, buf[0], buf, sizeof(buf),
HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
return ret == sizeof(buf) ? 0 : ret;
}
static int
logitech_g600_set_active_profile(struct ratbag_device *device, unsigned int index)
{
struct ratbag_profile *profile;
uint8_t buf[] = {LOGITECH_G600_REPORT_ID_SET_ACTIVE, 0x80 | (index << 4), 0x00, 0x00};
int ret, active_resolution = 0;
if (index >= LOGITECH_G600_NUM_PROFILES)
return -EINVAL;
ret = ratbag_hidraw_raw_request(device, buf[0], buf, sizeof(buf),
HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
if (ret != sizeof(buf))
return ret;
/* Update the active resolution. After profile change the default is used. */
list_for_each(profile, &device->profiles, link) {
struct ratbag_resolution *resolution;
if (profile->index != index)
continue;
ratbag_profile_for_each_resolution(profile, resolution) {
resolution->is_active = resolution->is_default;
if (resolution->is_active)
active_resolution = resolution->index;
}
}
ret = logitech_g600_set_current_resolution(device, active_resolution);
if (ret < 0)
return ret;
return 0;
}
static void
logitech_g600_read_button(struct ratbag_button *button)
{
const struct ratbag_button_action *action;
struct ratbag_profile *profile = button->profile;
struct ratbag_device *device = profile->device;
struct logitech_g600_data *drv_data = device->drv_data;
struct logitech_g600_profile_data *pdata;
struct logitech_g600_profile_report *profile_report;
struct logitech_g600_button *button_report;
pdata = &drv_data->profile_data[profile->index];
profile_report = &pdata->report;
button_report = &profile_report->buttons[button->index];
ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_BUTTON);
ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_SPECIAL);
ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_KEY);
ratbag_button_enable_action_type(button, RATBAG_BUTTON_ACTION_TYPE_MACRO);
button->type = logitech_g600_raw_to_button_type(button->index);
action = logitech_g600_raw_to_button_action(button_report->code);
if (action) {
ratbag_button_set_action(button, action);
}
else if (button_report->code == 0x00 && (button_report->modifier > 0x00 || button_report->key > 0x00))
{
unsigned int key, modifiers;
int rc;
key = ratbag_hidraw_get_keycode_from_keyboard_usage(device,
button_report->key);
modifiers = logitech_g600_raw_to_modifiers(button_report->modifier);
rc = ratbag_button_macro_new_from_keycode(button, key, modifiers);
if (rc < 0) {
log_error(device->ratbag,
"Error while reading button %d\n",
button->index);
button->action.type = RATBAG_BUTTON_ACTION_TYPE_NONE;
}
}
}
static void
logitech_g600_read_led(struct ratbag_led *led)
{
struct ratbag_profile *profile = led->profile;
struct ratbag_device *device = profile->device;
struct logitech_g600_data *drv_data = device->drv_data;
struct logitech_g600_profile_data *pdata;
struct logitech_g600_profile_report *report;
pdata = &drv_data->profile_data[profile->index];
report = &pdata->report;
led->type = RATBAG_LED_TYPE_SIDE;
led->colordepth = RATBAG_LED_COLORDEPTH_RGB_888;
ratbag_led_set_mode_capability(led, RATBAG_LED_OFF);
ratbag_led_set_mode_capability(led, RATBAG_LED_ON);
ratbag_led_set_mode_capability(led, RATBAG_LED_BREATHING);
ratbag_led_set_mode_capability(led, RATBAG_LED_CYCLE);
switch (report->led_effect) {
case LOGITECH_G600_LED_SOLID:
led->mode = RATBAG_LED_ON;
break;
case LOGITECH_G600_LED_BREATHE:
led->mode = RATBAG_LED_BREATHING;
led->ms = report->led_duration * 1000;
break;
case LOGITECH_G600_LED_CYCLE:
led->mode = RATBAG_LED_CYCLE;
led->ms = report->led_duration * 1000;
break;
}
led->color.red = report->led_red;
led->color.green = report->led_green;
led->color.blue = report->led_blue;
}
static void
logitech_g600_read_profile(struct ratbag_profile *profile)
{
struct ratbag_device *device = profile->device;
struct logitech_g600_data *drv_data = device->drv_data;
struct logitech_g600_profile_data *pdata;
struct logitech_g600_profile_report *report;
struct ratbag_resolution *resolution;
unsigned int report_rates[] = { 125, 142, 166, 200, 250, 333, 500, 1000 };
uint8_t report_id;
int rc;
struct ratbag_button *button;
struct ratbag_led *led;
assert(profile->index < LOGITECH_G600_NUM_PROFILES);
pdata = &drv_data->profile_data[profile->index];
report = &pdata->report;
switch (profile->index) {
case 0: report_id = LOGITECH_G600_REPORT_ID_PROFILE_0; break;
case 1: report_id = LOGITECH_G600_REPORT_ID_PROFILE_1; break;
case 2: report_id = LOGITECH_G600_REPORT_ID_PROFILE_2; break;
}
rc = ratbag_hidraw_raw_request(device, report_id,
(uint8_t*)report,
sizeof(*report),
HID_FEATURE_REPORT,
HID_REQ_GET_REPORT);
if (rc < (int)sizeof(*report)) {
log_error(device->ratbag,
"Error while requesting profile: %d\n", rc);
return;
}
ratbag_profile_for_each_resolution(profile, resolution) {
resolution->dpi_x = report->dpi[resolution->index] * 50;
resolution->dpi_y = report->dpi[resolution->index] * 50;
resolution->hz = 1000 / (report->frequency + 1);
resolution->is_default = report->dpi_default - 1U == resolution->index;
resolution->is_active = resolution->is_default;
ratbag_resolution_set_dpi_list_from_range(resolution,
LOGITECH_G600_DPI_MIN,
LOGITECH_G600_DPI_MAX);
ratbag_resolution_set_report_rate_list(resolution, report_rates,
ARRAY_LENGTH(report_rates));
}
ratbag_profile_for_each_button(profile, button)
logitech_g600_read_button(button);
ratbag_profile_for_each_led(profile, led)
logitech_g600_read_led(led);
log_debug(device->ratbag, "Unknown data in profile %d\n", profile->index);
log_buf_debug(device->ratbag, " profile->unknown1: ",
report->unknown1, 5);
log_buf_debug(device->ratbag, " profile->unknown2: ",
report->unknown2, 13);
}
static int
logitech_g600_test_hidraw(struct ratbag_device *device)
{
return ratbag_hidraw_has_report(device, LOGITECH_G600_REPORT_ID_GET_ACTIVE);
}
static int
logitech_g600_probe(struct ratbag_device *device)
{
int rc;
struct logitech_g600_data *drv_data = NULL;
struct ratbag_profile *profile;
rc = ratbag_find_hidraw(device, logitech_g600_test_hidraw);
if (rc)
goto err;
drv_data = zalloc(sizeof(*drv_data));
ratbag_set_drv_data(device, drv_data);
ratbag_device_init_profiles(device,
LOGITECH_G600_NUM_PROFILES,
LOGITECH_G600_NUM_DPI,
LOGITECH_G600_NUM_BUTTONS,
LOGITECH_G600_NUM_LED);
ratbag_device_for_each_profile(device, profile)
logitech_g600_read_profile(profile);
ratbag_device_set_capability(device, RATBAG_DEVICE_CAP_RESOLUTION);
ratbag_device_set_capability(device, RATBAG_DEVICE_CAP_PROFILE);
ratbag_device_set_capability(device, RATBAG_DEVICE_CAP_BUTTON);
ratbag_device_set_capability(device, RATBAG_DEVICE_CAP_BUTTON_KEY);
ratbag_device_set_capability(device, RATBAG_DEVICE_CAP_BUTTON_MACROS);
rc = logitech_g600_get_active_profile_and_resolution(device);
if (rc < 0) {
log_error(device->ratbag,
"Can't talk to the mouse: '%s' (%d)\n",
strerror(-rc),
rc);
rc = -ENODEV;
goto err;
}
return 0;
err:
free(drv_data);
ratbag_set_drv_data(device, NULL);
return rc;
}
static int
logitech_g600_write_profile(struct ratbag_profile *profile)
{
struct ratbag_device *device = profile->device;
struct logitech_g600_data *drv_data = device->drv_data;
struct logitech_g600_profile_data *pdata;
struct logitech_g600_profile_report *report;
struct ratbag_resolution *resolution;
struct ratbag_button *button;
struct ratbag_led *led;
uint8_t *buf;
int rc, active_resolution = 0;
pdata = &drv_data->profile_data[profile->index];
report = &pdata->report;
ratbag_profile_for_each_resolution(profile, resolution) {
report->dpi[resolution->index] = resolution->dpi_x / 50;
if (resolution->is_default)
report->dpi_default = resolution->index + 1;
if (profile->is_active && resolution->is_active)
active_resolution = resolution->index;
/* The same hz is used for all resolutions */
if (resolution->index == 0)
report->frequency = (1000 / resolution->hz) - 1;
}
list_for_each(button, &profile->buttons, link) {
struct ratbag_button_action *action = &button->action;
struct logitech_g600_button *raw_button;
raw_button = &report->buttons[button->index];
raw_button->code = logitech_g600_button_action_to_raw(action);
raw_button->modifier = 0x00;
raw_button->key = 0x00;
if (action->type == RATBAG_BUTTON_ACTION_TYPE_MACRO) {
unsigned int key, modifiers;
rc = ratbag_action_keycode_from_macro(action,
&key,
&modifiers);
if (rc < 0) {
log_error(device->ratbag,
"Error while writing macro for button %d\n",
button->index);
}
raw_button->key = ratbag_hidraw_get_keyboard_usage_from_keycode(
device, key);
raw_button->modifier = logitech_g600_modifier_to_raw(modifiers);
}
}
list_for_each(led, &profile->leds, link) {
report->led_red = led->color.red;
report->led_green = led->color.green;
report->led_blue = led->color.blue;
switch (led->mode) {
case RATBAG_LED_ON:
report->led_effect = LOGITECH_G600_LED_SOLID;
break;
case RATBAG_LED_OFF:
report->led_effect = LOGITECH_G600_LED_SOLID;
report->led_red = 0x00;
report->led_green = 0x00;
report->led_blue = 0x00;
break;
case RATBAG_LED_BREATHING:
report->led_effect = LOGITECH_G600_LED_BREATHE;
report->led_duration = led->ms / 1000;
break;
case RATBAG_LED_CYCLE:
report->led_effect = LOGITECH_G600_LED_CYCLE;
report->led_duration = led->ms / 1000;
break;
}
if (report->led_duration > 0x0f)
report->led_duration = 0x0f;
}
buf = (uint8_t*)report;
rc = ratbag_hidraw_raw_request(device, report->id,
buf,
sizeof(*report),
HID_FEATURE_REPORT,
HID_REQ_SET_REPORT);
if (rc < (int)sizeof(*report)) {
log_error(device->ratbag,
"Error while writing profile: %d\n", rc);
return rc;
}
if (profile->is_active) {
rc = logitech_g600_set_current_resolution(device, active_resolution);
if (rc < 0)
return rc;
}
return 0;
}
static int
logitech_g600_commit(struct ratbag_device *device)
{
struct ratbag_profile *profile;
int rc = 0;
list_for_each(profile, &device->profiles, link) {
if (!profile->dirty)
continue;
log_debug(device->ratbag,
"Profile %d changed, rewriting\n", profile->index);
rc = logitech_g600_write_profile(profile);
if (rc)
return rc;
}
return 0;
}
static void
logitech_g600_remove(struct ratbag_device *device)
{
ratbag_close_hidraw(device);
free(ratbag_get_drv_data(device));
}
struct ratbag_driver logitech_g600_driver = {
.name = "Logitech G600",
.id = "logitech_g600",
.probe = logitech_g600_probe,
.remove = logitech_g600_remove,
.commit = logitech_g600_commit,
.set_active_profile = logitech_g600_set_active_profile,
};