So after some tinkering I have code now that succeeds in retrieving state via sending reports once. After that all following sends time out. I am at a loss what I am doing wrong, tbh. RFC below. On Thu, 10 Nov 2022, Benjamin Tissoires wrote: > > > > > I had a look at the hidpp utility > > sources: > > https://github.com/cvuchener/hidpp/blob/057407fbb7248bbc6cefcfaa860758d0711c01b9/src/libhidpp/hidpp/Device.cpp#L82 > > Which seems to do a similar thing. From the top of my head the only > > difference seems to be that they are sending `0x1` as a ping value instead > > of `0x5a`. Might give that a shot next. > > Anyway hidpp-list-features successfully reads the protocol version in > > userspace (4, 2) as seen here: > > https://github.com/abergmeier/litra_glow_linux/blob/main/hidpp-list-features > > Hmm... It would seem wrong for me if the firmware suddenly expects to > have a specific ping value. > If it works in userspace, it might also mean that the timing is not > right and we are talking to the device too early, and it can't answer > yet. I needed to set some specific quirk flags to make communication work. See below. RFC on current PATCH: diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index dad953f66996..78265f7235ce 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -856,6 +856,7 @@ #define USB_DEVICE_ID_MX5500_RECEIVER_MOUSE_DEV 0xc71c #define USB_DEVICE_ID_DINOVO_MINI_RECEIVER_KBD_DEV 0xc71e #define USB_DEVICE_ID_DINOVO_MINI_RECEIVER_MOUSE_DEV 0xc71f +#define USB_DEVICE_ID_LOGITECH_LITRA_GLOW 0xc900 #define USB_DEVICE_ID_LOGITECH_MOMO_WHEEL2 0xca03 #define USB_DEVICE_ID_LOGITECH_VIBRATION_WHEEL 0xca04 diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c index 71a9c258a20b..949fd09d2b43 100644 --- a/drivers/hid/hid-logitech-hidpp.c +++ b/drivers/hid/hid-logitech-hidpp.c @@ -11,6 +11,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/device.h> +#include <linux/dmi.h> #include <linux/input.h> #include <linux/usb.h> #include <linux/hid.h> @@ -99,6 +100,7 @@ MODULE_PARM_DESC(disable_tap_to_click, #define HIDPP_CAPABILITY_HIDPP20_HI_RES_WHEEL BIT(7) #define HIDPP_CAPABILITY_HIDPP20_HI_RES_SCROLL BIT(8) #define HIDPP_CAPABILITY_HIDPP10_FAST_SCROLL BIT(9) +#define HIDPP_CAPABILITY_ILLUMINATION_LIGHT BIT(10) #define lg_map_key_clear(c) hid_map_usage_clear(hi, usage, bit, max, EV_KEY, (c)) @@ -206,7 +208,10 @@ struct hidpp_device { struct hidpp_battery battery; struct hidpp_scroll_counter vertical_wheel_counter; - u8 wireless_feature_index; + union { + u8 wireless_feature_index; + u8 illumination_feature_index; + }; }; /* HID++ 1.0 error codes */ @@ -355,15 +360,16 @@ static int hidpp_send_fap_command_sync(struct hidpp_device *hidpp, } static int hidpp_send_rap_command_sync(struct hidpp_device *hidpp_dev, - u8 report_id, u8 sub_id, u8 reg_address, u8 *params, int param_count, + u8 sub_id, u8 reg_address, u8 *params, int param_count, struct hidpp_report *response) { struct hidpp_report *message; int ret, max_count; + u8 report_id; - /* Send as long report if short reports are not supported. */ - if (report_id == REPORT_ID_HIDPP_SHORT && - !(hidpp_dev->supported_reports & HIDPP_REPORT_SHORT_SUPPORTED)) + if (hidpp_dev->supported_reports & HIDPP_REPORT_SHORT_SUPPORTED) + report_id = REPORT_ID_HIDPP_SHORT; + else report_id = REPORT_ID_HIDPP_LONG; switch (report_id) { @@ -544,7 +550,6 @@ static int hidpp10_set_register(struct hidpp_device *hidpp_dev, u8 params[3] = { 0 }; ret = hidpp_send_rap_command_sync(hidpp_dev, - REPORT_ID_HIDPP_SHORT, HIDPP_GET_REGISTER, register_address, NULL, 0, &response); @@ -557,7 +562,6 @@ static int hidpp10_set_register(struct hidpp_device *hidpp_dev, params[byte] |= value & mask; return hidpp_send_rap_command_sync(hidpp_dev, - REPORT_ID_HIDPP_SHORT, HIDPP_SET_REGISTER, register_address, params, 3, &response); @@ -653,7 +657,6 @@ static int hidpp10_query_battery_status(struct hidpp_device *hidpp) int ret, status; ret = hidpp_send_rap_command_sync(hidpp, - REPORT_ID_HIDPP_SHORT, HIDPP_GET_REGISTER, HIDPP_REG_BATTERY_STATUS, NULL, 0, &response); @@ -705,7 +708,6 @@ static int hidpp10_query_battery_mileage(struct hidpp_device *hidpp) int ret, status; ret = hidpp_send_rap_command_sync(hidpp, - REPORT_ID_HIDPP_SHORT, HIDPP_GET_REGISTER, HIDPP_REG_BATTERY_MILEAGE, NULL, 0, &response); @@ -777,7 +779,6 @@ static char *hidpp_unifying_get_name(struct hidpp_device *hidpp_dev) int len; ret = hidpp_send_rap_command_sync(hidpp_dev, - REPORT_ID_HIDPP_SHORT, HIDPP_GET_LONG_REGISTER, HIDPP_REG_PAIRING_INFORMATION, params, 1, &response); @@ -811,7 +812,6 @@ static int hidpp_unifying_get_serial(struct hidpp_device *hidpp, u32 *serial) u8 params[1] = { HIDPP_EXTENDED_PAIRING }; ret = hidpp_send_rap_command_sync(hidpp, - REPORT_ID_HIDPP_SHORT, HIDPP_GET_LONG_REGISTER, HIDPP_REG_PAIRING_INFORMATION, params, 1, &response); @@ -862,6 +862,8 @@ static int hidpp_unifying_init(struct hidpp_device *hidpp) #define CMD_ROOT_GET_FEATURE 0x00 #define CMD_ROOT_GET_PROTOCOL_VERSION 0x10 +#define HIDPP_FEATURE_TYPE_HIDDEN 0x70 + static int hidpp_root_get_feature(struct hidpp_device *hidpp, u16 feature, u8 *feature_index, u8 *feature_type) { @@ -893,9 +895,8 @@ static int hidpp_root_get_protocol_version(struct hidpp_device *hidpp) int ret; ret = hidpp_send_rap_command_sync(hidpp, - REPORT_ID_HIDPP_SHORT, HIDPP_PAGE_ROOT_IDX, - CMD_ROOT_GET_PROTOCOL_VERSION, + CMD_ROOT_GET_PROTOCOL_VERSION | LINUX_KERNEL_SW_ID, ping_data, sizeof(ping_data), &response); if (ret == HIDPP_ERROR_INVALID_SUBID) { @@ -1729,6 +1730,361 @@ static int hidpp_set_wireless_feature_index(struct hidpp_device *hidpp) return ret; } +/* -------------------------------------------------------------------------- */ +/* 0x1990: Illumination Light */ +/* -------------------------------------------------------------------------- */ + +#define HIDPP_PAGE_ILLUMINATION_LIGHT 0x1990 + +#define HIDPP_ILLUMINATION_FUNC_GET 0x00 +#define HIDPP_ILLUMINATION_FUNC_SET 0x10 +#define HIDPP_ILLUMINATION_FUNC_GET_BRIGHTNESS_INFO 0x20 +#define HIDPP_ILLUMINATION_FUNC_GET_BRIGHTNESS 0x30 +#define HIDPP_ILLUMINATION_FUNC_SET_BRIGHTNESS 0x40 + +/* Not yet supported +#define HIDPP_ILLUMINATION_FUNC_GET_BRIGHTNESS_LEVELS 0x50 +#define HIDPP_ILLUMINATION_FUNC_SET_BRIGHTNESS_LEVELS 0x60 +*/ + +#define HIDPP_ILLUMINATION_FUNC_GET_COLOR_TEMPERATURE_INFO 0x70 +#define HIDPP_ILLUMINATION_FUNC_GET_COLOR_TEMPERATURE 0x80 +#define HIDPP_ILLUMINATION_FUNC_SET_COLOR_TEMPERATURE 0x90 + +/* Not yet supported +#define HIDPP_ILLUMINATION_FUNC_GET_COLOR_TEMPERATURE_LEVELS 0xA0 +#define HIDPP_ILLUMINATION_FUNC_SET_COLOR_TEMPERATURE_LEVELS 0xB0 +*/ + +#define HIDPP_ILLUMINATION_EVENT_CHANGE 0x00 +#define HIDPP_ILLUMINATION_EVENT_BRIGHTNESS_CHANGE 0x10 +#define HIDPP_ILLUMINATION_EVENT_COLOR_TEMPERATURE_CHANGE 0x20 + +#define HIDPP_ILLUMINATION_CAP_EVENTS BIT(0) +#define HIDPP_ILLUMINATION_CAP_LINEAR_LEVELS BIT(1) +#define HIDPP_ILLUMINATION_CAP_NON_LINEAR_LEVELS BIT(2) + +struct control_info { + u16 min; + u16 max; + u16 res; + u8 capabilities; + u8 max_levels; +}; + +struct led_data { + struct led_classdev cdev; + struct hidpp_device *drv_data; + struct hid_device *hdev; + u16 feature_index; + bool on; + u16 brightness; + struct control_info brightness_info; + struct control_info color_temperature_info; + char dirname[256]; + bool removed; +}; + +/* kernel led interface designates 0 as off. To not lose the ability to chose + * minimal brightness, we thus need to increase the reported range by 1 + */ +static unsigned device_to_led_brightness_value(struct led_data* led, u16 device_brightness) { + u16 relative = device_brightness - led->brightness_info.min; + u16 step = relative / led->brightness_info.res; + return step + 1; +} + +static unsigned device_to_led_brightness(struct led_data* led) { + return device_to_led_brightness_value(led, led->brightness); +} + +static u16 led_to_device_brightness_value(struct led_data* led, unsigned led_brightness) { + unsigned step = led_brightness - 1; + unsigned relative = step * led->brightness_info.res; + return led->brightness_info.min + relative; +} + +static enum led_brightness led_brightness_get(struct led_classdev *led_cdev) +{ + struct led_data *led = container_of(led_cdev, struct led_data, cdev); + struct hidpp_device *hidpp = led->drv_data; + u8 params[1] = { 0 }; + struct hidpp_report report; + int ret; + u16 be_brightness; + + + ret = hidpp_send_fap_command_sync(hidpp, + hidpp->illumination_feature_index, + HIDPP_ILLUMINATION_FUNC_GET, params, + 0, &report); + if (ret) { + hid_err(hidpp->hid_dev, "Getting Illumination failed\n"); + goto exit; + } + + + led->on = report.fap.params[0] & 0x01; + if (!led->on) { + return LED_OFF; + } + + ret = hidpp_send_fap_command_sync( + hidpp, hidpp->illumination_feature_index, + HIDPP_ILLUMINATION_FUNC_GET_BRIGHTNESS, params, 0, &report); + if (ret) { + hid_err(hidpp->hid_dev, + "Getting Illumination Brightness failed\n"); + goto exit; + } + + be_brightness = (report.fap.params[0] << 8) | + (report.fap.params[1] << 0); + led->brightness = be16_to_cpu(be_brightness); +exit: + return device_to_led_brightness(led); +} + +static void led_brightness_set_dummy(struct led_classdev *led_cdev, + enum led_brightness brightness) { +} + +static int led_brightness_set_sync(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_data *led = container_of(led_cdev, struct led_data, cdev); + struct hidpp_device *hidpp = led->drv_data; + u16 be_brightness; + struct hidpp_report report; + u8 params[2]; + int params_count = sizeof(params) / sizeof(*params); + int ret; + + + be_brightness = cpu_to_be16(led->brightness); + led->on = brightness != 0; + if (led->on) { + led->brightness = led_to_device_brightness_value(led, brightness); + } + + memzero_explicit(params, params_count); + params[0] = led->on ? 0x01 : 0x00; + ret = hidpp_send_fap_command_sync(hidpp, + hidpp->illumination_feature_index, + HIDPP_ILLUMINATION_FUNC_SET, params, + params_count, &report); + if (ret) { + hid_err(hidpp->hid_dev, "Setting Illumination failed\n"); + return ret; + } + + if (!led->on) + return 0; + params[0] = (be_brightness & 0xFF00) >> 8; + params[1] = (be_brightness & 0x00FF) >> 0; + ret = hidpp_send_fap_command_sync( + hidpp, hidpp->illumination_feature_index, + HIDPP_ILLUMINATION_FUNC_SET_BRIGHTNESS, params, params_count, + &report); + if (ret) { + hid_err(hidpp->hid_dev, + "Setting Illumination Brightness failed\n"); + return ret; + } + return ret; +} + +static int get_brightness_info_sync(struct hidpp_device *hidpp, + struct control_info *info) +{ + struct hidpp_report resp; + int ret = hidpp_send_fap_command_sync( + hidpp, hidpp->illumination_feature_index, + HIDPP_ILLUMINATION_FUNC_GET_BRIGHTNESS_INFO, NULL, 0, &resp); + if (ret) { + hid_err(hidpp->hid_dev, + "get_brightness_info_sync failed with %d\n", ret); + return ret; + } + + info->capabilities = resp.fap.params[0]; + info->min = + be16_to_cpu(resp.fap.params[1] << 8 | resp.fap.params[2] << 0); + info->max = + be16_to_cpu(resp.fap.params[3] << 8 | resp.fap.params[4] << 0); + info->res = + be16_to_cpu(resp.fap.params[5] << 8 | resp.fap.params[6] << 0); + info->max_levels = resp.fap.params[7]; + return 0; +} + +static int get_color_temperature_info_sync(struct hidpp_device *hidpp, + struct control_info *info) +{ + struct hidpp_report resp; + int ret = hidpp_send_fap_command_sync( + hidpp, hidpp->illumination_feature_index, + HIDPP_ILLUMINATION_FUNC_GET_COLOR_TEMPERATURE_INFO, NULL, 0, + &resp); + if (ret) { + hid_err(hidpp->hid_dev, + "get_color_temperature_info_sync failed with %d\n", + ret); + return ret; + } + + info->capabilities = resp.fap.params[0]; + info->min = + be16_to_cpu(resp.fap.params[1] << 8 | resp.fap.params[2] << 0); + info->max = + be16_to_cpu(resp.fap.params[3] << 8 | resp.fap.params[4] << 0); + info->res = + be16_to_cpu(resp.fap.params[5] << 8 | resp.fap.params[6] << 0); + info->max_levels = resp.fap.params[7]; + return 0; +} + +static int register_led(struct hidpp_device *hidpp) +{ + char buf[256]; + int ret; + unsigned brightness_range, r = 0, w = 0; + struct led_data *led = devm_kzalloc(&hidpp->hid_dev->dev, sizeof(struct led_data), + GFP_KERNEL); + + if (!led) + return -ENOMEM; + + ret = get_brightness_info_sync(hidpp, &led->brightness_info); + if (ret) + goto cleanup; + + ret = get_color_temperature_info_sync(hidpp, + &led->color_temperature_info); + if (ret) + goto cleanup; + + led->drv_data = hidpp; + led->removed = false; + memzero_explicit(buf, 256); + strscpy(buf, hidpp->name, 256); + while (w != 256) { + char c = buf[r]; + if (c == '\'' || c == '\"') { + if (r != 255) { + r++; + } + continue; + } + buf[w] = buf[r]; + w++; + if (r != 255) { + r++; + } + } + strreplace(buf, ' ', '_'); + snprintf(led->dirname, sizeof(led->dirname) / sizeof(*led->dirname), + "%s::illumination", buf); + led->cdev.name = led->dirname; + led->cdev.flags = LED_HW_PLUGGABLE | LED_BRIGHT_HW_CHANGED; + led->cdev.max_brightness = device_to_led_brightness_value(led, led->brightness_info.max); + if (brightness_range == 0) { + /* According to docs set value is not supported under these + * conditions. + * LED interface enforces a set function. + */ + led->cdev.brightness_set = led_brightness_set_dummy; + } else { + led->cdev.brightness_set_blocking = led_brightness_set_sync; + } + led->cdev.brightness_get = led_brightness_get; + + ret = devm_led_classdev_register(&hidpp->hid_dev->dev, &led->cdev); + if (ret < 0) { + goto cleanup; + } + hidpp->private_data = led; + return 0; +cleanup: + devm_kfree(&hidpp->hid_dev->dev, led); + return ret; +} + +static int hidpp_initialize_illumination(struct hidpp_device *hidpp) +{ + int ret; + unsigned long capabilities = hidpp->capabilities; + + if (hidpp->protocol_major >= 2) { + u8 feature_index; + u8 feature_type; + + ret = hidpp_root_get_feature(hidpp, + HIDPP_PAGE_ILLUMINATION_LIGHT, + &feature_index, &feature_type); + if (!ret && !(feature_type & HIDPP_FEATURE_TYPE_HIDDEN)) { + hidpp->capabilities |= + HIDPP_CAPABILITY_ILLUMINATION_LIGHT; + hidpp->illumination_feature_index = feature_index; + hid_dbg(hidpp->hid_dev, + "Detected HID++ 2.0 Illumination Light\n"); + return 0; + } + } + + if (hidpp->capabilities == capabilities) + hid_dbg(hidpp->hid_dev, + "Did not detect HID++ Illumination Light hardware support\n"); + return 0; +} + +static int hidpp20_illumination_raw_event(struct hidpp_device *hidpp, u8 *data, + int size) +{ + struct led_data *led = (struct led_data *)hidpp->private_data; + struct hidpp_report *report = (struct hidpp_report *)data; + switch (report->report_id) { + case REPORT_ID_HIDPP_LONG: + /* size is already checked in hidpp_raw_event. + * only leave long through + */ + break; + default: + return 0; + } + + if (report->fap.feature_index != hidpp->illumination_feature_index) { + return 0; + } + + + if (report->fap.funcindex_clientid == HIDPP_ILLUMINATION_EVENT_CHANGE) { + led->on = report->fap.params[0] & 0x1; + if (led->on) { + unsigned led_brightness = device_to_led_brightness(led); + led_classdev_notify_brightness_hw_changed( + &led->cdev, led_brightness); + } else + led_classdev_notify_brightness_hw_changed(&led->cdev, + LED_OFF); + return 0; + } + + if (report->fap.funcindex_clientid == + HIDPP_ILLUMINATION_EVENT_BRIGHTNESS_CHANGE) { + unsigned led_brightness; + u16 brightness = be16_to_cpu(report->fap.params[0] << 8 | + report->fap.params[1] << 0); + led->brightness = brightness; + led_brightness = device_to_led_brightness(led); + led_classdev_notify_brightness_hw_changed(&led->cdev, + led_brightness); + return 0; + } + + return 0; +} + /* -------------------------------------------------------------------------- */ /* 0x2120: Hi-resolution scrolling */ /* -------------------------------------------------------------------------- */ @@ -2929,7 +3285,6 @@ static int m560_send_config_command(struct hid_device *hdev, bool connected) return hidpp_send_rap_command_sync( hidpp_dev, - REPORT_ID_HIDPP_SHORT, M560_SUB_ID, M560_BUTTON_MODE_REGISTER, (u8 *)m560_config_parameter, @@ -3468,7 +3823,6 @@ static int hidpp_initialize_hires_scroll(struct hidpp_device *hidpp) struct hidpp_report response; ret = hidpp_send_rap_command_sync(hidpp, - REPORT_ID_HIDPP_SHORT, HIDPP_GET_REGISTER, HIDPP_ENABLE_FAST_SCROLL, NULL, 0, &response); @@ -3648,6 +4002,12 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data, return ret; } + if (hidpp->capabilities & HIDPP_CAPABILITY_ILLUMINATION_LIGHT) { + ret = hidpp20_illumination_raw_event(hidpp, data, size); + if (ret != 0) + return ret; + } + if (hidpp->quirks & HIDPP_QUIRK_HIDPP_WHEELS) { ret = hidpp10_wheel_raw_event(hidpp, data, size); if (ret != 0) @@ -3972,6 +4332,7 @@ static void hidpp_connect_event(struct hidpp_device *hidpp) hidpp_initialize_battery(hidpp); hidpp_initialize_hires_scroll(hidpp); + hidpp_initialize_illumination(hidpp); /* forward current battery state */ if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP10_BATTERY) { @@ -3994,6 +4355,14 @@ static void hidpp_connect_event(struct hidpp_device *hidpp) if (hidpp->capabilities & HIDPP_CAPABILITY_HI_RES_SCROLL) hi_res_scroll_enable(hidpp); + if (hidpp->capabilities & HIDPP_CAPABILITY_ILLUMINATION_LIGHT) { + ret = register_led(hidpp); + if (ret) { + hid_err(hdev, "Registering leds failed.\n"); + return; + } + } + if (!(hidpp->quirks & HIDPP_QUIRK_NO_HIDINPUT) || hidpp->delayed_input) /* if the input nodes are already created, we can stop now */ return; @@ -4187,12 +4556,16 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id) if (hidpp->quirks & HIDPP_QUIRK_UNIFYING) hidpp_unifying_init(hidpp); - connected = hidpp_root_get_protocol_version(hidpp) == 0; + ret = hidpp_root_get_protocol_version(hidpp); + connected = ret == 0; atomic_set(&hidpp->connected, connected); if (!(hidpp->quirks & HIDPP_QUIRK_UNIFYING)) { if (!connected) { + if (ret == -ETIMEDOUT) + hid_err(hdev, "Device connection timed out"); + else + hid_err(hdev, "Device not connected"); ret = -ENODEV; - hid_err(hdev, "Device not connected"); goto hid_hw_init_fail; } @@ -4357,6 +4730,9 @@ static const struct hid_device_id hidpp_devices[] = { .driver_data = HIDPP_QUIRK_CLASS_G920 | HIDPP_QUIRK_FORCE_OUTPUT_REPORTS}, { /* Logitech G Pro Gaming Mouse over USB */ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC088) }, + { /* Logitech Litra Glow over USB*/ + HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_LITRA_GLOW), + .driver_data = HIDPP_QUIRK_DELAYED_INIT | HIDPP_QUIRK_FORCE_OUTPUT_REPORTS }, { /* MX5000 keyboard over Bluetooth */ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb305), diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c index 50e1c717fc0a..0332662692d2 100644 --- a/drivers/hid/hid-quirks.c +++ b/drivers/hid/hid-quirks.c @@ -491,6 +491,7 @@ static const struct hid_device_id hid_have_special_driver[] = { #endif #if IS_ENABLED(CONFIG_HID_LOGITECH_HIDPP) { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_G920_WHEEL) }, + { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_LITRA_GLOW) }, #endif #if IS_ENABLED(CONFIG_HID_MAGICMOUSE) { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_APPLE, USB_DEVICE_ID_APPLE_MAGICMOUSE) },