diff mbox series

[2/4] HID: Kysona: Add basic battery reporting for Kysona M600

Message ID 20241005120256.13847-3-me@lodewillems.com
State New
Headers show
Series HID: add Kysona driver | expand

Commit Message

Lode Willems Oct. 5, 2024, 11:57 a.m. UTC
In this initial the battery is only probed once, a following patch will add
periodic checking.

Signed-off-by: Lode Willems <me@lodewillems.com>
---
 drivers/hid/Kconfig      |   9 ++
 drivers/hid/Makefile     |   1 +
 drivers/hid/hid-kysona.c | 208 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 218 insertions(+)
 create mode 100644 drivers/hid/hid-kysona.c
diff mbox series

Patch

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 08446c89eff6..541dbee4fa00 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -459,6 +459,15 @@  config HID_KYE
 	- MousePen i608X tablet
 	- EasyPen M610X tablet
 
+config HID_KYSONA
+	tristate "Kysona devices"
+	depends on USB_HID
+	help
+	Support for Kysona mice.
+
+	Say Y here if you have a Kysona M600 mouse
+	and want to be able to read its battery capacity.
+
 config HID_UCLOGIC
 	tristate "UC-Logic"
 	depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index e40f1ddebbb7..c39ef074ac66 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -69,6 +69,7 @@  obj-$(CONFIG_HID_JABRA)		+= hid-jabra.o
 obj-$(CONFIG_HID_KENSINGTON)	+= hid-kensington.o
 obj-$(CONFIG_HID_KEYTOUCH)	+= hid-keytouch.o
 obj-$(CONFIG_HID_KYE)		+= hid-kye.o
+obj-$(CONFIG_HID_KYSONA)	+= hid-kysona.o
 obj-$(CONFIG_HID_LCPOWER)	+= hid-lcpower.o
 obj-$(CONFIG_HID_LENOVO)	+= hid-lenovo.o
 obj-$(CONFIG_HID_LETSKETCH)	+= hid-letsketch.o
diff --git a/drivers/hid/hid-kysona.c b/drivers/hid/hid-kysona.c
new file mode 100644
index 000000000000..91e359c31fa1
--- /dev/null
+++ b/drivers/hid/hid-kysona.c
@@ -0,0 +1,208 @@ 
+// 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_REPORT_ID 4
+
+struct kysona_drvdata {
+	struct hid_device *hdev;
+	struct power_supply_desc battery_desc;
+	struct power_supply *battery;
+	u8 battery_capacity;
+	bool battery_charging;
+	u16 battery_voltage;
+};
+
+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
+};
+
+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_STATUS:
+		// TODO: check if device is online
+		val->intval = drv_data->battery_charging ?
+			POWER_SUPPLY_STATUS_CHARGING :
+			POWER_SUPPLY_STATUS_DISCHARGING;
+		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 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->battery_capacity = 100;
+
+	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);
+	kysona_fetch_battery(hdev);
+
+	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];
+	}
+
+	return 0;
+}
+
+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
+};
+module_hid_driver(kysona_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("HID driver for Kysona devices");
+MODULE_AUTHOR("Lode Willems <me@lodewillems.com>");