This commit is contained in:
Kirill I 2025-01-29 12:53:47 +04:00
commit 6c60003ceb
9 changed files with 676 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
build
cmake-build-debug
.idea

View file

@ -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)

View file

@ -0,0 +1,106 @@
#include <QApplication>
#include <QTimer>
#include <QFile>
#include <QTextStream>
#include <QDebug>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <errno.h>
#include "netlinkmonitor.h"
// KStatusNotifierItem from KF6::Notifications
#include <KStatusNotifierItem>
#include <KLocalizedString> // 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();
}

View file

@ -0,0 +1,111 @@
#include "netlinkmonitor.h"
#include <QDebug>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#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<struct sockaddr*>(&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;
}
}
}

View file

@ -0,0 +1,31 @@
#ifndef NETLINKMONITOR_H
#define NETLINKMONITOR_H
#include <QObject>
#include <QSocketNotifier>
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

15
kernel/Makefile Normal file
View file

@ -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

6
kernel/dkms.conf Normal file
View file

@ -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"

View file

@ -0,0 +1,282 @@
#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");

69
kernel/netlinktest.c Normal file
View file

@ -0,0 +1,69 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#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;
}