// 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>
*/
struct lg_g15_led { union { struct led_classdev cdev; struct led_classdev_mc mcdev;
}; enum led_brightness brightness; enum lg_g15_led_type led; /* Used to store initial color intensities before subled_info is allocated */
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 ********/
staticint 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;
}
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);
}
ret = hid_hw_raw_request(g15->hdev, LG_G510_FEATURE_M_KEYS_LEDS,
g15->transfer_buf, 2,
HID_FEATURE_REPORT, HID_REQ_SET_REPORT); if (ret == 2) { /* 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;
}
/******** Generic LED functions ********/ staticint 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 lg_g510_update_mkey_led_brightness(g15); case LG_Z10: /* * Getting the LCD backlight brightness is not supported. * Reading Feature(2) fails with -EPIPE and this crashes * the LCD and touch keys part of the speakers.
*/ return 0;
} return -EINVAL; /* Never reached */
}
/******** Input functions ********/
/* On the G15 Mark I Logitech has been quite creative with which bit is what */ staticvoid lg_g15_handle_lcd_menu_keys(struct lg_g15_data *g15, u8 *data)
{ int i, val;
/* Most left (round/display) button below the LCD */
input_report_key(g15->input, KEY_KBD_LCD_MENU1, data[8] & 0x80); /* 4 other buttons below the LCD */ for (i = 0; i < 4; i++) {
val = data[i + 2] & 0x80;
input_report_key(g15->input, KEY_KBD_LCD_MENU2 + i, val);
}
}
staticint lg_g15_event(struct lg_g15_data *g15, u8 *data)
{ int i, val;
/* G1 - G6 */ for (i = 0; i < 6; i++) {
val = data[i + 1] & (1 << i);
input_report_key(g15->input, KEY_MACRO1 + i, val);
} /* G7 - G12 */ for (i = 0; i < 6; i++) {
val = data[i + 2] & (1 << i);
input_report_key(g15->input, KEY_MACRO7 + i, val);
} /* G13 - G17 */ for (i = 0; i < 5; i++) {
val = data[i + 1] & (4 << i);
input_report_key(g15->input, KEY_MACRO13 + i, val);
} /* G18 */
input_report_key(g15->input, KEY_MACRO18, data[8] & 0x40);
/* M1 - M3 */ for (i = 0; i < 3; i++) {
val = data[i + 6] & (1 << i);
input_report_key(g15->input, KEY_MACRO_PRESET1 + i, val);
} /* MR */
input_report_key(g15->input, KEY_MACRO_RECORD_START, data[7] & 0x40);
/* Round button to the left of the LCD */
input_report_key(g15->input, KEY_KBD_LCD_MENU1, data[2] & 0x80); /* 4 buttons below the LCD */ for (i = 0; i < 4; i++) {
val = data[2] & (2 << i);
input_report_key(g15->input, KEY_KBD_LCD_MENU2 + i, val);
}
/* * 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);
switch (g15->model) { case LG_G15: case LG_G15_V2:
g15->leds[i].cdev.brightness_get = lg_g15_led_get;
fallthrough; case LG_Z10:
g15->leds[i].cdev.brightness_set_blocking = lg_g15_led_set; 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;
}
ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev); 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";
fallthrough; case LG_G15_KBD_BRIGHTNESS: /* register multicolor LED */
lg_g15_setup_led_rgb(g15, i);
ret = devm_led_classdev_multicolor_register_ext(&g15->hdev->dev,
&g15->leds[i].mcdev,
NULL); break; default:
g15->leds[i].cdev.brightness_set_blocking =
lg_g510_mkey_led_set;
g15->leds[i].cdev.brightness_get =
lg_g510_mkey_led_get;
g15->leds[i].cdev.max_brightness = 1;
ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
} break;
}
return ret;
}
/* Common input device init code shared between keyboards and Z-10 speaker handling */ staticvoid lg_g15_init_input_dev(struct hid_device *hdev, struct input_dev *input, constchar *name)
{ int i;
/* Keys below the LCD, intended for controlling a menu on the LCD */ for (i = 0; i < 5; i++)
input_set_capability(input, EV_KEY, KEY_KBD_LCD_MENU1 + i);
}
/* * Some models have multiple interfaces, we want the interface with * the f000.0000 application input report.
*/
rep_enum = &hdev->report_enum[HID_INPUT_REPORT];
list_for_each_entry(rep, &rep_enum->report_list, list) { if (rep->application == 0xff000000)
has_ff000000 = true;
} if (!has_ff000000) return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
g15 = devm_kzalloc(&hdev->dev, sizeof(*g15), GFP_KERNEL); if (!g15) return -ENOMEM;
mutex_init(&g15->mutex);
input = devm_input_allocate_device(&hdev->dev); if (!input) return -ENOMEM;
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 * on the G-keys, which we disable, rendering the emulated kbd * non-functional, so we do not let hid-input connect.
*/
connect_mask = HID_CONNECT_HIDRAW;
gkeys_settings_output_report = 0x02;
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; break; case LG_Z10:
connect_mask = HID_CONNECT_HIDRAW; break;
}
ret = hid_hw_start(hdev, connect_mask); if (ret) return ret;
/* Tell the keyboard to stop sending F1-F12 + 1-6 for G1 - G18 */ if (gkeys_settings_output_report) {
g15->transfer_buf[0] = gkeys_settings_output_report;
memset(g15->transfer_buf + 1, 0, gkeys); /* * The kbd ignores our output report if we do not queue * an URB on the USB input endpoint first...
*/
ret = hid_hw_open(hdev); if (ret) goto error_hw_stop;
ret = hid_hw_output_report(hdev, g15->transfer_buf, gkeys + 1);
hid_hw_close(hdev);
}
if (ret < 0) {
hid_err(hdev, "Error %d disabling keyboard emulation for the G-keys, falling back to generic hid-input driver\n",
ret);
hid_set_drvdata(hdev, NULL); return 0;
}
/* Get initial brightness levels */
ret = lg_g15_get_initial_led_brightness(g15); if (ret) goto error_hw_stop;
if (g15->model == LG_Z10) {
lg_g15_init_input_dev(hdev, g15->input, "Logitech Z-10 LCD Menu Keys");
ret = input_register_device(g15->input); if (ret) goto error_hw_stop;
ret = lg_g15_register_led(g15, 1, "z-10::lcd_backlight"); if (ret) goto error_hw_stop;
/* G-keys */ for (i = 0; i < gkeys; i++)
input_set_capability(input, EV_KEY, KEY_MACRO1 + i);
/* M1 - M3 and MR keys */ for (i = 0; i < 3; i++)
input_set_capability(input, EV_KEY, KEY_MACRO_PRESET1 + i);
input_set_capability(input, EV_KEY, KEY_MACRO_RECORD_START);
/* * On the G510 only report headphone and mic mute keys when *not* using * the builtin USB audio device. When the builtin audio is used these * keys directly toggle mute (and the LEDs) on/off.
*/ if (g15->model == LG_G510) {
input_set_capability(input, EV_KEY, KEY_MUTE); /* Userspace expects F20 for micmute */
input_set_capability(input, EV_KEY, KEY_F20);
}
ret = input_register_device(input); if (ret) goto error_hw_stop;
/* Register LED devices */ for (i = 0; i < LG_G15_LED_MAX; i++) {
ret = lg_g15_register_led(g15, i, led_names[i]); if (ret) goto error_hw_stop;
}
return 0;
error_hw_stop:
hid_hw_stop(hdev); return ret;
}
staticconststruct hid_device_id lg_g15_devices[] = { /* The G11 is a G15 without the LCD, treat it as a G15 */
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_G11),
.driver_data = LG_G15 },
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_G15_LCD),
.driver_data = LG_G15 },
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_G15_V2_LCD),
.driver_data = LG_G15_V2 }, /* G510 without a headset plugged in */
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_G510),
.driver_data = LG_G510 }, /* G510 with headset plugged in / with extra USB audio interface */
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_G510_USB_AUDIO),
.driver_data = LG_G510_USB_AUDIO }, /* Z-10 speakers */
{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_Z_10_SPK),
.driver_data = LG_Z10 },
{ }
};
MODULE_DEVICE_TABLE(hid, lg_g15_devices);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.