summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--drivers/hid/hid-lg-g15.c272
1 files changed, 257 insertions, 15 deletions
diff --git a/drivers/hid/hid-lg-g15.c b/drivers/hid/hid-lg-g15.c
index b0891503fbcf..b8ea059c46f3 100644
--- a/drivers/hid/hid-lg-g15.c
+++ b/drivers/hid/hid-lg-g15.c
@@ -19,6 +19,10 @@
#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,
@@ -41,6 +45,7 @@ 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 {
@@ -56,13 +61,12 @@ struct lg_g15_data {
bool game_mode_enabled;
};
+/******** G15 and G15 v2 LED functions ********/
+
static int lg_g15_update_led_brightness(struct lg_g15_data *g15)
{
int ret;
- if (g15->model == LG_G510 || g15->model == LG_G510_USB_AUDIO)
- return 0;
-
ret = hid_hw_raw_request(g15->hdev, LG_G15_FEATURE_REPORT,
g15->transfer_buf, 4,
HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
@@ -183,6 +187,198 @@ static void lg_g15_leds_changed_work(struct work_struct *work)
}
}
+/******** G510 LED functions ********/
+
+static int lg_g510_get_initial_led_brightness(struct lg_g15_data *g15, int i)
+{
+ int ret, high;
+
+ ret = hid_hw_raw_request(g15->hdev, LG_G510_FEATURE_BACKLIGHT_RGB + i,
+ 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;
+ }
+
+ high = max3(g15->transfer_buf[1], g15->transfer_buf[2],
+ g15->transfer_buf[3]);
+
+ if (high) {
+ g15->leds[i].red =
+ DIV_ROUND_CLOSEST(g15->transfer_buf[1] * 255, high);
+ g15->leds[i].green =
+ DIV_ROUND_CLOSEST(g15->transfer_buf[2] * 255, high);
+ g15->leds[i].blue =
+ DIV_ROUND_CLOSEST(g15->transfer_buf[3] * 255, high);
+ g15->leds[i].brightness = high;
+ } else {
+ g15->leds[i].red = 255;
+ g15->leds[i].green = 255;
+ g15->leds[i].blue = 255;
+ g15->leds[i].brightness = 0;
+ }
+
+ return 0;
+}
+
+/* Must be called with g15->mutex locked */
+static int lg_g510_kbd_led_write(struct lg_g15_data *g15,
+ struct lg_g15_led *g15_led,
+ enum led_brightness brightness)
+{
+ int ret;
+
+ g15->transfer_buf[0] = 5 + g15_led->led;
+ g15->transfer_buf[1] =
+ DIV_ROUND_CLOSEST(g15_led->red * brightness, 255);
+ g15->transfer_buf[2] =
+ DIV_ROUND_CLOSEST(g15_led->green * brightness, 255);
+ g15->transfer_buf[3] =
+ DIV_ROUND_CLOSEST(g15_led->blue * brightness, 255);
+
+ ret = hid_hw_raw_request(g15->hdev,
+ LG_G510_FEATURE_BACKLIGHT_RGB + g15_led->led,
+ 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;
+ }
+
+ return ret;
+}
+
+static int lg_g510_kbd_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);
+ int ret;
+
+ /* Ignore LED off on unregister / keyboard unplug */
+ if (led_cdev->flags & LED_UNREGISTERING)
+ return 0;
+
+ mutex_lock(&g15->mutex);
+ ret = lg_g510_kbd_led_write(g15, g15_led, brightness);
+ mutex_unlock(&g15->mutex);
+
+ return ret;
+}
+
+static enum led_brightness lg_g510_kbd_led_get(struct led_classdev *led_cdev)
+{
+ struct lg_g15_led *g15_led =
+ container_of(led_cdev, struct lg_g15_led, cdev);
+
+ return g15_led->brightness;
+}
+
+static ssize_t color_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ 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);
+ unsigned long value;
+ int ret;
+
+ if (count < 7 || (count == 8 && buf[7] != '\n') || count > 8)
+ return -EINVAL;
+
+ if (buf[0] != '#')
+ return -EINVAL;
+
+ ret = kstrtoul(buf + 1, 16, &value);
+ if (ret)
+ return ret;
+
+ mutex_lock(&g15->mutex);
+ g15_led->red = (value & 0xff0000) >> 16;
+ g15_led->green = (value & 0x00ff00) >> 8;
+ g15_led->blue = (value & 0x0000ff);
+ ret = lg_g510_kbd_led_write(g15, g15_led, g15_led->brightness);
+ mutex_unlock(&g15->mutex);
+
+ return (ret < 0) ? ret : count;
+}
+
+static ssize_t color_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ 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);
+ ssize_t ret;
+
+ mutex_lock(&g15->mutex);
+ ret = sprintf(buf, "#%02x%02x%02x\n",
+ g15_led->red, g15_led->green, g15_led->blue);
+ mutex_unlock(&g15->mutex);
+
+ return ret;
+}
+
+static DEVICE_ATTR_RW(color);
+
+static struct attribute *lg_g510_kbd_led_attrs[] = {
+ &dev_attr_color.attr,
+ NULL,
+};
+
+static const struct attribute_group lg_g510_kbd_led_group = {
+ .attrs = lg_g510_kbd_led_attrs,
+};
+
+static const struct attribute_group *lg_g510_kbd_led_groups[] = {
+ &lg_g510_kbd_led_group,
+ NULL,
+};
+
+static void lg_g510_leds_sync_work(struct work_struct *work)
+{
+ struct lg_g15_data *g15 = container_of(work, struct lg_g15_data, work);
+
+ mutex_lock(&g15->mutex);
+ lg_g510_kbd_led_write(g15, &g15->leds[LG_G15_KBD_BRIGHTNESS],
+ g15->leds[LG_G15_KBD_BRIGHTNESS].brightness);
+ mutex_unlock(&g15->mutex);
+}
+
+/******** Generic LED functions ********/
+static int lg_g15_get_initial_led_brightness(struct lg_g15_data *g15)
+{
+ int ret;
+
+ switch (g15->model) {
+ case LG_G15:
+ case LG_G15_V2:
+ return lg_g15_update_led_brightness(g15);
+ case LG_G510:
+ case LG_G510_USB_AUDIO:
+ ret = lg_g510_get_initial_led_brightness(g15, 0);
+ if (ret)
+ return ret;
+
+ ret = lg_g510_get_initial_led_brightness(g15, 1);
+ if (ret)
+ return ret;
+
+ return 0;
+ }
+ return -EINVAL; /* Never reached */
+}
+
+/******** Input functions ********/
+
/* On the G15 Mark I Logitech has been quite creative with which bit is what */
static int lg_g15_event(struct lg_g15_data *g15, u8 *data, int size)
{
@@ -306,6 +502,22 @@ static int lg_g510_event(struct lg_g15_data *g15, u8 *data, int size)
return 0;
}
+static int lg_g510_leds_event(struct lg_g15_data *g15, u8 *data, int size)
+{
+ bool backlight_disabled;
+
+ /*
+ * The G510 ignores backlight updates when the backlight is turned off
+ * through the light toggle button on the keyboard, to work around this
+ * we queue a workitem to sync values when the backlight is turned on.
+ */
+ backlight_disabled = data[1] & 0x04;
+ if (!backlight_disabled)
+ schedule_work(&g15->work);
+
+ return 0;
+}
+
static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
u8 *data, int size)
{
@@ -327,6 +539,8 @@ static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
case LG_G510_USB_AUDIO:
if (data[0] == 0x03 && size == 5)
return lg_g510_event(g15, data, size);
+ if (data[0] == 0x04 && size == 2)
+ return lg_g510_leds_event(g15, data, size);
break;
}
@@ -360,13 +574,42 @@ static int lg_g15_register_led(struct lg_g15_data *g15, int i)
g15->leds[i].led = i;
g15->leds[i].cdev.name = led_names[i];
- g15->leds[i].cdev.brightness_set_blocking = lg_g15_led_set;
- g15->leds[i].cdev.brightness_get = lg_g15_led_get;
- if (i < LG_G15_BRIGHTNESS_MAX) {
- g15->leds[i].cdev.flags = LED_BRIGHT_HW_CHANGED;
- g15->leds[i].cdev.max_brightness = 2;
- } else {
- g15->leds[i].cdev.max_brightness = 1;
+
+ switch (g15->model) {
+ case LG_G15:
+ case LG_G15_V2:
+ g15->leds[i].cdev.brightness_set_blocking = lg_g15_led_set;
+ g15->leds[i].cdev.brightness_get = lg_g15_led_get;
+ if (i < LG_G15_BRIGHTNESS_MAX) {
+ g15->leds[i].cdev.flags = LED_BRIGHT_HW_CHANGED;
+ g15->leds[i].cdev.max_brightness = 2;
+ } else {
+ g15->leds[i].cdev.max_brightness = 1;
+ }
+ break;
+ case LG_G510:
+ case LG_G510_USB_AUDIO:
+ switch (i) {
+ case LG_G15_LCD_BRIGHTNESS:
+ /*
+ * The G510 does not have a separate LCD brightness,
+ * but it does have a separate power-on (reset) value.
+ */
+ g15->leds[i].cdev.name = "g15::power_on_backlight_val";
+ /* fall through */
+ case LG_G15_KBD_BRIGHTNESS:
+ g15->leds[i].cdev.brightness_set_blocking =
+ lg_g510_kbd_led_set;
+ g15->leds[i].cdev.brightness_get =
+ lg_g510_kbd_led_get;
+ g15->leds[i].cdev.max_brightness = 255;
+ g15->leds[i].cdev.groups = lg_g510_kbd_led_groups;
+ break;
+ default:
+ /* TODO: Add support for M1 - M3 and MR leds */
+ return 0;
+ }
+ break;
}
return devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
@@ -414,11 +657,11 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
g15->hdev = hdev;
g15->model = id->driver_data;
- INIT_WORK(&g15->work, lg_g15_leds_changed_work);
hid_set_drvdata(hdev, (void *)g15);
switch (g15->model) {
case LG_G15:
+ INIT_WORK(&g15->work, lg_g15_leds_changed_work);
/*
* The G15 and G15 v2 use a separate usb-device (on a builtin
* hub) which emulates a keyboard for the F1 - F12 emulation
@@ -430,12 +673,14 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
gkeys = 18;
break;
case LG_G15_V2:
+ INIT_WORK(&g15->work, lg_g15_leds_changed_work);
connect_mask = HID_CONNECT_HIDRAW;
gkeys_settings_output_report = 0x02;
gkeys = 6;
break;
case LG_G510:
case LG_G510_USB_AUDIO:
+ INIT_WORK(&g15->work, lg_g510_leds_sync_work);
connect_mask = HID_CONNECT_HIDINPUT | HID_CONNECT_HIDRAW;
gkeys_settings_feature_report = 0x01;
gkeys = 18;
@@ -476,7 +721,7 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
/* Get initial brightness levels */
- ret = lg_g15_update_led_brightness(g15);
+ ret = lg_g15_get_initial_led_brightness(g15);
if (ret)
goto error_hw_stop;
@@ -523,9 +768,6 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (ret)
goto error_hw_stop;
- if (g15->model == LG_G510 || g15->model == LG_G510_USB_AUDIO)
- return 0;
-
/* Register LED devices */
for (i = 0; i < LG_G15_LED_MAX; i++) {
ret = lg_g15_register_led(g15, i);