commit 6c60003cebdddc337eed71cd393adc7f64843c45 Author: Kirill I Date: Wed Jan 29 12:53:47 2025 +0400 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d6ded8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +cmake-build-debug +.idea diff --git a/kde-app/JBLQunatumStatus/CMakeLists.txt b/kde-app/JBLQunatumStatus/CMakeLists.txt new file mode 100644 index 0000000..b57d3d8 --- /dev/null +++ b/kde-app/JBLQunatumStatus/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.20) + +project(helloworld) + +set(QT_MIN_VERSION "6.6.0") +set(KF_MIN_VERSION "6.0.0") + +find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(FeatureSummary) + +find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS + Core # QCommandLineParser, QStringLiteral + Widgets # QApplication + Gui + Quick +) + +find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS + CoreAddons # KAboutData + I18n # KLocalizedString + WidgetsAddons # KMessageBox +) + +find_package(KF6StatusNotifierItem) + + +add_executable(helloworld + netlinkmonitor.h + netlinkmonitor.cpp) + +target_sources(helloworld + PRIVATE + main.cpp +) + +target_link_libraries(helloworld + Qt6::Widgets + Qt6::Core + KF6::CoreAddons + KF6::I18n + KF6::WidgetsAddons + Qt6::Quick + KF6::StatusNotifierItem +) + +install(TARGETS helloworld ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) \ No newline at end of file diff --git a/kde-app/JBLQunatumStatus/main.cpp b/kde-app/JBLQunatumStatus/main.cpp new file mode 100644 index 0000000..95bad19 --- /dev/null +++ b/kde-app/JBLQunatumStatus/main.cpp @@ -0,0 +1,106 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "netlinkmonitor.h" + +// KStatusNotifierItem from KF6::Notifications +#include +#include // if you want i18n (need KF6I18n) +#define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1)) + + +static QString readSysfsFile(const QString &path) +{ + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "Cannot open sysfs:" << path; + return QString(); + } + QTextStream in(&file); + return in.readLine().trimmed(); +} + +int main(int argc, char *argv[]) +{ + // Use QApplication because we need a GUI event loop for system tray icons + QApplication app(argc, argv); + + // 1) Create our tray icon via KStatusNotifierItem + KStatusNotifierItem *trayIcon = new KStatusNotifierItem; + trayIcon->setTitle(QStringLiteral("JBL Quantum 610")); + trayIcon->setIconByName(QStringLiteral("audio-headset")); + // (You can also set a custom icon path with setIconByPixmap) + + // 2) Set category & status + trayIcon->setCategory(KStatusNotifierItem::Hardware); + trayIcon->setStatus(KStatusNotifierItem::Active); + + // 3) Function to update the tray tooltip with sysfs data + auto updateTray = [trayIcon]() { + // Example sysfs paths: adjust to your kernel module's actual files + QString batteryStr = readSysfsFile(QStringLiteral("/sys/class/power_supply/JBL_Quantum_610/capacity")); + QString micMuteStr = readSysfsFile(QStringLiteral("/sys/bus/hid/devices/0003:0ECB:205C.0007/mic_mute")); + + if (batteryStr.isEmpty()) { + batteryStr = QStringLiteral("?"); + } + bool isMuted = (micMuteStr.trimmed() == QLatin1String("1")); + + // Compose tooltip title and subtitle + QString tooltipTitle = QStringLiteral("JBL Quantum 610"); + QString tooltipSubTitle = + QStringLiteral("Battery: %1%\nMic: %2") + .arg(batteryStr) + .arg(isMuted ? QStringLiteral("Muted") : QStringLiteral("Unmuted")); + + trayIcon->setToolTipTitle(tooltipTitle); + trayIcon->setToolTipSubTitle(tooltipSubTitle); + + // Optionally change the icon depending on battery level / mute + // For example: + int batteryVal = batteryStr.toInt(); + if (batteryVal < 20) { + trayIcon->setIconByName(QStringLiteral("battery-caution")); + } else { + trayIcon->setIconByName(QStringLiteral("audio-headset")); + } + }; + // 4) QTimer to update every 10 seconds (example) + QTimer timer; + //QObject::connect(&timer, &QTimer::timeout, updateTray); + //timer.start(10000); + NetlinkMonitor netlinkMonitor; + if (!netlinkMonitor.setupNetlink()) { + qWarning("Failed to setup netlink socket. Exiting..."); + return -1; + } + updateTray(); + + QObject::connect(&netlinkMonitor, &NetlinkMonitor::messageReceived, + [trayIcon](const QString &msg) { + printf("%s\n", msg.toStdString().c_str()); + if (msg == QStringLiteral("JBL610_DEVICE_MUTED")) { + printf("MUTED"); + trayIcon->setIconByName(QStringLiteral("audio-headphones")); + } else { + trayIcon->setIconByName(QStringLiteral("audio-headset")); + printf("UNMUTED"); + } + + }); + + + // Call once at startup + + + // 5) Show the tray icon (KStatusNotifierItem auto-shows if the desktop supports it) + return app.exec(); +} \ No newline at end of file diff --git a/kde-app/JBLQunatumStatus/netlinkmonitor.cpp b/kde-app/JBLQunatumStatus/netlinkmonitor.cpp new file mode 100644 index 0000000..48af175 --- /dev/null +++ b/kde-app/JBLQunatumStatus/netlinkmonitor.cpp @@ -0,0 +1,111 @@ +#include "netlinkmonitor.h" +#include +#include +#include +#include +#include +#include + +#define NETLINK_MYPROTO NETLINK_USERSOCK // Matches your kernel module, or use NETLINK_USERSOCK +#define MY_NETLINK_GROUP 27 // Multicast group your kernel module broadcasts on +#define MAX_PAYLOAD 1024 + +NetlinkMonitor::NetlinkMonitor(QObject *parent) + : QObject(parent) + , m_sockFd(-1) + , m_socketNotifier(nullptr) +{ +} + +NetlinkMonitor::~NetlinkMonitor() +{ + if (m_socketNotifier) { + delete m_socketNotifier; + m_socketNotifier = nullptr; + } + if (m_sockFd != -1) { + close(m_sockFd); + m_sockFd = -1; + } +} + +bool NetlinkMonitor::setupNetlink() +{ + // 1. Create netlink socket + m_sockFd = socket(AF_NETLINK, SOCK_RAW, NETLINK_MYPROTO); + if (m_sockFd < 0) { + qWarning() << "Failed to create netlink socket:" << strerror(errno); + return false; + } + + // 2. Bind to a local address with the desired groups + struct sockaddr_nl addr; + memset(&addr, 0, sizeof(addr)); + addr.nl_family = AF_NETLINK; + addr.nl_pid = getpid(); // our user-space PID + // Subscribe to the group (bitmask). For group #1, you set bit (1-1). + addr.nl_groups = 1 << (MY_NETLINK_GROUP - 1); + + if (bind(m_sockFd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + qWarning() << "Failed to bind netlink socket:" << strerror(errno); + close(m_sockFd); + m_sockFd = -1; + return false; + } + + // 3. Create a QSocketNotifier to watch for incoming data + m_socketNotifier = new QSocketNotifier(m_sockFd, QSocketNotifier::Read, this); + connect(m_socketNotifier, &QSocketNotifier::activated, + this, &NetlinkMonitor::handleReadyRead); + + qDebug() << "NetlinkMonitor: socket setup complete, fd =" << m_sockFd; + return true; +} + +void NetlinkMonitor::handleReadyRead() +{ + // This slot is triggered whenever the netlink socket has data to read. + char buffer[MAX_PAYLOAD]; + struct iovec iov = { + .iov_base = buffer, + .iov_len = sizeof(buffer) + }; + struct sockaddr_nl srcAddr; + struct msghdr msg; + struct nlmsghdr *nlh; + + memset(&srcAddr, 0, sizeof(srcAddr)); + memset(&msg, 0, sizeof(msg)); + + msg.msg_name = &srcAddr; + msg.msg_namelen = sizeof(srcAddr); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ssize_t ret = recvmsg(m_sockFd, &msg, 0); + if (ret < 0) { + qWarning() << "recvmsg() failed:" << strerror(errno); + return; + } + + // The netlink message is in the buffer as one or more nlmsghdr blocks. + for (nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, ret); + nlh = NLMSG_NEXT(nlh, ret)) { + + if (nlh->nlmsg_type == NLMSG_DONE) { + // The actual payload is after the nlmsghdr + char *payload = (char *)NLMSG_DATA(nlh); + // Convert to QString, then emit a signal + QString msgStr = QString::fromLatin1(payload); + messageReceived(msgStr); + printf("%s\n", msgStr.toStdString().c_str()); + } + else if (nlh->nlmsg_type == NLMSG_ERROR) { + qWarning() << "Received netlink error message"; + } + else { + // In a real app, handle other message types as needed + qDebug() << "Received netlink type:" << nlh->nlmsg_type; + } + } +} \ No newline at end of file diff --git a/kde-app/JBLQunatumStatus/netlinkmonitor.h b/kde-app/JBLQunatumStatus/netlinkmonitor.h new file mode 100644 index 0000000..ef5c657 --- /dev/null +++ b/kde-app/JBLQunatumStatus/netlinkmonitor.h @@ -0,0 +1,31 @@ +#ifndef NETLINKMONITOR_H +#define NETLINKMONITOR_H + +#include +#include + +class NetlinkMonitor : public QObject +{ + Q_OBJECT +public: + explicit NetlinkMonitor(QObject *parent = nullptr); + ~NetlinkMonitor(); + + // Call this to open and configure the netlink socket. + // Returns true on success, false on failure. + Q_INVOKABLE bool setupNetlink(); + + Q_SIGNALS: + // Emitted whenever a new netlink message arrives. + void messageReceived(const QString &msg); + + private Q_SLOTS: + // Called automatically by QSocketNotifier when netlink data is available. + void handleReadyRead(); + +private: + int m_sockFd; // The netlink socket file descriptor + QSocketNotifier *m_socketNotifier; +}; + +#endif // NETLINKMONITOR_H \ No newline at end of file diff --git a/kernel/Makefile b/kernel/Makefile new file mode 100644 index 0000000..f261929 --- /dev/null +++ b/kernel/Makefile @@ -0,0 +1,15 @@ +obj-m += jbl_headset_battery.o + +all: + make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules +clean: + make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean +install: + mkdir -p /lib/modules/$(shell uname -r)/extra + cp jbl_headset_battery.ko /lib/modules/$(shell uname -r)/extra/ + depmod -a +installdkms: + mkdir -p /usr/src/jbl-headset-0.1/ + cp jbl_headset_battery.c /usr/src/jbl-headset-0.1/ + cp Makefile /usr/src/jbl-headset-0.1/ + cp dkms.conf /usr/src/jbl-headset-0.1 \ No newline at end of file diff --git a/kernel/dkms.conf b/kernel/dkms.conf new file mode 100644 index 0000000..859ea79 --- /dev/null +++ b/kernel/dkms.conf @@ -0,0 +1,6 @@ +MAKE="make" +CLEAN="make clean" +BUILT_MODULE_NAME="jbl_headset_battery" +DEST_MODULE_LOCATION="/extra" +PACKAGE_NAME="jbl-headset" +PACKAGE_VERSION="0.1" \ No newline at end of file diff --git a/kernel/jbl_headset_battery.c b/kernel/jbl_headset_battery.c new file mode 100644 index 0000000..d4511e9 --- /dev/null +++ b/kernel/jbl_headset_battery.c @@ -0,0 +1,282 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "); +MODULE_DESCRIPTION("Kernel driver for JBL Headset battery"); \ No newline at end of file diff --git a/kernel/netlinktest.c b/kernel/netlinktest.c new file mode 100644 index 0000000..85dc340 --- /dev/null +++ b/kernel/netlinktest.c @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include + +#define MAX_PAYLOAD 1024 +#define MYPROTO NETLINK_USERSOCK +#define MY_GROUP 27 + + +int main(void) +{ + int sock_fd; + struct sockaddr_nl addr; + struct nlmsghdr *nlh; + struct iovec iov; + struct msghdr msg; + char buffer[MAX_PAYLOAD]; + + + /* 1. Create netlink socket */ + sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK); + if (sock_fd < 0) { + perror("socket"); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.nl_family = AF_NETLINK; + addr.nl_pid = getpid(); + addr.nl_groups = 1 << (MY_GROUP - 1); + + // (Group is 1-based, sets bit (group-1) for subscription) + + /* 2. Bind the socket */ + if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(sock_fd); + return -1; + } + + + while (1) { + /* 3. Prepare to receive messages in a loop */ + memset(buffer, 0, sizeof(buffer)); + nlh = (struct nlmsghdr *)buffer; + iov.iov_base = (void *)nlh; + iov.iov_len = MAX_PAYLOAD; + + memset(&msg, 0, sizeof(msg)); + msg.msg_name = (void *)&addr; + msg.msg_namelen = sizeof(addr); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + printf("Waiting for kernel broadcast...\n"); + if (recvmsg(sock_fd, &msg, 0) < 0) { + perror("recvmsg"); + break; + } + + printf("Received kernel message: \"%s\"\n", (char *)NLMSG_DATA(nlh)); + } + + close(sock_fd); + return 0; +} \ No newline at end of file