WITRN produces a series of devices to monitor power characteristics of
USB connections and display those on a on-device display. Most of them
contain an additional port which exposes the measurements via USB HID.
Add a barebone HID driver to collect these measurements. The monitoring
support can be implemented later.
Developed and tested with WITRN K2 USB-C tester.
Signed-off-by: Rong Zhang <i@rong.moe>
---
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/witrn.rst | 23 ++++
MAINTAINERS | 7 ++
drivers/hwmon/Kconfig | 10 ++
drivers/hwmon/Makefile | 1 +
drivers/hwmon/witrn.c | 268 ++++++++++++++++++++++++++++++++++++++++++
6 files changed, 310 insertions(+)
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index b2ca8513cfcd..f1f2b599c76b 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -276,6 +276,7 @@ Hardware Monitoring Kernel Drivers
w83795
w83l785ts
w83l786ng
+ witrn
wm831x
wm8350
xgene-hwmon
diff --git a/Documentation/hwmon/witrn.rst b/Documentation/hwmon/witrn.rst
new file mode 100644
index 000000000000..e64c527928d0
--- /dev/null
+++ b/Documentation/hwmon/witrn.rst
@@ -0,0 +1,23 @@
+.. SPDX-License-Identifier: GPL-2.0+
+
+Kernel driver witrn
+====================
+
+Supported chips:
+
+ * WITRN K2
+
+ Prefix: 'witrn'
+
+ Addresses scanned: -
+
+Author:
+
+ - Rong Zhang <i@rong.moe>
+
+Description
+-----------
+
+This driver implements support for the WITRN USB tester family.
+
+The device communicates with the custom protocol over USB HID.
diff --git a/MAINTAINERS b/MAINTAINERS
index 0481aca2286c..18a1077d38e7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28444,6 +28444,13 @@ M: Miloslav Trmac <mitr@volny.cz>
S: Maintained
F: drivers/input/misc/wistron_btns.c
+WITRN USB TESTER HARDWARE MONITOR DRIVER
+M: Rong Zhang <i@rong.moe>
+L: linux-hwmon@vger.kernel.org
+S: Maintained
+F: Documentation/hwmon/witrn.rst
+F: drivers/hwmon/witrn.c
+
WMI BINARY MOF DRIVER
M: Armin Wolf <W_Armin@gmx.de>
R: Thomas Weißschuh <linux@weissschuh.net>
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 7ad909033e79..746184608f81 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -2629,6 +2629,16 @@ config SENSORS_W83627EHF
This driver can also be built as a module. If so, the module
will be called w83627ehf.
+config SENSORS_WITRN
+ tristate "WITRN USB tester"
+ depends on USB_HID
+ help
+ If you say yes here you get support for WITRN USB charging
+ testers.
+
+ This driver can also be built as a module. If so, the module
+ will be called witrn.
+
config SENSORS_WM831X
tristate "WM831x PMICs"
depends on MFD_WM831X
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 6dba69f712be..f87eb1710974 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -243,6 +243,7 @@ obj-$(CONFIG_SENSORS_VT8231) += vt8231.o
obj-$(CONFIG_SENSORS_W83627EHF) += w83627ehf.o
obj-$(CONFIG_SENSORS_W83L785TS) += w83l785ts.o
obj-$(CONFIG_SENSORS_W83L786NG) += w83l786ng.o
+obj-$(CONFIG_SENSORS_WITRN) += witrn.o
obj-$(CONFIG_SENSORS_WM831X) += wm831x-hwmon.o
obj-$(CONFIG_SENSORS_WM8350) += wm8350-hwmon.o
obj-$(CONFIG_SENSORS_XGENE) += xgene-hwmon.o
diff --git a/drivers/hwmon/witrn.c b/drivers/hwmon/witrn.c
new file mode 100644
index 000000000000..e8713da6de5a
--- /dev/null
+++ b/drivers/hwmon/witrn.c
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * witrn - Driver for WITRN USB charging testers
+ *
+ * Copyright (C) 2026 Rong Zhang <i@rong.moe>
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/bitops.h>
+#include <linux/completion.h>
+#include <linux/cleanup.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/limits.h>
+#include <linux/module.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/units.h>
+#include <linux/workqueue.h>
+
+#define DRIVER_NAME "witrn"
+#define WITRN_EP_CMD_OUT 0x01
+#define WITRN_EP_DATA_IN 0x81
+
+#define WITRN_REPORT_SZ 64
+
+/* flags */
+#define WITRN_HID_OPENED 0
+
+/*
+ * The device sends reports every 10ms (100Hz!) once it's opened, which is
+ * really annoying and produces a lot of irq noise.
+ *
+ * Unfortunately, the device doesn't provide any command to start/stop reporting
+ * on demand -- it simply spams reports blindly. The only way to stop reporting
+ * is to close the HID device (i.e., to stop IN URB (re)submission).
+ *
+ * Let's close the HID device if the device has not been accessed for a while.
+ */
+#define PAUSE_TIMEOUT secs_to_jiffies(8)
+#define UP_TO_DATE_TIMEOUT msecs_to_jiffies(100)
+
+enum witrn_report_type {
+ WITRN_PD = 0xfe,
+ WITRN_SENSOR = 0xff,
+};
+
+struct witrn_sensor {
+ __le16 record_threshold; /* mA */
+ __le32 record_charge; /* Ah (float) */
+ __le32 record_energy; /* Wh (float) */
+ __le32 record_time; /* s */
+ __le32 uptime; /* s */
+ __le32 vdp; /* V (float) */
+ __le32 vdm; /* V (float) */
+ u8 __unknown[4];
+ __le32 temp_ntc; /* Celsius (float) */
+ __le32 vbus; /* V (float) */
+ __le32 ibus; /* A (float) */
+ u8 record_group; /* 0: group 1 on device, ... */
+ u8 vcc1; /* dV */
+ u8 vcc2; /* dV */
+} __packed;
+
+struct witrn_report {
+ u8 report_type;
+ u8 __unknown_0[11];
+
+ struct witrn_sensor sensor;
+
+ u8 __unknown_1[7];
+} __packed;
+static_assert(sizeof(struct witrn_report) == WITRN_REPORT_SZ);
+
+struct witrn_priv {
+ struct hid_device *hdev;
+
+ struct work_struct pause_work;
+
+ unsigned long flags;
+
+ spinlock_t lock; /* Protects members below */
+
+ struct completion completion;
+ unsigned long last_update; /* jiffies */
+ unsigned long last_access; /* jiffies */
+
+ struct witrn_sensor sensor;
+};
+
+static inline bool sensor_is_outdated(struct witrn_priv *priv)
+{
+ return time_after(jiffies, priv->last_update + UP_TO_DATE_TIMEOUT);
+}
+
+static inline bool hwmon_is_inactive(struct witrn_priv *priv)
+{
+ return time_after(jiffies, priv->last_access + PAUSE_TIMEOUT);
+}
+
+/* ======== HID ======== */
+
+static int witrn_open_hid(struct witrn_priv *priv)
+{
+ int ret;
+
+ if (test_and_set_bit(WITRN_HID_OPENED, &priv->flags))
+ return 0; /* Already opened */
+
+ hid_dbg(priv->hdev, "opening hid hw\n");
+
+ ret = hid_hw_open(priv->hdev);
+ if (ret) {
+ hid_err(priv->hdev, "hid hw open failed with %d\n", ret);
+ clear_bit(WITRN_HID_OPENED, &priv->flags);
+ }
+
+ return ret;
+}
+
+static void witrn_close_hid(struct witrn_priv *priv)
+{
+ if (!test_and_clear_bit(WITRN_HID_OPENED, &priv->flags))
+ return; /* Already closed */
+
+ hid_dbg(priv->hdev, "closing hid hw\n");
+
+ hid_hw_close(priv->hdev);
+}
+
+static void witrn_pause_hid(struct work_struct *work)
+{
+ struct witrn_priv *priv = container_of(work, struct witrn_priv, pause_work);
+
+ scoped_guard(spinlock, &priv->lock) {
+ /* Double check. Condition may change after being scheduled. */
+ if (!hwmon_is_inactive(priv))
+ return;
+ }
+
+ witrn_close_hid(priv);
+}
+
+static int witrn_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct witrn_priv *priv = hid_get_drvdata(hdev);
+ const struct witrn_report *wreport;
+ bool do_pause = false;
+
+ /* HIDRAW has opened the device while we are pausing. */
+ if (!test_bit(WITRN_HID_OPENED, &priv->flags))
+ return 0;
+
+ if (size < WITRN_REPORT_SZ) {
+ hid_dbg(hdev, "report size mismatch: %d < %d\n", size, WITRN_REPORT_SZ);
+ return 0;
+ }
+
+ wreport = (const struct witrn_report *)data;
+ if (wreport->report_type != WITRN_SENSOR) {
+ hid_dbg(hdev, "report ignored with type 0x%02x", wreport->report_type);
+ return 0;
+ }
+
+ scoped_guard(spinlock, &priv->lock) {
+ priv->last_update = jiffies;
+ do_pause = hwmon_is_inactive(priv);
+
+ memcpy(&priv->sensor, &wreport->sensor, sizeof(wreport->sensor));
+ complete(&priv->completion);
+ }
+
+ if (do_pause)
+ schedule_work(&priv->pause_work);
+
+ return 0;
+}
+
+static int witrn_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ struct device *parent = &hdev->dev;
+ struct witrn_priv *priv;
+ int ret;
+
+ priv = devm_kzalloc(parent, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->hdev = hdev;
+ hid_set_drvdata(hdev, priv);
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "hid parse failed with %d\n", ret);
+ return ret;
+ }
+
+ /* Enable HIDRAW so existing user-space tools can continue to work. */
+ ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (ret) {
+ hid_err(hdev, "hid hw start failed with %d\n", ret);
+ return ret;
+ }
+
+ spin_lock_init(&priv->lock);
+ init_completion(&priv->completion);
+
+ INIT_WORK(&priv->pause_work, witrn_pause_hid);
+
+ priv->last_access = jiffies;
+ priv->last_update = priv->last_access - UP_TO_DATE_TIMEOUT - 1;
+ clear_bit(WITRN_HID_OPENED, &priv->flags);
+
+ ret = witrn_open_hid(priv);
+ if (ret) {
+ hid_hw_stop(hdev);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void witrn_remove(struct hid_device *hdev)
+{
+ struct witrn_priv *priv = hid_get_drvdata(hdev);
+
+ witrn_close_hid(priv);
+
+ /* Cancel it after closing HID so that it won't be rescheduled. */
+ cancel_work_sync(&priv->pause_work);
+
+ hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id witrn_id_table[] = {
+ { HID_USB_DEVICE(0x0716, 0x5060) }, /* WITRN K2 USB-C tester */
+ { }
+};
+
+MODULE_DEVICE_TABLE(hid, witrn_id_table);
+
+static struct hid_driver witrn_driver = {
+ .name = DRIVER_NAME,
+ .id_table = witrn_id_table,
+ .probe = witrn_probe,
+ .remove = witrn_remove,
+ .raw_event = witrn_raw_event,
+};
+
+static int __init witrn_init(void)
+{
+ return hid_register_driver(&witrn_driver);
+}
+
+static void __exit witrn_exit(void)
+{
+ hid_unregister_driver(&witrn_driver);
+}
+
+/* When compiled into the kernel, initialize after the HID bus */
+late_initcall(witrn_init);
+module_exit(witrn_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
+MODULE_DESCRIPTION("WITRN USB tester driver");
--
2.53.0
© 2016 - 2026 Red Hat, Inc.