Merge branch 'for-6.4/logitech-hidpp' into for-linus

- support for ADC measurement (Bastien Nocera)
- support for Logitech G935 (Bastien Nocera)
This commit is contained in:
Jiri Kosina 2023-04-26 22:57:40 +02:00
commit 0549fbac40
6 changed files with 373 additions and 7 deletions

View File

@ -166,6 +166,23 @@ Description:
The file will be present for all speeds of USB devices, and will
always read "no" for USB 1.1 and USB 2.0 devices.
What: /sys/bus/usb/devices/<INTERFACE>/wireless_status
Date: February 2023
Contact: Bastien Nocera <hadess@hadess.net>
Description:
Some USB devices use a USB receiver dongle to communicate
wirelessly with their device using proprietary protocols. This
attribute allows user-space to know whether the device is
connected to its receiver dongle, and, for example, consider
the device to be absent when choosing whether to show the
device's battery, show a headset in a list of outputs, or show
an on-screen keyboard if the only wireless keyboard is
turned off.
This attribute is not to be used to replace protocol specific
statuses available in WWAN, WLAN/Wi-Fi, Bluetooth, etc.
If the device does not use a receiver dongle with a wireless
device, then this attribute will not exist.
What: /sys/bus/usb/devices/.../<hub_interface>/port<X>
Date: August 2012
Contact: Lan Tianyu <tianyu.lan@intel.com>

View File

