| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * USB HID driver for Kysona |
| * Kysona M600 mice. |
| * |
| * Copyright (c) 2024 Lode Willems <me@lodewillems.com> |
| */ |
| |
| #include <linux/device.h> |
| #include <linux/hid.h> |
| #include <linux/usb.h> |
| |
| #include "hid-ids.h" |
| |
| #define BATTERY_TIMEOUT_MS 5000 |
| |
| #define BATTERY_REPORT_ID 4 |
| |
| struct kysona_drvdata { |
| struct hid_device *hdev; |
| bool online; |
| |
| struct power_supply_desc battery_desc; |
| struct power_supply *battery; |
| u8 battery_capacity; |
| bool battery_charging; |
| u16 battery_voltage; |
| struct delayed_work battery_work; |
| }; |
| |
| static enum power_supply_property kysona_battery_props[] = { |
| POWER_SUPPLY_PROP_STATUS, |
| POWER_SUPPLY_PROP_PRESENT, |
| POWER_SUPPLY_PROP_CAPACITY, |
| POWER_SUPPLY_PROP_SCOPE, |
| POWER_SUPPLY_PROP_MODEL_NAME, |
| POWER_SUPPLY_PROP_VOLTAGE_NOW, |
| POWER_SUPPLY_PROP_ONLINE |
| }; |
| |
| static int kysona_battery_get_property(struct power_supply *psy, |
| enum power_supply_property psp, |
| union power_supply_propval *val) |
| { |
| struct kysona_drvdata *drv_data = power_supply_get_drvdata(psy); |
| int ret = 0; |
| |
| switch (psp) { |
| case POWER_SUPPLY_PROP_PRESENT: |
| val->intval = 1; |
| break; |
| case POWER_SUPPLY_PROP_ONLINE: |
| val->intval = drv_data->online; |
| break; |
| case POWER_SUPPLY_PROP_STATUS: |
| if (drv_data->online) |
| val->intval = drv_data->battery_charging ? |
| POWER_SUPPLY_STATUS_CHARGING : |
| POWER_SUPPLY_STATUS_DISCHARGING; |
| else |
| val->intval = POWER_SUPPLY_STATUS_UNKNOWN; |
| break; |
| case POWER_SUPPLY_PROP_SCOPE: |
| val->intval = POWER_SUPPLY_SCOPE_DEVICE; |
| break; |
| case POWER_SUPPLY_PROP_CAPACITY: |
| val->intval = drv_data->battery_capacity; |
| break; |
| case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
| /* hardware reports voltage in mV. sysfs expects uV */ |
| val->intval = drv_data->battery_voltage * 1000; |
| break; |
| case POWER_SUPPLY_PROP_MODEL_NAME: |
| val->strval = drv_data->hdev->name; |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| return ret; |
| } |
| |
| static const char kysona_battery_request[] = { |
| 0x08, BATTERY_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49 |
| }; |
| |
| static int kysona_m600_fetch_battery(struct hid_device *hdev) |
| { |
| u8 *write_buf; |
| int ret; |
| |
| /* Request battery information */ |
| write_buf = kmemdup(kysona_battery_request, sizeof(kysona_battery_request), GFP_KERNEL); |
| if (!write_buf) |
| return -ENOMEM; |
| |
| ret = hid_hw_raw_request(hdev, kysona_battery_request[0], |
| write_buf, sizeof(kysona_battery_request), |
| HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); |
| if (ret < (int)sizeof(kysona_battery_request)) { |
| hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret); |
| ret = -ENODATA; |
| } |
| kfree(write_buf); |
| return ret; |
| } |
| |
| static void kysona_fetch_battery(struct hid_device *hdev) |
| { |
| int ret = kysona_m600_fetch_battery(hdev); |
| |
| if (ret < 0) |
| hid_dbg(hdev, |
| "Battery query failed (err: %d)\n", ret); |
| } |
| |
| static void kysona_battery_timer_tick(struct work_struct *work) |
| { |
| struct kysona_drvdata *drv_data = container_of(work, |
| struct kysona_drvdata, battery_work.work); |
| struct hid_device *hdev = drv_data->hdev; |
| |
| kysona_fetch_battery(hdev); |
| schedule_delayed_work(&drv_data->battery_work, |
| msecs_to_jiffies(BATTERY_TIMEOUT_MS)); |
| } |
| |
| static int kysona_battery_probe(struct hid_device *hdev) |
| { |
| struct kysona_drvdata *drv_data = hid_get_drvdata(hdev); |
| struct power_supply_config pscfg = { .drv_data = drv_data }; |
| int ret = 0; |
| |
| drv_data->online = false; |
| drv_data->battery_capacity = 100; |
| drv_data->battery_voltage = 4200; |
| |
| drv_data->battery_desc.properties = kysona_battery_props; |
| drv_data->battery_desc.num_properties = ARRAY_SIZE(kysona_battery_props); |
| drv_data->battery_desc.get_property = kysona_battery_get_property; |
| drv_data->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY; |
| drv_data->battery_desc.use_for_apm = 0; |
| drv_data->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL, |
| "kysona-%s-battery", |
| strlen(hdev->uniq) ? |
| hdev->uniq : dev_name(&hdev->dev)); |
| if (!drv_data->battery_desc.name) |
| return -ENOMEM; |
| |
| drv_data->battery = devm_power_supply_register(&hdev->dev, |
| &drv_data->battery_desc, &pscfg); |
| if (IS_ERR(drv_data->battery)) { |
| ret = PTR_ERR(drv_data->battery); |
| drv_data->battery = NULL; |
| hid_err(hdev, "Unable to register battery device\n"); |
| return ret; |
| } |
| |
| power_supply_powers(drv_data->battery, &hdev->dev); |
| |
| INIT_DELAYED_WORK(&drv_data->battery_work, kysona_battery_timer_tick); |
| kysona_fetch_battery(hdev); |
| schedule_delayed_work(&drv_data->battery_work, |
| msecs_to_jiffies(BATTERY_TIMEOUT_MS)); |
| |
| return ret; |
| } |
| |
| static int kysona_probe(struct hid_device *hdev, const struct hid_device_id *id) |
| { |
| int ret; |
| struct kysona_drvdata *drv_data; |
| struct usb_interface *usbif; |
| |
| if (!hid_is_usb(hdev)) |
| return -EINVAL; |
| |
| usbif = to_usb_interface(hdev->dev.parent); |
| |
| drv_data = devm_kzalloc(&hdev->dev, sizeof(*drv_data), GFP_KERNEL); |
| if (!drv_data) |
| return -ENOMEM; |
| |
| hid_set_drvdata(hdev, drv_data); |
| drv_data->hdev = hdev; |
| |
| ret = hid_parse(hdev); |
| if (ret) |
| return ret; |
| |
| ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); |
| if (ret) |
| return ret; |
| |
| if (usbif->cur_altsetting->desc.bInterfaceNumber == 1) { |
| if (kysona_battery_probe(hdev) < 0) |
| hid_err(hdev, "Kysona hid battery_probe failed: %d\n", ret); |
| } |
| |
| return 0; |
| } |
| |
| static int kysona_raw_event(struct hid_device *hdev, |
| struct hid_report *report, u8 *data, int size) |
| { |
| struct kysona_drvdata *drv_data = hid_get_drvdata(hdev); |
| |
| if (drv_data->battery && size == sizeof(kysona_battery_request) && |
| data[0] == 8 && data[1] == BATTERY_REPORT_ID) { |
| drv_data->battery_capacity = data[6]; |
| drv_data->battery_charging = data[7]; |
| drv_data->battery_voltage = (data[8] << 8) | data[9]; |
| drv_data->online = true; |
| } |
| |
| return 0; |
| } |
| |
| static void kysona_remove(struct hid_device *hdev) |
| { |
| struct kysona_drvdata *drv_data = hid_get_drvdata(hdev); |
| |
| if (drv_data->battery) |
| cancel_delayed_work_sync(&drv_data->battery_work); |
| |
| hid_hw_stop(hdev); |
| } |
| |
| static const struct hid_device_id kysona_devices[] = { |
| { HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_DONGLE) }, |
| { HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_WIRED) }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(hid, kysona_devices); |
| |
| static struct hid_driver kysona_driver = { |
| .name = "kysona", |
| .id_table = kysona_devices, |
| .probe = kysona_probe, |
| .raw_event = kysona_raw_event, |
| .remove = kysona_remove |
| }; |
| module_hid_driver(kysona_driver); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_DESCRIPTION("HID driver for Kysona devices"); |
| MODULE_AUTHOR("Lode Willems <me@lodewillems.com>"); |