1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * USB HID driver for Kysona
4 * Kysona M600 mice.
5 *
6 * Copyright (c) 2024 Lode Willems <me@lodewillems.com>
7 */
8
9#include <linux/device.h>
10#include <linux/hid.h>
11#include <linux/usb.h>
12
13#include "hid-ids.h"
14
15#define BATTERY_TIMEOUT_MS 5000
16
17#define ONLINE_REPORT_ID 3
18#define BATTERY_REPORT_ID 4
19
20struct kysona_drvdata {
21 struct hid_device *hdev;
22 bool online;
23
24 struct power_supply_desc battery_desc;
25 struct power_supply *battery;
26 u8 battery_capacity;
27 bool battery_charging;
28 u16 battery_voltage;
29 struct delayed_work battery_work;
30};
31
32static enum power_supply_property kysona_battery_props[] = {
33 POWER_SUPPLY_PROP_STATUS,
34 POWER_SUPPLY_PROP_PRESENT,
35 POWER_SUPPLY_PROP_CAPACITY,
36 POWER_SUPPLY_PROP_SCOPE,
37 POWER_SUPPLY_PROP_MODEL_NAME,
38 POWER_SUPPLY_PROP_VOLTAGE_NOW,
39 POWER_SUPPLY_PROP_ONLINE
40};
41
42static int kysona_battery_get_property(struct power_supply *psy,
43 enum power_supply_property psp,
44 union power_supply_propval *val)
45{
46 struct kysona_drvdata *drv_data = power_supply_get_drvdata(psy);
47 int ret = 0;
48
49 switch (psp) {
50 case POWER_SUPPLY_PROP_PRESENT:
51 val->intval = 1;
52 break;
53 case POWER_SUPPLY_PROP_ONLINE:
54 val->intval = drv_data->online;
55 break;
56 case POWER_SUPPLY_PROP_STATUS:
57 if (drv_data->online)
58 val->intval = drv_data->battery_charging ?
59 POWER_SUPPLY_STATUS_CHARGING :
60 POWER_SUPPLY_STATUS_DISCHARGING;
61 else
62 val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
63 break;
64 case POWER_SUPPLY_PROP_SCOPE:
65 val->intval = POWER_SUPPLY_SCOPE_DEVICE;
66 break;
67 case POWER_SUPPLY_PROP_CAPACITY:
68 val->intval = drv_data->battery_capacity;
69 break;
70 case POWER_SUPPLY_PROP_VOLTAGE_NOW:
71 /* hardware reports voltage in mV. sysfs expects uV */
72 val->intval = drv_data->battery_voltage * 1000;
73 break;
74 case POWER_SUPPLY_PROP_MODEL_NAME:
75 val->strval = drv_data->hdev->name;
76 break;
77 default:
78 ret = -EINVAL;
79 break;
80 }
81 return ret;
82}
83
84static const char kysona_online_request[] = {
85 0x08, ONLINE_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
86 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a
87};
88
89static const char kysona_battery_request[] = {
90 0x08, BATTERY_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
91 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49
92};
93
94static int kysona_m600_fetch_online(struct hid_device *hdev)
95{
96 u8 *write_buf;
97 int ret;
98
99 /* Request online information */
100 write_buf = kmemdup(kysona_online_request, sizeof(kysona_online_request), GFP_KERNEL);
101 if (!write_buf)
102 return -ENOMEM;
103
104 ret = hid_hw_raw_request(hdev, reportnum: kysona_online_request[0],
105 buf: write_buf, len: sizeof(kysona_online_request),
106 rtype: HID_OUTPUT_REPORT, reqtype: HID_REQ_SET_REPORT);
107 if (ret < (int)sizeof(kysona_online_request)) {
108 hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
109 ret = -ENODATA;
110 }
111 kfree(objp: write_buf);
112 return ret;
113}
114
115static void kysona_fetch_online(struct hid_device *hdev)
116{
117 int ret = kysona_m600_fetch_online(hdev);
118
119 if (ret < 0)
120 hid_dbg(hdev,
121 "Online query failed (err: %d)\n", ret);
122}
123
124static int kysona_m600_fetch_battery(struct hid_device *hdev)
125{
126 u8 *write_buf;
127 int ret;
128
129 /* Request battery information */
130 write_buf = kmemdup(kysona_battery_request, sizeof(kysona_battery_request), GFP_KERNEL);
131 if (!write_buf)
132 return -ENOMEM;
133
134 ret = hid_hw_raw_request(hdev, reportnum: kysona_battery_request[0],
135 buf: write_buf, len: sizeof(kysona_battery_request),
136 rtype: HID_OUTPUT_REPORT, reqtype: HID_REQ_SET_REPORT);
137 if (ret < (int)sizeof(kysona_battery_request)) {
138 hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
139 ret = -ENODATA;
140 }
141 kfree(objp: write_buf);
142 return ret;
143}
144
145static void kysona_fetch_battery(struct hid_device *hdev)
146{
147 int ret = kysona_m600_fetch_battery(hdev);
148
149 if (ret < 0)
150 hid_dbg(hdev,
151 "Battery query failed (err: %d)\n", ret);
152}
153
154static void kysona_battery_timer_tick(struct work_struct *work)
155{
156 struct kysona_drvdata *drv_data = container_of(work,
157 struct kysona_drvdata, battery_work.work);
158 struct hid_device *hdev = drv_data->hdev;
159
160 kysona_fetch_online(hdev);
161 kysona_fetch_battery(hdev);
162 schedule_delayed_work(dwork: &drv_data->battery_work,
163 delay: msecs_to_jiffies(BATTERY_TIMEOUT_MS));
164}
165
166static int kysona_battery_probe(struct hid_device *hdev)
167{
168 struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
169 struct power_supply_config pscfg = { .drv_data = drv_data };
170 int ret = 0;
171
172 drv_data->online = false;
173 drv_data->battery_capacity = 100;
174 drv_data->battery_voltage = 4200;
175
176 drv_data->battery_desc.properties = kysona_battery_props;
177 drv_data->battery_desc.num_properties = ARRAY_SIZE(kysona_battery_props);
178 drv_data->battery_desc.get_property = kysona_battery_get_property;
179 drv_data->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
180 drv_data->battery_desc.use_for_apm = 0;
181 drv_data->battery_desc.name = devm_kasprintf(dev: &hdev->dev, GFP_KERNEL,
182 fmt: "kysona-%s-battery",
183 strlen(hdev->uniq) ?
184 hdev->uniq : dev_name(dev: &hdev->dev));
185 if (!drv_data->battery_desc.name)
186 return -ENOMEM;
187
188 drv_data->battery = devm_power_supply_register(parent: &hdev->dev,
189 desc: &drv_data->battery_desc, cfg: &pscfg);
190 if (IS_ERR(ptr: drv_data->battery)) {
191 ret = PTR_ERR(ptr: drv_data->battery);
192 drv_data->battery = NULL;
193 hid_err(hdev, "Unable to register battery device\n");
194 return ret;
195 }
196
197 power_supply_powers(psy: drv_data->battery, dev: &hdev->dev);
198
199 INIT_DELAYED_WORK(&drv_data->battery_work, kysona_battery_timer_tick);
200 kysona_fetch_online(hdev);
201 kysona_fetch_battery(hdev);
202 schedule_delayed_work(dwork: &drv_data->battery_work,
203 delay: msecs_to_jiffies(BATTERY_TIMEOUT_MS));
204
205 return ret;
206}
207
208static int kysona_probe(struct hid_device *hdev, const struct hid_device_id *id)
209{
210 int ret;
211 struct kysona_drvdata *drv_data;
212 struct usb_interface *usbif;
213
214 if (!hid_is_usb(hdev))
215 return -EINVAL;
216
217 usbif = to_usb_interface(hdev->dev.parent);
218
219 drv_data = devm_kzalloc(dev: &hdev->dev, size: sizeof(*drv_data), GFP_KERNEL);
220 if (!drv_data)
221 return -ENOMEM;
222
223 hid_set_drvdata(hdev, data: drv_data);
224 drv_data->hdev = hdev;
225
226 ret = hid_parse(hdev);
227 if (ret)
228 return ret;
229
230 ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
231 if (ret)
232 return ret;
233
234 if (usbif->cur_altsetting->desc.bInterfaceNumber == 1) {
235 if (kysona_battery_probe(hdev) < 0)
236 hid_err(hdev, "Kysona hid battery_probe failed: %d\n", ret);
237 }
238
239 return 0;
240}
241
242static int kysona_raw_event(struct hid_device *hdev,
243 struct hid_report *report, u8 *data, int size)
244{
245 struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
246
247 if (size == sizeof(kysona_online_request) &&
248 data[0] == 8 && data[1] == ONLINE_REPORT_ID) {
249 drv_data->online = data[6];
250 }
251
252 if (size == sizeof(kysona_battery_request) &&
253 data[0] == 8 && data[1] == BATTERY_REPORT_ID) {
254 drv_data->battery_capacity = data[6];
255 drv_data->battery_charging = data[7];
256 drv_data->battery_voltage = (data[8] << 8) | data[9];
257 }
258
259 return 0;
260}
261
262static void kysona_remove(struct hid_device *hdev)
263{
264 struct kysona_drvdata *drv_data = hid_get_drvdata(hdev);
265
266 if (drv_data->battery)
267 cancel_delayed_work_sync(dwork: &drv_data->battery_work);
268
269 hid_hw_stop(hdev);
270}
271
272static const struct hid_device_id kysona_devices[] = {
273 { HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_DONGLE) },
274 { HID_USB_DEVICE(USB_VENDOR_ID_KYSONA, USB_DEVICE_ID_KYSONA_M600_WIRED) },
275 { }
276};
277MODULE_DEVICE_TABLE(hid, kysona_devices);
278
279static struct hid_driver kysona_driver = {
280 .name = "kysona",
281 .id_table = kysona_devices,
282 .probe = kysona_probe,
283 .raw_event = kysona_raw_event,
284 .remove = kysona_remove
285};
286module_hid_driver(kysona_driver);
287
288MODULE_LICENSE("GPL");
289MODULE_DESCRIPTION("HID driver for Kysona devices");
290MODULE_AUTHOR("Lode Willems <me@lodewillems.com>");
291

source code of linux/drivers/hid/hid-kysona.c