jbl610-linux/kernel/jbl_headset_battery.c
2025-01-29 12:53:47 +04:00

282 lines
No EOL
7.6 KiB
C

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/hid.h>
#include <linux/power_supply.h>
#include <linux/init.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>
#include <linux/kernel.h>
#define JBL_VID 0x0ecb
#define JBL_PID 0x205c
#define MY_NETLINK_GROUP 27
//#define NETLINK_MYPROTO 31
static struct sock *nl_sk = NULL;
/* List of HID devices this driver supports */
static const struct hid_device_id jbl_hid_table[] = {
{ HID_USB_DEVICE(JBL_VID, JBL_PID) },
{ }
};
MODULE_DEVICE_TABLE(hid, jbl_hid_table);
static void jbl_netlink_broadcast(const char *msg) {
struct sk_buff *skb_out;
struct nlmsghdr *nlh;
int msg_size = strlen(msg);
int res;
if (!nl_sk) {
pr_err("jbl610: netlink socket not ready!\n");
return;
}
skb_out = nlmsg_new(msg_size, GFP_KERNEL);
if (!skb_out) {
pr_err("jbl610: Failed to allocate skb\n");
return;
}
nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
if (!nlh) {
pr_err("jbl610: nlmsg_put() failed\n");
kfree_skb(skb_out);
return;
}
memcpy(nlmsg_data(nlh), msg, msg_size);
res = netlink_broadcast(nl_sk, skb_out, 0, MY_NETLINK_GROUP, GFP_KERNEL);
//res = netlink_unicast(nl_sk, skb_out, 0, MY_NETLINK_GROUP, GFP_KERNEL);
if (res < 0) {
pr_err("jbl610: netlink_broadcast error: %d\n", res);
} else {
pr_info("jbl610: broadcasted message: \"%s\"\n", msg);
}
}
struct jbl_headset_data {
struct hid_device *hdev;
struct power_supply *battery;
int capacity;
int status;
int present;
int muted;
};
static enum power_supply_property jbl_battery_props[] = {
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_MODEL_NAME,
POWER_SUPPLY_PROP_MANUFACTURER,
POWER_SUPPLY_PROP_PRESENT
};
static int jbl_battery_get_property(
struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
struct jbl_headset_data *jbl = power_supply_get_drvdata(psy);
switch (psp) {
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = jbl->capacity; // e.g. 0..100
return 0;
case POWER_SUPPLY_PROP_STATUS:
val->intval = jbl->status;
return 0;
case POWER_SUPPLY_PROP_MODEL_NAME:
val->strval = "JBL Quantum 610";
return 0;
case POWER_SUPPLY_PROP_MANUFACTURER:
val->strval = "JBL";
return 0;
case POWER_SUPPLY_PROP_PRESENT:
val->intval = jbl->present;
return 0;
default:
return -EINVAL;
}
}
static const struct power_supply_desc jbl_battery_desc = {
.name = "JBL_Quantum_610",
.type = POWER_SUPPLY_TYPE_WIRELESS,
.properties = jbl_battery_props,
.num_properties = ARRAY_SIZE(jbl_battery_props),
.get_property = jbl_battery_get_property,
};
static int jbl_raw_event(struct hid_device *hdev, struct hid_report *report,
u8 *data, int size)
{
struct jbl_headset_data *jbl = hid_get_drvdata(hdev);
if (size > 1 && data[0] == 0x08) {
jbl->present = true; // if we have any reports from device, that's mean that it is now connected
int new_cap = data[1];
if (new_cap != jbl->capacity) {
dev_info(&hdev->dev, "Capacity changed, old value: %d, new: %d", jbl->capacity, new_cap);
if(new_cap > jbl->capacity) {
jbl->status = POWER_SUPPLY_STATUS_CHARGING;
} else {
jbl->status = POWER_SUPPLY_STATUS_DISCHARGING;
}
if(new_cap == 100) {
jbl->status = POWER_SUPPLY_STATUS_FULL;
}
char buffer[30];
sprintf(buffer, "JBL610_BATTERY=%d", new_cap);
jbl_netlink_broadcast(buffer);
jbl->capacity = new_cap;
power_supply_changed(jbl->battery);
}
}
// device disconnected
if(size > 1 && data[0] == 0x09) {
jbl->present = 0;
}
// device connected
if(size > 1 && data[0] == 0x07 && data[1] == 0x01) {
jbl->present = 1;
}
if(size > 1 && data[0] == 0x06) {
int muted = data[1] == 0 ? 1 : 0;
char buf[20];
if(muted) {
sprintf(buf, "JBL610_DEVICE_MUTED");
} else {
sprintf(buf, "JBL610_DEVICE_UNMUTED");
}
jbl_netlink_broadcast(buf);
jbl->muted = muted;
}
return 0;
}
static ssize_t mic_mute_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct jbl_headset_data *jbl = dev_get_drvdata(dev);
return sprintf(buf, "%d\n", jbl->muted ? 1 : 0);
}
static DEVICE_ATTR_RO(mic_mute); // read-only attribute, for example
static int jbl_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
int ret;
struct jbl_headset_data *jbl;
struct power_supply_config psy_cfg = { };
/* Allocate memory for our driver data */
jbl = devm_kzalloc(&hdev->dev, sizeof(*jbl), GFP_KERNEL);
if (!jbl)
return -ENOMEM;
jbl->hdev = hdev;
hid_set_drvdata(hdev, jbl);
/* Register with the HID subsystem */
ret = hid_parse(hdev);
if (ret) {
dev_err(&hdev->dev, "hid_parse failed\n");
return ret;
}
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
dev_err(&hdev->dev, "hid_hw_start failed\n");
return ret;
}
/* Setup power_supply */
psy_cfg.drv_data = jbl; // we can retrieve jbl in get_property
jbl->capacity = 50; // default (unknown) until we get a real reading
jbl->status = POWER_SUPPLY_STATUS_DISCHARGING;
jbl->battery = devm_power_supply_register(&hdev->dev,
&jbl_battery_desc,
&psy_cfg);
if (IS_ERR(jbl->battery)) {
dev_err(&hdev->dev, "Failed to register power supply\n");
hid_hw_stop(hdev);
return PTR_ERR(jbl->battery);
}
ret = device_create_file(&hdev->dev, &dev_attr_mic_mute);
if (ret) {
dev_err(&hdev->dev, "Failed to create mic_mute attribute\n");
return ret;
}
struct netlink_kernel_cfg cfg = {
.input = NULL, // We do NOT receive from user space
.groups = 0,
};
pr_info("jbl610: netlink Initializing...\n");
nl_sk = netlink_kernel_create(&init_net, NETLINK_USERSOCK, &cfg);
//nl_sk =netlink_kernel_create(&init_net, NETLINK_USERSOCK, 1, jbl_nl_receive_callback, NULL, THIS_MODULE)
if (!nl_sk) {
pr_err("jbl610: Error creating netlink socket.\n");
return -ENOMEM;
}
/* Example: Send a test broadcast right away (for demonstration). */
// jbl_netlink_broadcast("jbl_driver_initialized");
dev_info(&hdev->dev, "JBL headset driver probed\n");
return 0;
}
static void jbl_nl_receive_callback(struct sk_buff *skb) {
nlmsg_free(skb);
}
/* Called when the driver is unbound or device is removed */
static void jbl_remove(struct hid_device *hdev)
{
if (nl_sk) {
netlink_kernel_release(nl_sk);
nl_sk = NULL;
}
pr_info("jbl610: Exiting.\n");
device_remove_file(&hdev->dev, &dev_attr_mic_mute);
hid_hw_stop(hdev);
/* power_supply is auto-freed by devm_ once the device is gone */
}
static struct hid_driver jbl_driver = {
.name = "jbl_headset",
.id_table = jbl_hid_table,
.probe = jbl_probe,
.remove = jbl_remove,
.raw_event = jbl_raw_event, // We'll get HID input packets here
};
module_hid_driver(jbl_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kirill Ivlev <webster.spb@gmail.com>");
MODULE_DESCRIPTION("Kernel driver for JBL Headset battery");