// SPDX-License-Identifier: GPL-2.0+
/*
* HID driver for gaming keys on Logitech gaming keyboards (such as the G15)
*
* Copyright (c) 2019 Hans de Goede <hdegoede@redhat.com>
*/
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/sched.h>
#include <linux/usb.h>
#include <linux/wait.h>
#include "hid-ids.h"
#define LG_G15_TRANSFER_BUF_SIZE 20
#define LG_G15_FEATURE_REPORT 0x02
#define LG_G510_FEATURE_M_KEYS_LEDS 0x04
#define LG_G510_FEATURE_BACKLIGHT_RGB 0x05
#define LG_G510_FEATURE_POWER_ON_RGB 0x06
enum lg_g15_model {
LG_G15,
LG_G15_V2,
LG_G510,
LG_G510_USB_AUDIO,
};
enum lg_g15_led_type {
LG_G15_KBD_BRIGHTNESS,
LG_G15_LCD_BRIGHTNESS,
LG_G15_BRIGHTNESS_MAX,
LG_G15_MACRO_PRESET1 = 2,
LG_G15_MACRO_PRESET2,
LG_G15_MACRO_PRESET3,
LG_G15_MACRO_RECORD,
LG_G15_LED_MAX
};
struct lg_g15_led {
struct led_classdev cdev;
enum led_brightness brightness;
enum lg_g15_led_type led;
u8 red, green, blue;
};
struct lg_g15_data {
/* Must be first for proper dma alignment */
u8 transfer_buf[LG_G15_TRANSFER_BUF_SIZE];
/* Protects the transfer_buf and led brightness */
struct mutex mutex;
struct work_struct work;
struct input_dev *input;
struct hid_device *hdev;
enum lg_g15_model model;
struct lg_g15_led leds[LG_G15_LED_MAX];
bool game_mode_enabled;
};
/******** G15 and G15 v2 LED functions ********/
static int lg_g15_update_led_brightness(struct lg_g15_data *g15)
{
int ret;
ret = hid_hw_raw_request(g15->hdev, LG_G15_FEATURE_REPORT,
g15->transfer_buf, 4,
HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
if (ret != 4) {
hid_err(g15->hdev, "Error getting LED brightness: %d\n", ret);
return (ret < 0) ? ret : -EIO;
}
g15->leds[LG_G15_KBD_BRIGHTNESS].brightness = g15->transfer_buf[1];
g15->leds[LG_G15_LCD_BRIGHTNESS].brightness = g15->transfer_buf[2];
g15->leds[LG_G15_MACRO_PRESET1].brightness =
!(g15->transfer_buf[3] & 0x01);
g15->leds[LG_G15_MACRO_PRESET2].brightness =
!(g15->transfer_buf[3] & 0x02);
g15->leds[LG_G15_MACRO_PRESET3].brightness =
!(g15->transfer_buf[3] & 0x04);
g15->leds[LG_G15_MACRO_RECORD].brightness =
!(g15->transfer_buf[3] & 0x08);
return 0;
}
static enum led_brightness lg_g15_led_get(struct led_classdev *led_cdev)
{
struct lg_g15_led *g15_led =
container_of(led_cdev, struct lg_g15_led, cdev);
struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
enum led_brightness brightness;
mutex_lock(&g15->mutex);
lg_g15_update_led_brightness(g15);
brightness = g15->leds[g15_led->led].brightness;
mutex_unlock(&g15->mutex);
return brightness;
}
static int lg_g15_led_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct lg_g15_led *g15_led =
container_of(led_cdev, struct lg_g15_led, cdev);
struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
u8 val, mask = 0;
int i, ret;
/* Ignore LED off on unregister / keyboard unplug */
if (led_cdev->flags & LED_UNREGISTERING)
return 0;
mutex_lock(&g15->mutex);
g15->transfer_buf[0] = LG_G15_FEATURE_REPORT;
g15->transfer_buf[3] = 0;
if (g15_led->led < LG_G15_BRIGHTNESS_MAX) {
g15->transfer_buf[1] = g15_led->led + 1;
g15->transfer_buf[2] = brightness << (g15_led->led * 4);
} else {
for (i = LG_G15_MACRO_PRESET1; i < LG_G15_LED_MAX; i++) {
if (i == g15_led->led)
val = brightness;
else
val = g15->leds[i].brightness;
if (val)
mask |= 1 << (i - LG_G15_MACRO_PRESET1);
}
g15->transfer_buf[1] = 0x04;
g15->transfer_buf[2] = ~mask;
}
ret = hid_hw_raw_request(g15->hdev, LG_G15_FEATURE_REPORT,
g15->transfer_buf, 4,
HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
if (ret == 4) {
/* Success */
g15_led->brightness = brightness;
ret = 0;
} else {
hid_err(g15->hdev, "Error setting LED brightness: %d\n", ret);
ret = (ret < 0) ? ret : -EIO;
}
mutex_unlock(&g15->mutex);
return ret;
}
static void lg_g15_leds_changed_work(struct work_struct *work)
{