From c9ff8adb1523d32a92ed4389d2fd105536c6f7fc Mon Sep 17 00:00:00 2001 From: Yicong Hui Date: Fri, 21 Aug 2026 20:18:49 +0200 Subject: [PATCH 1/3] HID: dockchannel-hid: forward correct report type on SET_REPORT dchid_set_report() always sends HID_OUTPUT_REPORT regardless of the report type requested by the caller. Inline it into its sole caller and forward rtype directly, so SET_REPORT works correctly for feature reports too, not just output reports, and to simplify function calls. Signed-off-by: Yicong Hui --- drivers/hid/dockchannel-hid/dockchannel-hid.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/drivers/hid/dockchannel-hid/dockchannel-hid.c b/drivers/hid/dockchannel-hid/dockchannel-hid.c index 9cab638b30c328..68f6625313ea32 100644 --- a/drivers/hid/dockchannel-hid/dockchannel-hid.c +++ b/drivers/hid/dockchannel-hid/dockchannel-hid.c @@ -593,12 +593,6 @@ static int dchid_get_report_cmd(struct dchid_iface *iface, u8 reportnum, void *b return ret <= 0 ? ret : ret - 1; } -/* Note: buf includes report number! */ -static int dchid_set_report(struct dchid_iface *iface, void *buf, size_t len) -{ - return dchid_cmd(iface, HID_OUTPUT_REPORT, REQ_SET_REPORT, buf, len, NULL, 0); -} - static int dchid_raw_request(struct hid_device *hdev, unsigned char reportnum, __u8 *buf, size_t len, unsigned char rtype, int reqtype) @@ -610,7 +604,7 @@ static int dchid_raw_request(struct hid_device *hdev, buf[0] = reportnum; return dchid_cmd(iface, rtype, REQ_GET_REPORT, &reportnum, 1, buf + 1, len - 1); case HID_REQ_SET_REPORT: - return dchid_set_report(iface, buf, len); + return dchid_cmd(iface, rtype, REQ_SET_REPORT, buf, len, NULL, 0); default: return -EIO; } From 33d1f0c57d81e8a01d7d3917d30c691eb5e9a57e Mon Sep 17 00:00:00 2001 From: Yicong Hui Date: Fri, 21 Aug 2026 20:29:06 +0200 Subject: [PATCH 2/3] HID: apple-haptic: add driver for apple MTP haptic touchpad actuator Adds a driver to control the actuator of the MTP touchpad's actuator for M2 and later devices. The specific hardware commands and their fields have been reverse engineered on my own machine (Macbook Pro 2023 M2 Max) using the m1n1 hypervisor. Signed-off-by: Yicong Hui --- drivers/hid/Kconfig | 12 +++ drivers/hid/Makefile | 1 + drivers/hid/hid-apple-mtp-haptic.c | 117 +++++++++++++++++++++++++++++ include/linux/soc/apple/actuator.h | 19 +++++ 4 files changed, 149 insertions(+) create mode 100644 drivers/hid/hid-apple-mtp-haptic.c create mode 100644 include/linux/soc/apple/actuator.h diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 18244ecf301614..8d48546911aa98 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -146,6 +146,17 @@ config HID_APPLE Say Y here if you want support for keyboards of Apple iBooks, PowerBooks, MacBooks, MacBook Pros and Apple Aluminum. +config HID_APPLE_MTP_HAPTIC + tristate "Apple MTP Haptic Actuator support" + depends on HID + help + This driver enables support for userspace controlled haptics on Apple Silicon devices + using the MTP protocol (M2 and newer MacBooks). Say Y here if you have an M2 or + newer MacBook. + + This driver can also be built as a module. If so, it will be called hid-apple-mtp-haptic. + + config HID_APPLEIR tristate "Apple infrared receiver" depends on (USB_HID) @@ -739,6 +750,7 @@ config LOGIWHEELS_FF config HID_MAGICMOUSE tristate "Apple Magic Mouse/Trackpad multi-touch support" + depends on (HID && HID_APPLE_MTP_HAPTIC) || HID_APPLE_MTP_HAPTIC=n help Support for the Apple Magic Mouse/Trackpad multi-touch. diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index d2579e299b5e51..97f6bb600c0aed 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -29,6 +29,7 @@ obj-$(CONFIG_HID_ACCUTOUCH) += hid-accutouch.o obj-$(CONFIG_HID_ALPS) += hid-alps.o obj-$(CONFIG_HID_ACRUX) += hid-axff.o obj-$(CONFIG_HID_APPLE) += hid-apple.o +obj-$(CONFIG_HID_APPLE_MTP_HAPTIC) += hid-apple-mtp-haptic.o obj-$(CONFIG_HID_APPLEIR) += hid-appleir.o obj-$(CONFIG_HID_APPLETB_BL) += hid-appletb-bl.o obj-$(CONFIG_HID_APPLETB_KBD) += hid-appletb-kbd.o diff --git a/drivers/hid/hid-apple-mtp-haptic.c b/drivers/hid/hid-apple-mtp-haptic.c new file mode 100644 index 00000000000000..85606596304351 --- /dev/null +++ b/drivers/hid/hid-apple-mtp-haptic.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "hid-ids.h" + +int apple_taptic_send(struct hid_device *haptic_hdev, u16 effect_type, u8 strength, u8 softness) +{ + int ret; + u8 *msg; + int msg_length; + u8 event_click[] = {0x53, 0x01, strength, softness, 0x03, 0x02, 0x22, 0x48, 0x49, + 0x01, 0x04, 0x60, 0x17, 0x2D, 0x03, 0x02, 0x18, 0x32, 0x1E}; + u8 event_deep_click[] = {0x53, 0x01, strength, softness, 0x01, 0x07, 0x18, 0x18, 0x4F, + 0x02, 0x02, 0x60, 0x0C, 0x25}; + u8 event_release[] = {0x53, 0x01, strength, softness}; + + switch (effect_type) { + case HID_HP_WAVEFORMPRESS & HID_USAGE: + msg = event_click; + msg_length = sizeof(event_click); + break; + case APPLE_HP_WAVEFORMDEEPCLICK & HID_USAGE: + msg = event_deep_click; + msg_length = sizeof(event_deep_click); + break; + case HID_HP_WAVEFORMRELEASE & HID_USAGE: + msg = event_release; + msg_length = sizeof(event_release); + break; + default: + return -EINVAL; + } + + msg = kmemdup(msg, msg_length, GFP_KERNEL); + if (!msg) + return -ENOMEM; + + ret = hid_hw_raw_request(haptic_hdev, msg[0], msg, msg_length, + HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); + kfree(msg); + + if (ret < 0) + return ret; + return 0; +} +EXPORT_SYMBOL_GPL(apple_taptic_send); + +int apple_taptic_switch_modes(struct hid_device *haptic_hdev, bool enable_host_controlled) +{ + int ret; + u8 *buf; + u8 msg[] = { 0x21, enable_host_controlled }; + + buf = kmemdup(msg, sizeof(msg), GFP_KERNEL); + if (!buf) + return -ENOMEM; + + ret = hid_hw_raw_request(haptic_hdev, buf[0], buf, sizeof(msg), + HID_FEATURE_REPORT, HID_REQ_SET_REPORT); + kfree(buf); + + if (ret < 0) + return ret; + return 0; +} +EXPORT_SYMBOL_GPL(apple_taptic_switch_modes); + +static int apple_actuator_probe(struct hid_device *haptic_hdev, const struct hid_device_id *id) +{ + int ret; + + ret = hid_parse(haptic_hdev); + if (ret) { + hid_err(haptic_hdev, "apple_haptic hid parse failed\n"); + return ret; + } + + ret = hid_hw_start(haptic_hdev, 0); + if (ret) { + hid_err(haptic_hdev, "apple_haptic hw start failed\n"); + return ret; + } + + return 0; +} + +static bool actuator_match(struct hid_device *hdev, bool ignore_special_drivers) +{ + return (strcmp(hdev->name, "Apple MTP actuator") == 0); +} + +static void actuator_remove(struct hid_device *haptic_hdev) +{ + apple_taptic_switch_modes(haptic_hdev, 0); + hid_hw_stop(haptic_hdev); +} + +static const struct hid_device_id apple_haptic_devices[] = { + { HID_DEVICE(BUS_HOST, HID_GROUP_ANY, HOST_VENDOR_ID_APPLE, + HID_ANY_ID), .driver_data = 0 }, + { } +}; +MODULE_DEVICE_TABLE(hid, apple_haptic_devices); + +static struct hid_driver apple_actuator_driver = { + .name = "hid-apple-mtp-haptic", + .id_table = apple_haptic_devices, + .probe = apple_actuator_probe, + .match = actuator_match, + .remove = actuator_remove +}; +module_hid_driver(apple_actuator_driver); + +MODULE_DESCRIPTION("Apple MTP Haptic Actuator driver"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/soc/apple/actuator.h b/include/linux/soc/apple/actuator.h new file mode 100644 index 00000000000000..68a24a91d52868 --- /dev/null +++ b/include/linux/soc/apple/actuator.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef __APPLE_MTP_ACTUATOR_H__ +#define __APPLE_MTP_ACTUATOR_H__ +#include + +#define APPLE_HP_WAVEFORMDEEPCLICK 0x000e2001 + +#if IS_ENABLED(CONFIG_HID_APPLE_MTP_HAPTIC) +int apple_taptic_send(struct hid_device *hdev, u16 effect_type, u8 strength, u8 softness); +int apple_taptic_switch_modes(struct hid_device *hdev, bool currently_host_controlled); + +#else +static inline int apple_taptic_send(struct hid_device *hdev, u16 effect_type, + u8 strength, u8 softness) { return -ENODEV; } +static inline int apple_taptic_switch_modes(struct hid_device *hdev, + bool enable_host_controlled) { return -ENODEV; } +#endif +#endif /* __APPLE_MTP_ACTUATOR_H__ */ From 807128b8cd7fe646d3596e91e0d8b9a69008e53b Mon Sep 17 00:00:00 2001 From: Yicong Hui Date: Fri, 21 Aug 2026 21:23:49 +0200 Subject: [PATCH 3/3] HID: apple-haptic: add support for controlling MTP touchpad haptics Integrate the MTP actuator driver with the MTP trackpad driver to expose new functionality that allows userspace to control the haptic effects of the touchpad actuator as a force feedback device, following the convention for haptic touchpad functionality as specified in [1]. Set the INPUT_PROP_PRESSUREPAD property within the MTP trackpad device indicating that this device is a haptic touchpad, and set the FF_HAPTIC capability to indicate that force feedback haptic effects can be uploaded to the trackpad [2]. Userspace can thus choose to upload force feedback effects to the touchpad, which would trigger it to switch into "Host controlled" mode, where the touchpad will no longer click unless signalled to do so by userspace. This patch uses an unconventional means of doing so, by writing a separate loadable module for the haptic actuator. This is because the haptic actuator is considered a separate device on MTP trackpads, whereas haptics for HUTRR63 (And the other kernel drivers) considers the haptic actuator and the touchpad to be the very same device. Therefore, to conform to the standardized interface in [1] this patch creates a separate driver for the separate HID device, and is designed such that commands to the actuator are sent through the touchpad. This changeset is relevant because allowing userspace control of the haptics allows for user control of: 1. Click feedback intensity 2. Sensitivity of when clicks trigger 3. Configuring "Force Touch"-like deep click functionality [1] https://lore.kernel.org/all/20250818-support-forcepads-v3-0-e4f9ab0add84@google.com/T/#m12f9be6691015fb9b2e834248c368e5f90 [2] https://docs.kernel.org/input/event-codes.html#input-prop-pressurepad Signed-off-by: Yicong Hui --- drivers/hid/hid-magicmouse.c | 259 ++++++++++++++++++++++++++++++++++- 1 file changed, 258 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-magicmouse.c b/drivers/hid/hid-magicmouse.c index a770ceccabde6b..859fe8ad92f229 100644 --- a/drivers/hid/hid-magicmouse.c +++ b/drivers/hid/hid-magicmouse.c @@ -17,6 +17,8 @@ #include #include #include +#include "hid-haptic.h" +#include #include "hid-ids.h" @@ -138,6 +140,14 @@ struct magicmouse_input_ops { int (*setup_input)(struct input_dev *input, struct hid_device *hdev); }; +struct effect_job { + struct work_struct work; + struct hid_device *taptic_hdev; + u16 effect_type; + u8 strength; + u8 softness; +}; + /** * struct magicmouse_sc - Tracks Magic Mouse-specific data. * @input: Input device through which we report events. @@ -175,6 +185,10 @@ struct magicmouse_sc { } touches[MAX_CONTACTS]; int tracking_ids[MAX_CONTACTS]; + struct hid_haptic_device *haptics; + struct hid_device *taptic_hdev; + struct effect_job *haptic_effects; + struct hid_device *hdev; struct delayed_work work; struct timer_list battery_timer; @@ -1042,6 +1056,7 @@ static int magicmouse_setup_input_mtp(struct input_dev *input, struct magicmouse_sc *msc = hid_get_drvdata(hdev); __set_bit(INPUT_PROP_BUTTONPAD, input->propbit); + __set_bit(INPUT_PROP_PRESSUREPAD, input->propbit); __clear_bit(BTN_0, input->keybit); __clear_bit(BTN_RIGHT, input->keybit); __clear_bit(BTN_MIDDLE, input->keybit); @@ -1049,7 +1064,7 @@ static int magicmouse_setup_input_mtp(struct input_dev *input, __clear_bit(REL_X, input->relbit); __clear_bit(REL_Y, input->relbit); - mt_flags = INPUT_MT_POINTER | INPUT_MT_DROP_UNUSED | INPUT_MT_TRACK; + mt_flags = INPUT_MT_POINTER | INPUT_MT_DROP_UNUSED | INPUT_MT_TRACK | INPUT_MT_TOTAL_FORCE; /* finger touch area */ input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, 5000, 0, 0); @@ -1150,6 +1165,29 @@ static int magicmouse_input_mapping(struct hid_device *hdev, return 0; } +static int match_actuator(struct device *dev, const void *data) +{ + if (dev->bus != &hid_bus_type) + return 0; + + struct hid_device *hdev = to_hid_device(dev); + + return hdev && !strcmp(hdev->name, "Apple MTP actuator"); +} + +static int magicmouse_switch_mode(struct input_dev *trackpad_idev, int mode) +{ + struct hid_device *trackpad_hdev = input_get_drvdata(trackpad_idev); + struct magicmouse_sc *msc = hid_get_drvdata(trackpad_hdev); + int ret; + + hid_info(trackpad_hdev, "Switching modes to %i\n", mode); + + ret = apple_taptic_switch_modes(msc->taptic_hdev, mode); + + return ret; +} + static int magicmouse_input_configured(struct hid_device *hdev, struct hid_input *hi) @@ -1288,6 +1326,208 @@ static void magicmouse_battery_timer_tick(struct timer_list *t) } } +static int apple_upload_effects(struct input_dev *trackpad_idev, + struct ff_effect *effect, struct ff_effect *old) +{ + struct hid_device *trackpad_hdev = input_get_drvdata(trackpad_idev); + struct magicmouse_sc *msc = hid_get_drvdata(trackpad_hdev); + struct hid_haptic_device *haptics = msc->haptics; + int ret; + + switch (effect->u.haptic.hid_usage) { + case (HID_HP_WAVEFORMPRESS & HID_USAGE): + case (HID_HP_WAVEFORMRELEASE & HID_USAGE): + case (APPLE_HP_WAVEFORMDEEPCLICK & HID_USAGE): + break; + default: + return -EINVAL; + } + + msc->haptic_effects[effect->id].taptic_hdev = msc->taptic_hdev; + msc->haptic_effects[effect->id].effect_type = (effect->u.haptic.hid_usage) & HID_USAGE; + msc->haptic_effects[effect->id].strength = + (min(100, (effect->u.haptic.intensity)) * 255) / 100; + msc->haptic_effects[effect->id].softness = 0x90; + /* A future extension could add configurability to softness. */ + + if (haptics->mode == HID_HAPTIC_MODE_DEVICE) { + ret = magicmouse_switch_mode(trackpad_idev, HID_HAPTIC_MODE_HOST); + + if (ret) { + dev_err(&msc->hdev->dev, "Error: Unable to switch mouse to host-controlled mode."); + msc->haptic_effects[effect->id].effect_type = 0; + return ret; + } + + haptics->mode = HID_HAPTIC_MODE_HOST; + } + + hid_info(trackpad_hdev, "Successfully uploaded effects!!\n"); + return 0; +} + +static void haptic_playback_worker(struct work_struct *ws) +{ + struct effect_job *job = container_of(ws, struct effect_job, work); + + struct hid_device *taptic_hdev = job->taptic_hdev; + + if (job->effect_type && apple_taptic_send(taptic_hdev, job->effect_type, + job->strength, job->softness)) + pr_err("apple-haptic: unable to send haptic event.\n"); +} + +static int apple_taptic_playback(struct input_dev *trackpad_idev, int effect_id, int value) +{ + struct hid_device *trackpad_hdev = input_get_drvdata(trackpad_idev); + struct magicmouse_sc *msc = hid_get_drvdata(trackpad_hdev); + + if (value) + queue_work(msc->haptics->wq, &msc->haptic_effects[effect_id].work); + + return 0; +} + +static int apple_taptic_erase(struct input_dev *trackpad_idev, int effect_id) +{ + struct hid_device *trackpad_hdev = input_get_drvdata(trackpad_idev); + struct magicmouse_sc *msc = hid_get_drvdata(trackpad_hdev); + int i, ret = 0; + + msc->haptic_effects[effect_id].effect_type = 0; + + for (i = 0; i < FF_MAX_EFFECTS; i++) { + if (msc->haptic_effects[i].effect_type != 0) + return 0; + } + + /* Return to device-controlled mode if there are no effects left */ + if (msc->haptics->mode == HID_HAPTIC_MODE_HOST) { + flush_workqueue(msc->haptics->wq); + ret = magicmouse_switch_mode(trackpad_idev, HID_HAPTIC_MODE_DEVICE); + + if (ret) { + dev_err(&msc->hdev->dev, "Error: Unable to switch mouse back to device-controlled mode."); + return ret; + } + + msc->haptics->mode = HID_HAPTIC_MODE_DEVICE; + } + + return 0; +} + +static void apple_taptic_destroy(struct ff_device *ff) +{ + struct hid_haptic_device *haptic_dev = ff->private; + struct magicmouse_sc *msc = hid_get_drvdata(haptic_dev->hdev); + int ret; + + if (msc->haptics && msc->haptics->mode == HID_HAPTIC_MODE_HOST) { + flush_workqueue(msc->haptics->wq); + ret = magicmouse_switch_mode(msc->input, HID_HAPTIC_MODE_DEVICE); + if (ret) + hid_err(msc->hdev, "Failed to switch back to device-controlled mode.\n"); + + msc->haptics->mode = HID_HAPTIC_MODE_DEVICE; + } + + destroy_workqueue(haptic_dev->wq); + haptic_dev->wq = NULL; + + kfree(msc->haptic_effects); + msc->haptic_effects = NULL; +} + +static int apple_taptic_init_mtp(struct magicmouse_sc *msc, + struct hid_haptic_device *haptic_dev) +{ + struct hid_device *trackpad_hdev = msc->hdev; + struct ff_device *ff; + int ret, i; + + haptic_dev->hdev = trackpad_hdev; + + msc->haptic_effects = kzalloc_objs(struct effect_job, FF_MAX_EFFECTS); + if (!msc->haptic_effects) { + dev_err(&trackpad_hdev->dev, "Cannot allocate haptic effects\n"); + return -ENOMEM; + } + + haptic_dev->wq = create_singlethread_workqueue("Apple trackpad haptics workqueue"); + if (!haptic_dev->wq) { + dev_err(&trackpad_hdev->dev, "Cannot allocate haptic workqueue\n"); + kfree(msc->haptic_effects); + msc->haptic_effects = NULL; + return -ENOMEM; + } + + for (i = 0; i < FF_MAX_EFFECTS; i++) + INIT_WORK(&msc->haptic_effects[i].work, haptic_playback_worker); + + ret = input_ff_create(msc->input, FF_MAX_EFFECTS); + if (ret) { + kfree(msc->haptic_effects); + msc->haptic_effects = NULL; + + destroy_workqueue(haptic_dev->wq); + haptic_dev->wq = NULL; + + dev_err(&trackpad_hdev->dev, "Failed to create force-feedback device.\n"); + return ret; + } + + ff = msc->input->ff; + ff->private = haptic_dev; + ff->upload = apple_upload_effects; + ff->playback = apple_taptic_playback; + ff->erase = apple_taptic_erase; + ff->destroy = apple_taptic_destroy; + + input_set_capability(msc->input, EV_FF, FF_HAPTIC); + + hid_info(trackpad_hdev, "Successfully init'd the haptics\n"); + return 0; +} + +static int magicmouse_init_haptics(struct magicmouse_sc *msc, struct hid_device *trackpad_hdev) +{ +#ifdef CONFIG_HID_APPLE_MTP_HAPTIC + struct device *taptic_dev; + + /* Check if actuator is allocated */ + taptic_dev = device_find_child(trackpad_hdev->dev.parent, NULL, match_actuator); + if (taptic_dev) + msc->taptic_hdev = to_hid_device(taptic_dev); + + if (msc->taptic_hdev) { + hid_info(trackpad_hdev, "Successfully bound trackpad drivers to the actuator\n"); + + msc->haptics = devm_kzalloc(&trackpad_hdev->dev, sizeof(*msc->haptics), GFP_KERNEL); + + if (!msc->haptics) { + dev_warn(&trackpad_hdev->dev, "Cannot allocate haptics for %s\n", trackpad_hdev->name); + put_device(taptic_dev); + msc->taptic_hdev = NULL; + + return -ENOMEM; + } + + msc->haptics->hdev = trackpad_hdev; + + if (apple_taptic_init_mtp(msc, msc->haptics)) { + put_device(taptic_dev); + msc->taptic_hdev = NULL; + + devm_kfree(&trackpad_hdev->dev, msc->haptics); + msc->haptics = NULL; + } + } +#endif + + return 0; +} + static int magicmouse_probe(struct hid_device *hdev, const struct hid_device_id *id) { @@ -1356,6 +1596,11 @@ static int magicmouse_probe(struct hid_device *hdev, goto err_stop_hw; } + ret = magicmouse_init_haptics(msc, hdev); + + if (ret) + goto err_stop_hw; + switch (id->product) { case USB_DEVICE_ID_APPLE_MAGICMOUSE: report = hid_register_report(hdev, HID_INPUT_REPORT, MOUSE_REPORT_ID, 0); @@ -1416,6 +1661,12 @@ static int magicmouse_probe(struct hid_device *hdev, timer_delete_sync(&msc->battery_timer); hid_hw_stop(hdev); + + if (msc->taptic_hdev) { + put_device(&msc->taptic_hdev->dev); + msc->taptic_hdev = NULL; + } + return ret; } @@ -1431,6 +1682,12 @@ static void magicmouse_remove(struct hid_device *hdev) } hid_hw_stop(hdev); + + if (msc && msc->taptic_hdev) { + put_device(&msc->taptic_hdev->dev); + msc->taptic_hdev = NULL; + } + } static const __u8 *magicmouse_report_fixup(struct hid_device *hdev, __u8 *rdesc,