@ -74,6 +74,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
#define HIDPP_QUIRK_HIDPP_EXTRA_MOUSE_BTNS BIT(27)
#define HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS BIT(28)
#define HIDPP_QUIRK_HI_RES_SCROLL_1P0 BIT(29)
#define HIDPP_QUIRK_WIRELESS_STATUS BIT(30)
/* These are just aliases for now */
#define HIDPP_QUIRK_KBD_SCROLL_WHEEL HIDPP_QUIRK_HIDPP_WHEELS
@ -94,6 +95,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_ADC_MEASUREMENT BIT(10)
#define lg_map_key_clear(c) hid_map_usage_clear(hi, usage, bit, max, EV_KEY, (c))
@ -145,6 +147,7 @@ struct hidpp_battery {
u8 feature_index;
u8 solar_feature_index;
u8 voltage_feature_index;
u8 adc_measurement_feature_index;
struct power_supply_desc desc;
struct power_supply *ps;
char name[64];
@ -471,6 +474,26 @@ static void hidpp_prefix_name(char **name, int name_length)
*name = new_name;
}
/*
* Updates the USB wireless_status based on whether the headset
* is turned on and reachable.
*/
static void hidpp_update_usb_wireless_status(struct hidpp_device *hidpp)
{
struct hid_device *hdev = hidpp->hid_dev;
struct usb_interface *intf;
if (!(hidpp->quirks & HIDPP_QUIRK_WIRELESS_STATUS))
return;
if (!hid_is_usb(hdev))
return;
intf = to_usb_interface(hdev->dev.parent);
usb_set_wireless_status(intf, hidpp->battery.online ?
USB_WIRELESS_STATUS_CONNECTED :
USB_WIRELESS_STATUS_DISCONNECTED);
}
/**
* hidpp_scroll_counter_handle_scroll() - Send high- and low-resolution scroll
* events given a high-resolution wheel
@ -853,8 +876,7 @@ static int hidpp_unifying_init(struct hidpp_device *hidpp)
if (ret)
return ret;
snprintf(hdev->uniq, sizeof(hdev->uniq), "%04x-%4phD",
hdev->product, &serial);
snprintf(hdev->uniq, sizeof(hdev->uniq), "%4phD", &serial);
dbg_hid("HID++ Unifying: Got serial: %s\n", hdev->uniq);
name = hidpp_unifying_get_name(hidpp);
@ -947,6 +969,54 @@ print_version:
return 0;
}
/* -------------------------------------------------------------------------- */
/* 0x0003: Device Information */
/* -------------------------------------------------------------------------- */
#define HIDPP_PAGE_DEVICE_INFORMATION 0x0003
#define CMD_GET_DEVICE_INFO 0x00
static int hidpp_get_serial(struct hidpp_device *hidpp, u32 *serial)
{
struct hidpp_report response;
u8 feature_type;
u8 feature_index;
int ret;
ret = hidpp_root_get_feature(hidpp, HIDPP_PAGE_DEVICE_INFORMATION,
&feature_index,
&feature_type);
if (ret)
return ret;
ret = hidpp_send_fap_command_sync(hidpp, feature_index,
CMD_GET_DEVICE_INFO,
NULL, 0, &response);
if (ret)
return ret;
/* See hidpp_unifying_get_serial() */
*serial = *((u32 *)&response.rap.params[1]);
return 0;
}
static int hidpp_serial_init(struct hidpp_device *hidpp)
{
struct hid_device *hdev = hidpp->hid_dev;
u32 serial;
int ret;
ret = hidpp_get_serial(hidpp, &serial);
if (ret)
return ret;
snprintf(hdev->uniq, sizeof(hdev->uniq), "%4phD", &serial);
dbg_hid("HID++ DeviceInformation: Got serial: %s\n", hdev->uniq);
return 0;
}
/* -------------------------------------------------------------------------- */
/* 0x0005: GetDeviceNameType */
/* -------------------------------------------------------------------------- */
@ -1357,7 +1427,7 @@ static int hidpp20_map_battery_capacity(struct hid_device *hid_dev, int voltage)
* there are a few devices that use different battery technology.
*/
static const int voltages[] = {
static const int voltages[100] = {
4186, 4156, 4143, 4133, 4122, 4113, 4103, 4094, 4086, 4075,
4067, 4059, 4051, 4043, 4035, 4027, 4019, 4011, 4003, 3997,
3989, 3983, 3976, 3969, 3961, 3955, 3949, 3942, 3935, 3929,
@ -1372,8 +1442,6 @@ static int hidpp20_map_battery_capacity(struct hid_device *hid_dev, int voltage)
int i;
BUILD_BUG_ON(ARRAY_SIZE(voltages) != 100);
if (unlikely(voltage < 3500 || voltage >= 5000))
hid_warn_once(hid_dev,
"%s: possibly using the wrong voltage curve\n",
@ -1745,6 +1813,164 @@ static int hidpp_set_wireless_feature_index(struct hidpp_device *hidpp)
return ret;
}
/* -------------------------------------------------------------------------- */
/* 0x1f20: ADC measurement */
/* -------------------------------------------------------------------------- */
#define HIDPP_PAGE_ADC_MEASUREMENT 0x1f20
#define CMD_ADC_MEASUREMENT_GET_ADC_MEASUREMENT 0x00
#define EVENT_ADC_MEASUREMENT_STATUS_BROADCAST 0x00
static int hidpp20_map_adc_measurement_1f20_capacity(struct hid_device *hid_dev, int voltage)
{
/* NB: This voltage curve doesn't necessarily map perfectly to all
* devices that implement the ADC_MEASUREMENT feature. This is because
* there are a few devices that use different battery technology.
*
* Adapted from:
* https://github.com/Sapd/HeadsetControl/blob/acd972be0468e039b93aae81221f20a54d2d60f7/src/devices/logitech_g633_g933_935.c#L44-L52
*/
static const int voltages[100] = {
4030, 4024, 4018, 4011, 4003, 3994, 3985, 3975, 3963, 3951,
3937, 3922, 3907, 3893, 3880, 3868, 3857, 3846, 3837, 3828,
3820, 3812, 3805, 3798, 3791, 3785, 3779, 3773, 3768, 3762,
3757, 3752, 3747, 3742, 3738, 3733, 3729, 3724, 3720, 3716,
3712, 3708, 3704, 3700, 3696, 3692, 3688, 3685, 3681, 3677,
3674, 3670, 3667, 3663, 3660, 3657, 3653, 3650, 3646, 3643,
3640, 3637, 3633, 3630, 3627, 3624, 3620, 3617, 3614, 3611,
3608, 3604, 3601, 3598, 3595, 3592, 3589, 3585, 3582, 3579,
3576, 3573, 3569, 3566, 3563, 3560, 3556, 3553, 3550, 3546,
3543, 3539, 3536, 3532, 3529, 3525, 3499, 3466, 3433, 3399,
};
int i;
if (voltage == 0)
return 0;
if (unlikely(voltage < 3400 || voltage >= 5000))
hid_warn_once(hid_dev,
"%s: possibly using the wrong voltage curve\n",
__func__);
for (i = 0; i < ARRAY_SIZE(voltages); i++) {
if (voltage >= voltages[i])
return ARRAY_SIZE(voltages) - i;
}
return 0;
}
static int hidpp20_map_adc_measurement_1f20(u8 data[3], int *voltage)
{
int status;
u8 flags;
flags = data[2];
switch (flags) {
case 0x01:
status = POWER_SUPPLY_STATUS_DISCHARGING;
break;
case 0x03:
status = POWER_SUPPLY_STATUS_CHARGING;
break;
case 0x07:
status = POWER_SUPPLY_STATUS_FULL;
break;
case 0x0F:
default:
status = POWER_SUPPLY_STATUS_UNKNOWN;
break;
}
*voltage = get_unaligned_be16(data);
dbg_hid("Parsed 1f20 data as flag 0x%02x voltage %dmV\n",
flags, *voltage);
return status;
}
/* Return value is whether the device is online */
static bool hidpp20_get_adc_measurement_1f20(struct hidpp_device *hidpp,
u8 feature_index,
int *status, int *voltage)
{
struct hidpp_report response;
int ret;
u8 *params = (u8 *)response.fap.params;
*status = POWER_SUPPLY_STATUS_UNKNOWN;
*voltage = 0;
ret = hidpp_send_fap_command_sync(hidpp, feature_index,
CMD_ADC_MEASUREMENT_GET_ADC_MEASUREMENT,
NULL, 0, &response);
if (ret > 0) {
hid_dbg(hidpp->hid_dev, "%s: received protocol error 0x%02x\n",
__func__, ret);
return false;
}
*status = hidpp20_map_adc_measurement_1f20(params, voltage);
return true;
}
static int hidpp20_query_adc_measurement_info_1f20(struct hidpp_device *hidpp)
{
u8 feature_type;
if (hidpp->battery.adc_measurement_feature_index == 0xff) {
int ret;
ret = hidpp_root_get_feature(hidpp, HIDPP_PAGE_ADC_MEASUREMENT,
&hidpp->battery.adc_measurement_feature_index,
&feature_type);
if (ret)
return ret;
hidpp->capabilities |= HIDPP_CAPABILITY_ADC_MEASUREMENT;
}
hidpp->battery.online = hidpp20_get_adc_measurement_1f20(hidpp,
hidpp->battery.adc_measurement_feature_index,
&hidpp->battery.status,
&hidpp->battery.voltage);
hidpp->battery.capacity = hidpp20_map_adc_measurement_1f20_capacity(hidpp->hid_dev,
hidpp->battery.voltage);
hidpp_update_usb_wireless_status(hidpp);
return 0;
}
static int hidpp20_adc_measurement_event_1f20(struct hidpp_device *hidpp,
u8 *data, int size)
{
struct hidpp_report *report = (struct hidpp_report *)data;
int status, voltage;
if (report->fap.feature_index != hidpp->battery.adc_measurement_feature_index ||
report->fap.funcindex_clientid != EVENT_ADC_MEASUREMENT_STATUS_BROADCAST)
return 0;
status = hidpp20_map_adc_measurement_1f20(report->fap.params, &voltage);
hidpp->battery.online = status != POWER_SUPPLY_STATUS_UNKNOWN;
if (voltage != hidpp->battery.voltage || status != hidpp->battery.status) {
hidpp->battery.status = status;
hidpp->battery.voltage = voltage;
hidpp->battery.capacity = hidpp20_map_adc_measurement_1f20_capacity(hidpp->hid_dev, voltage);
if (hidpp->battery.ps)
power_supply_changed(hidpp->battery.ps);
hidpp_update_usb_wireless_status(hidpp);
}
return 0;
}
/* -------------------------------------------------------------------------- */
/* 0x2120: Hi-resolution scrolling */
/* -------------------------------------------------------------------------- */
@ -3663,6 +3889,9 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
ret = hidpp20_battery_voltage_event(hidpp, data, size);
if (ret != 0)
return ret;
ret = hidpp20_adc_measurement_event_1f20(hidpp, data, size);
if (ret != 0)
return ret;
}
if (hidpp->capabilities & HIDPP_CAPABILITY_HIDPP10_BATTERY) {
@ -3786,6 +4015,7 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
hidpp->battery.feature_index = 0xff;
hidpp->battery.solar_feature_index = 0xff;
hidpp->battery.voltage_feature_index = 0xff;
hidpp->battery.adc_measurement_feature_index = 0xff;
if (hidpp->protocol_major >= 2) {
if (hidpp->quirks & HIDPP_QUIRK_CLASS_K750)
@ -3799,6 +4029,8 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
ret = hidpp20_query_battery_info_1004(hidpp);
if (ret)
ret = hidpp20_query_battery_voltage_info(hidpp);
if (ret)
ret = hidpp20_query_adc_measurement_info_1f20(hidpp);
}
if (ret)
@ -3828,7 +4060,8 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
if (hidpp->capabilities & HIDPP_CAPABILITY_BATTERY_MILEAGE ||
hidpp->capabilities & HIDPP_CAPABILITY_BATTERY_PERCENTAGE ||
hidpp->capabilities & HIDPP_CAPABILITY_BATTERY_VOLTAGE)
hidpp->capabilities & HIDPP_CAPABILITY_BATTERY_VOLTAGE ||
hidpp->capabilities & HIDPP_CAPABILITY_ADC_MEASUREMENT)
battery_props[num_battery_props++] =
POWER_SUPPLY_PROP_CAPACITY;
@ -3836,7 +4069,8 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
battery_props[num_battery_props++] =
POWER_SUPPLY_PROP_CAPACITY_LEVEL;
if (hidpp->capabilities & HIDPP_CAPABILITY_BATTERY_VOLTAGE)
if (hidpp->capabilities & HIDPP_CAPABILITY_BATTERY_VOLTAGE ||
hidpp->capabilities & HIDPP_CAPABILITY_ADC_MEASUREMENT)
battery_props[num_battery_props++] =
POWER_SUPPLY_PROP_VOLTAGE_NOW;
@ -4009,6 +4243,8 @@ static void hidpp_connect_event(struct hidpp_device *hidpp)
hidpp20_query_battery_voltage_info(hidpp);
else if (hidpp->capabilities & HIDPP_CAPABILITY_UNIFIED_BATTERY)
hidpp20_query_battery_info_1004(hidpp);
else if (hidpp->capabilities & HIDPP_CAPABILITY_ADC_MEASUREMENT)
hidpp20_query_adc_measurement_info_1f20(hidpp);
else
hidpp20_query_battery_info_1000(hidpp);
}
@ -4210,6 +4446,8 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (hidpp->quirks & HIDPP_QUIRK_UNIFYING)
hidpp_unifying_init(hidpp);
else if (hid_is_usb(hidpp->hid_dev))
hidpp_serial_init(hidpp);
connected = hidpp_root_get_protocol_version(hidpp) == 0;
atomic_set(&hidpp->connected, connected);
@ -4379,6 +4617,10 @@ static const struct hid_device_id hidpp_devices[] = {
{ /* Logitech G Pro Gaming Mouse over USB */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC088) },
{ /* G935 Gaming Headset */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0x0a87),
.driver_data = HIDPP_QUIRK_WIRELESS_STATUS },
{ /* MX5000 keyboard over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb305),
.driver_data = HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS },

View File

@ -1908,6 +1908,45 @@ static void __usb_queue_reset_device(struct work_struct *ws)
usb_put_intf(iface); /* Undo _get_ in usb_queue_reset_device() */
}
/*
* Internal function to set the wireless_status sysfs attribute
* See usb_set_wireless_status() for more details
*/
static void __usb_wireless_status_intf(struct work_struct *ws)
{
struct usb_interface *iface =
container_of(ws, struct usb_interface, wireless_status_work);
device_lock(iface->dev.parent);
if (iface->sysfs_files_created)
usb_update_wireless_status_attr(iface);
device_unlock(iface->dev.parent);
usb_put_intf(iface); /* Undo _get_ in usb_set_wireless_status() */
}
/**
* usb_set_wireless_status - sets the wireless_status struct member
* @iface: the interface to modify
* @status: the new wireless status
*
* Set the wireless_status struct member to the new value, and emit
* sysfs changes as necessary.
*
* Returns: 0 on success, -EALREADY if already set.
*/
int usb_set_wireless_status(struct usb_interface *iface,
enum usb_wireless_status status)
{
if (iface->wireless_status == status)
return -EALREADY;
usb_get_intf(iface);
iface->wireless_status = status;
schedule_work(&iface->wireless_status_work);
return 0;
}
EXPORT_SYMBOL_GPL(usb_set_wireless_status);
/*
* usb_set_configuration - Makes a particular device setting be current
@ -2100,6 +2139,7 @@ free_interfaces:
intf->dev.type = &usb_if_device_type;
intf->dev.groups = usb_interface_groups;
INIT_WORK(&intf->reset_ws, __usb_queue_reset_device);
INIT_WORK(&intf->wireless_status_work, __usb_wireless_status_intf);
intf->minor = -1;
device_initialize(&intf->dev);
pm_runtime_no_callbacks(&intf->dev);

View File

@ -1227,9 +1227,59 @@ static const struct attribute_group intf_assoc_attr_grp = {
.is_visible = intf_assoc_attrs_are_visible,
};
static ssize_t wireless_status_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct usb_interface *intf;
intf = to_usb_interface(dev);
if (intf->wireless_status == USB_WIRELESS_STATUS_DISCONNECTED)
return sysfs_emit(buf, "%s\n", "disconnected");
return sysfs_emit(buf, "%s\n", "connected");
}
static DEVICE_ATTR_RO(wireless_status);
static struct attribute *intf_wireless_status_attrs[] = {
&dev_attr_wireless_status.attr,
NULL
};
static umode_t intf_wireless_status_attr_is_visible(struct kobject *kobj,
struct attribute *a, int n)
{
struct device *dev = kobj_to_dev(kobj);
struct usb_interface *intf = to_usb_interface(dev);
if (a != &dev_attr_wireless_status.attr ||
intf->wireless_status != USB_WIRELESS_STATUS_NA)
return a->mode;
return 0;
}
static const struct attribute_group intf_wireless_status_attr_grp = {
.attrs = intf_wireless_status_attrs,
.is_visible = intf_wireless_status_attr_is_visible,
};
int usb_update_wireless_status_attr(struct usb_interface *intf)
{
struct device *dev = &intf->dev;
int ret;
ret = sysfs_update_group(&dev->kobj, &intf_wireless_status_attr_grp);
if (ret < 0)
return ret;
sysfs_notify(&dev->kobj, NULL, "wireless_status");
kobject_uevent(&dev->kobj, KOBJ_CHANGE);
return 0;
}
const struct attribute_group *usb_interface_groups[] = {
&intf_attr_grp,
&intf_assoc_attr_grp,
&intf_wireless_status_attr_grp,
NULL
};

View File

@ -15,6 +15,7 @@ extern int usb_create_sysfs_dev_files(struct usb_device *dev);
extern void usb_remove_sysfs_dev_files(struct usb_device *dev);
extern void usb_create_sysfs_intf_files(struct usb_interface *intf);
extern void usb_remove_sysfs_intf_files(struct usb_interface *intf);
extern int usb_update_wireless_status_attr(struct usb_interface *intf);
extern int usb_create_ep_devs(struct device *parent,
struct usb_host_endpoint *endpoint,
struct usb_device *udev);

View File

@ -170,6 +170,12 @@ usb_find_last_int_out_endpoint(struct usb_host_interface *alt,
return usb_find_common_endpoints_reverse(alt, NULL, NULL, NULL, int_out);
}
enum usb_wireless_status {
USB_WIRELESS_STATUS_NA = 0,
USB_WIRELESS_STATUS_DISCONNECTED,
USB_WIRELESS_STATUS_CONNECTED,
};
/**
* struct usb_interface - what usb device drivers talk to
* @altsetting: array of interface structures, one for each alternate
@ -197,6 +203,10 @@ usb_find_last_int_out_endpoint(struct usb_host_interface *alt,
* following a reset or suspend operation it doesn't support.
* @authorized: This allows to (de)authorize individual interfaces instead
* a whole device in contrast to the device authorization.
* @wireless_status: if the USB device uses a receiver/emitter combo, whether
* the emitter is connected.
* @wireless_status_work: Used for scheduling wireless status changes
* from atomic context.
* @dev: driver model's view of this device
* @usb_dev: if an interface is bound to the USB major, this will point
* to the sysfs representation for that device.
@ -253,6 +263,8 @@ struct usb_interface {
unsigned needs_binding:1; /* needs delayed unbind/rebind */
unsigned resetting_device:1; /* true: bandwidth alloc after reset */
unsigned authorized:1; /* used for interface authorization */
enum usb_wireless_status wireless_status;
struct work_struct wireless_status_work;
struct device dev; /* interface specific device info */
struct device *usb_dev;
@ -887,6 +899,10 @@ static inline int usb_interface_claimed(struct usb_interface *iface)
extern void usb_driver_release_interface(struct usb_driver *driver,
struct usb_interface *iface);
int usb_set_wireless_status(struct usb_interface *iface,
enum usb_wireless_status status);
const struct usb_device_id *usb_match_id(struct usb_interface *interface,
const struct usb_device_id *id);
extern int usb_match_one_id(struct usb_interface *interface,