From be8c39d65c51b1384d7f613c16f55eb0d4d43e5a Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Tue, 4 Nov 2025 01:13:42 +0100 Subject: [PATCH 1/4] hid: tmff2: add Thrustmaster T500RS wheel base driver Add initial support for the Thrustmaster T500RS wheel base in the hid-tmff2 driver. This introduces a new device backend that formats and sends device-specific force-feedback commands over the wheel's USB interrupt endpoint and plugs into the tmff2 core machinery. Highlights: - New t500rs USB backend wired into Kbuild/Makefile and core registration. - Device hooks implement effect lifecycle (play/stop/update), global gain, and autocenter; FF_GAIN/FF_AUTOCENTER updates are routed through the same process-context path as effect updates. - Input device is initialized with the expected capabilities for this base, and packets follow the T500RS protocol. This is the foundational driver support required for the T500RS wheel base. --- Kbuild | 6 +- Makefile | 11 + src/hid-tmff2.c | 111 ++- src/hid-tmff2.h | 14 + src/tmt500rs/hid-tmt500rs-usb.c | 1260 +++++++++++++++++++++++++++++++ tools/build-reload.sh | 172 +++++ udev/99-thrustmaster.rules | 26 + 7 files changed, 1593 insertions(+), 7 deletions(-) create mode 100644 src/tmt500rs/hid-tmt500rs-usb.c create mode 100755 tools/build-reload.sh diff --git a/Kbuild b/Kbuild index 3f4d740..b282e47 100644 --- a/Kbuild +++ b/Kbuild @@ -4,4 +4,8 @@ hid-tmff-new-y := \ src/tmt300rs/hid-tmt300rs.o \ src/tmt248/hid-tmt248.o \ src/tmtx/hid-tmtx.o \ - src/tmtsxw/hid-tmtsxw.o + src/tmtsxw/hid-tmtsxw.o \ + src/tmt500rs/hid-tmt500rs-usb.o + +# Pass through the T500RS version define from Makefile (original branch style) +ccflags-y += $(T500RS_VERSION_DEF) diff --git a/Makefile b/Makefile index ad52621..4226c41 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,17 @@ KDIR ?= /lib/modules/$(shell uname -r)/build +# Auto-generated build-time version for T500RS +T500RS_BASE_VERSION ?= 0.1 +GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null || echo "local") +BUILD_HASH := $(shell date +%s | sha1sum | cut -c1-7) + +T500RS_VERSION ?= $(T500RS_BASE_VERSION)-$(GIT_HASH)+b$(BUILD_HASH) +export T500RS_VERSION_DEF := -DT500RS_DRIVER_VERSION=\"$(T500RS_VERSION)\" + + all: deps/hid-tminit + @echo "T500RS build version: $(T500RS_VERSION)" + @echo " - base: $(T500RS_BASE_VERSION), commit: $(GIT_HASH), build: $(BUILD_HASH)" $(MAKE) -C $(KDIR) M=$(shell pwd) modules install: deps/hid-tminit diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index ee3188a..3aa33de 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -5,6 +5,22 @@ #include #include "hid-tmff2.h" +/* Share t500rs_log_level across compilation units (level 3 = unknown-only) */ +extern int t500rs_log_level; + +/* Known vendor/opcode IDs managed by the driver (TX side) */ +static inline int tmff2_is_known_vendor_id(unsigned char id) +{ + switch (id) { + case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: + case 0x40: case 0x41: case 0x42: case 0x43: + case 0x0a: + return 1; + default: + return 0; + } +} + int open_mode = 1; module_param(open_mode, int, 0660); @@ -242,8 +258,15 @@ static ssize_t gain_store(struct device *dev, } gain = value; - if (tmff2->set_gain) /* if we can, update gain immediately */ - tmff2->set_gain(tmff2->data, (GAIN_MAX * gain) / GAIN_MAX); + if (tmff2->set_gain) { + unsigned long flags; + spin_lock_irqsave(&tmff2->lock, flags); + tmff2->pending_gain_value = GAIN_MAX; + tmff2->gain_pending = 1; + spin_unlock_irqrestore(&tmff2->lock, flags); + if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) + schedule_delayed_work(&tmff2->work, 0); + } return count; } @@ -258,6 +281,7 @@ static DEVICE_ATTR_RW(gain); static void tmff2_set_gain(struct input_dev *dev, uint16_t value) { struct tmff2_device_entry *tmff2 = tmff2_from_input(dev); + unsigned long flags; if (!tmff2) return; @@ -267,13 +291,20 @@ static void tmff2_set_gain(struct input_dev *dev, uint16_t value) return; } - if (tmff2->set_gain(tmff2->data, (value * gain) / GAIN_MAX)) - hid_warn(tmff2->hdev, "unable to set gain\n"); + /* Defer to workqueue: store pending gain and schedule */ + spin_lock_irqsave(&tmff2->lock, flags); + tmff2->pending_gain_value = value; + tmff2->gain_pending = 1; + spin_unlock_irqrestore(&tmff2->lock, flags); + + if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) + schedule_delayed_work(&tmff2->work, 0); } static void tmff2_set_autocenter(struct input_dev *dev, uint16_t value) { struct tmff2_device_entry *tmff2 = tmff2_from_input(dev); + unsigned long flags; if (!tmff2) return; @@ -283,8 +314,14 @@ static void tmff2_set_autocenter(struct input_dev *dev, uint16_t value) return; } - if (tmff2->set_autocenter(tmff2->data, value)) - hid_warn(tmff2->hdev, "unable to set autocenter\n"); + /* Defer to workqueue: store pending autocenter and schedule */ + spin_lock_irqsave(&tmff2->lock, flags); + tmff2->pending_autocenter_value = value; + tmff2->autocenter_pending = 1; + spin_unlock_irqrestore(&tmff2->lock, flags); + + if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) + schedule_delayed_work(&tmff2->work, 0); } static void tmff2_work_handler(struct work_struct *w) @@ -301,6 +338,30 @@ static void tmff2_work_handler(struct work_struct *w) if (!tmff2) return; + /* Apply pending control changes (gain/autocenter) in process context */ + { + unsigned long f2; + uint16_t pg = 0, pac = 0; + int do_gain = 0, do_ac = 0; + spin_lock_irqsave(&tmff2->lock, f2); + if (tmff2->gain_pending) { + pg = tmff2->pending_gain_value; + tmff2->gain_pending = 0; + do_gain = 1; + } + if (tmff2->autocenter_pending) { + pac = tmff2->pending_autocenter_value; + tmff2->autocenter_pending = 0; + do_ac = 1; + } + spin_unlock_irqrestore(&tmff2->lock, f2); + + if (do_gain && tmff2->set_gain) + tmff2->set_gain(tmff2->data, (pg * gain) / GAIN_MAX); + if (do_ac && tmff2->set_autocenter) + tmff2->set_autocenter(tmff2->data, pac); + } + for (effect_id = 0; effect_id < tmff2->max_effects; ++effect_id) { unsigned long actions = 0; struct tmff2_effect_state effect; @@ -694,6 +755,11 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) goto wheel_err; break; + case TMT500RS_PC_ID: + if ((ret = t500rs_populate_api(tmff2))) + goto wheel_err; + break; + case TMT248_PC_ID: if ((ret = t248_populate_api(tmff2))) goto wheel_err; @@ -743,6 +809,7 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) #if LINUX_VERSION_CODE < KERNEL_VERSION(6,12,0) static __u8 *tmff2_report_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *rsize) #else static const __u8 *tmff2_report_fixup(struct hid_device *hdev, __u8 *rdesc, @@ -778,6 +845,7 @@ static void tmff2_remove(struct hid_device *hdev) if (tmff2->params & PARAM_DAMPER_LEVEL) device_remove_file(dev, &dev_attr_damper_level); + if (tmff2->params & PARAM_SPRING_LEVEL) device_remove_file(dev, &dev_attr_spring_level); @@ -802,6 +870,8 @@ static const struct hid_device_id tmff2_devices[] = { {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT300RS_PS3_NORM_ID)}, {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT300RS_PS3_ADV_ID)}, {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT300RS_PS4_NORM_ID)}, + /* t500rs */ + {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT500RS_PC_ID)}, /* t248 PC*/ {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT248_PC_ID)}, /* tx */ @@ -812,6 +882,34 @@ static const struct hid_device_id tmff2_devices[] = { {} }; MODULE_DEVICE_TABLE(hid, tmff2_devices); +static int tmff2_raw_event(struct hid_device *hdev, struct hid_report *report, + __u8 *data, int size) +{ + /* At level 3: ignore normal input reports (axes/buttons) and idless reports; + * only dump vendor/feature-like unknowns. */ + if (t500rs_log_level >= 3 && report && data && size > 0) { + /* Skip input traffic entirely (these are the steering updates you saw). */ + if (report->type == HID_INPUT_REPORT) + return 0; + + /* Use the real Report ID, not data[0] (id==0 means no Report ID). */ + if (report->id == 0) + return 0; + + if (!tmff2_is_known_vendor_id((unsigned char)report->id)) { + char hex[3 * 64 + 4]; + int i, off = 0, max = size > 64 ? 64 : size; + for (i = 0; i < max && off + 3 < sizeof(hex); i++) + off += scnprintf(hex + off, sizeof(hex) - off, "%02x ", data[i]); + if (size > max) + scnprintf(hex + off, sizeof(hex) - off, "..."); + hid_info(hdev, "HID RX UNKNOWN [type=%d id=0x%02x len=%d]: %s\n", + report->type, report->id, size, hex); + } + } + return 0; /* always pass through */ +} + static struct hid_driver tmff2_driver = { .name = "tmff2", @@ -819,6 +917,7 @@ static struct hid_driver tmff2_driver = { .probe = tmff2_probe, .remove = tmff2_remove, .report_fixup = tmff2_report_fixup, + .raw_event = tmff2_raw_event, }; module_hid_driver(tmff2_driver); diff --git a/src/hid-tmff2.h b/src/hid-tmff2.h index f747f44..405ef0c 100644 --- a/src/hid-tmff2.h +++ b/src/hid-tmff2.h @@ -64,6 +64,12 @@ struct tmff2_device_entry { spinlock_t lock; + /* Pending control changes to be applied from workqueue context */ + uint16_t pending_gain_value; + uint16_t pending_autocenter_value; + int gain_pending; + int autocenter_pending; + int allow_scheduling; /* fields relevant to each actual device (T300, T248...) */ @@ -92,6 +98,10 @@ struct tmff2_device_entry { ssize_t (*alt_mode_show)(void *data, char *buf); ssize_t (*alt_mode_store)(void *data, const char *buf, size_t count); int (*set_autocenter)(void *data, uint16_t autocenter); + int (*set_spring_level)(void *data, u8 level); + int (*set_damper_level)(void *data, u8 level); + int (*set_friction_level)(void *data, u8 level); + __u8 *(*wheel_fixup)(struct hid_device *hdev, __u8 *rdesc, unsigned int *rsize); /* void pointers are dangerous, I know, but in this case likely the @@ -100,6 +110,7 @@ struct tmff2_device_entry { /* external */ int t300rs_populate_api(struct tmff2_device_entry *tmff2); +int t500rs_populate_api(struct tmff2_device_entry *tmff2); int t248_populate_api(struct tmff2_device_entry *tmff2); int tx_populate_api(struct tmff2_device_entry *tmff2); int tsxw_populate_api(struct tmff2_device_entry *tmff2); @@ -108,6 +119,9 @@ int tsxw_populate_api(struct tmff2_device_entry *tmff2); #define TMT300RS_PS3_ADV_ID 0xb66f #define TMT300RS_PS4_NORM_ID 0xb66d +#define TMT500RS_INIT_ID 0xb65d +#define TMT500RS_PC_ID 0xb65e + #define TMT248_PC_ID 0xb696 #define TX_ACTIVE 0xb669 diff --git a/src/tmt500rs/hid-tmt500rs-usb.c b/src/tmt500rs/hid-tmt500rs-usb.c new file mode 100644 index 0000000..db7edda --- /dev/null +++ b/src/tmt500rs/hid-tmt500rs-usb.c @@ -0,0 +1,1260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Force feedback support for Thrustmaster T500RS + * + * USB INTERRUPT implementation + * Uses endpoint 0x01 OUT for all communication + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "../hid-tmff2.h" + +/* Build-time injected version (from Makefile/Kbuild) */ +#ifndef T500RS_DRIVER_VERSION +#ifdef T500RS_DRIVER_VERSION_STR +#define __T500_STR_HELPER(x) #x +#define __T500_STR(x) __T500_STR_HELPER(x) +#define T500RS_DRIVER_VERSION __T500_STR(T500RS_DRIVER_VERSION_STR) +#else +#define T500RS_DRIVER_VERSION "0.1-local" +#endif +#endif + +/* T500RS Constants */ +#define T500RS_MAX_EFFECTS 16 +#define T500RS_BUFFER_LENGTH 32 /* USB endpoint max packet size */ +#define T500RS_EP_OUT 0x01 /* INTERRUPT OUT endpoint */ + +/* USB timeout */ +#define T500RS_USB_TIMEOUT 1000 /* 1 second */ + +/* Gain scaling */ +#define GAIN_MAX 65535 + +/* Logging verbosity (0=minimal, 1=verbose) */ +int t500rs_log_level; +module_param(t500rs_log_level, int, 0644); +MODULE_PARM_DESC(t500rs_log_level, + "Log level: 0=minimal, 1=verbose, 2=usb-dump TX, 3=usb-dump " + "UNKNOWN only (TX+RX)"); + +/* Helper: classify whether a TX buffer is a known/managed report + * Known first bytes (report IDs / opcodes) we intentionally emit: + * 0x01,0x02,0x03,0x04,0x05,0x40,0x41,0x42,0x43,0x0a + */ +static inline int t500rs_is_known_tx(const unsigned char *data, size_t len) { + unsigned char r, s; + if (!data || !len) + return 1; + r = data[0]; + s = (len > 1) ? data[1] : 0; + switch (r) { + case 0x01: + return len == 15; /* main effect upload */ + case 0x02: + return len == 9 && s == 0x1c; /* envelope */ + case 0x03: + return len == 4 && s == 0x0e && + ((len > 2) ? data[2] == 0x00 : 0); /* const force level */ + case 0x04: + return (s == 0x0e) && (len == 8 || len == 9); /* periodic/ramp params */ + case 0x05: + return (len == 11) && (s == 0x0e || s == 0x1c) && + ((len > 2) ? data[2] == 0x00 : 0); + case 0x40: + return (len == 4) && (s == 0x03 || s == 0x04 || s == 0x08 || s == 0x11); + case 0x41: + return len == 4; /* play/stop/clear */ + case 0x42: + return ((len == 2) && (s == 0x05 || s == 0x04)) || + ((len == 15) && s == 0x01); + case 0x43: + return len == 2; /* global gain */ + case 0x0a: + return (len == 15) && s == 0x04; /* config */ + default: + return 0; + } +} + +/* Debug logging helper (requires local variable named 't500rs') */ +#define T500RS_DBG(fmt, ...) \ + do { \ + /* Suppress normal debug at level 3 (unknown-only mode) */ \ + if (t500rs_log_level > 0 && t500rs_log_level < 3) \ + hid_info(t500rs->hdev, fmt, ##__VA_ARGS__); \ + } while (0) + +/* T500RS device data */ +struct t500rs_device_entry { + struct hid_device *hdev; + struct input_dev *input_dev; + struct usb_device *usbdev; + struct usb_interface *usbif; + + int ep_out; /* INTERRUPT OUT endpoint address */ + + u8 *send_buffer; + size_t buffer_length; + + /* Per-effect levels (0-100) from base driver's sysfs callbacks */ + u8 spring_gain; + u8 damper_gain; + u8 friction_gain; + u8 inertia_gain; + + /* Current wheel range for smooth transitions */ + u16 current_range; /* Current rotation range in degrees */ +}; + +/* Supported parameters */ +static const unsigned long t500rs_params = + PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | PARAM_FRICTION_LEVEL | + PARAM_GAIN | PARAM_RANGE; + +/* Supported effects */ +static const signed short t500rs_effects[] = { + FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, + FF_PERIODIC, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; + +/* Forward declarations to avoid implicit declarations before worker uses them + */ +static int t500rs_send_usb(struct t500rs_device_entry *t500rs, const u8 *data, + size_t len); +static int t500rs_set_autocenter(void *data, u16 autocenter); +static int t500rs_set_range(void *data, u16 range); +static int t500rs_upload_effect(void *data, + const struct tmff2_effect_state *state); +static int t500rs_update_effect(void *data, + const struct tmff2_effect_state *state); +static int t500rs_play_effect(void *data, + const struct tmff2_effect_state *state); +static int t500rs_stop_effect(void *data, + const struct tmff2_effect_state *state); + +static int t500rs_set_gain(void *data, u16 gain) { + struct t500rs_device_entry *t500rs = data; + u8 *buf; + u8 device_gain_byte; + if (!t500rs) + return -ENODEV; + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + /* Scale 0..65535 to device 0..255 */ + device_gain_byte = (u8)((gain * 255) / GAIN_MAX); + buf[0] = 0x43; + buf[1] = device_gain_byte; + return t500rs_send_usb(t500rs, buf, 2); +} + +/* Send data via USB INTERRUPT transfer (blocking) */ +static int t500rs_send_usb(struct t500rs_device_entry *t500rs, const u8 *data, + size_t len) { + int ret, transferred; + if (!t500rs || !data || len == 0 || len > T500RS_BUFFER_LENGTH) + return -EINVAL; + if (t500rs_log_level > 1) { + char hex[3 * T500RS_BUFFER_LENGTH + 1]; + size_t i, off = 0; + for (i = 0; i < len && off + 3 < sizeof(hex); i++) + off += scnprintf(hex + off, sizeof(hex) - off, "%02x ", data[i]); + if (t500rs_log_level == 2) { + hid_info(t500rs->hdev, "USB TX [%zu]: %s\n", len, hex); + } else if (t500rs_log_level >= 3) { + if (!t500rs_is_known_tx(data, len)) + hid_info(t500rs->hdev, "USB TX UNKNOWN [%zu]: %s\n", len, hex); + } + } + + ret = usb_interrupt_msg(t500rs->usbdev, + usb_sndintpipe(t500rs->usbdev, t500rs->ep_out), + (void *)data, len, &transferred, T500RS_USB_TIMEOUT); + if (ret < 0) + return ret; + return (transferred == len) ? 0 : -EIO; +} + +/* Upload constant force effect */ +static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; /* Use DMA-safe buffer */ + int ret; + int level = effect->u.constant.level; + + /* Note: Gain is applied in play_effect, not here */ + + T500RS_DBG("Upload constant: id=%d, level=%d\n", effect->id, level); + + /* NO DEADZONE - Send all forces exactly as requested, matching Windows + * behavior */ + + /* Report 0x02 - Envelope (attack/fade) */ + memset(buf, 0, 15); + buf[0] = 0x02; + buf[1] = 0x1c; /* Subtype 0x1c */ + buf[2] = 0x00; + T500RS_DBG("Sending Report 0x02 (envelope)...\n"); + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); + return ret; + } + + /* Report 0x01 - Main effect upload - MATCH WINDOWS DRIVER EXACTLY! */ + memset(buf, 0, 15); + buf[0] = 0x01; + buf[1] = 0x00; /* Effect ID 0 (Windows uses 0, not effect->id) */ + buf[2] = 0x00; /* Constant force type */ + buf[3] = 0x40; + buf[4] = 0xff; /* Windows uses 0xff (was 0x69) */ + buf[5] = 0xff; /* Windows uses 0xff (was 0x23) */ + buf[6] = 0x00; + buf[7] = 0xff; + buf[8] = 0xff; + buf[9] = 0x0e; /* Parameter subtype reference */ + buf[10] = 0x00; + buf[11] = 0x1c; /* Envelope subtype reference */ + buf[12] = 0x00; + buf[13] = 0x00; + buf[14] = 0x00; + T500RS_DBG("Sending Report 0x01 (duration/control)...\n"); + ret = t500rs_send_usb(t500rs, buf, 15); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); + return ret; + } + + T500RS_DBG("Constant effect %d uploaded (simple sequence)\n", effect->id); + + /* CRITICAL FIX : Always update the force level when uploading. + * Game calls stop/upload/play in rapid succession, so the timer might be + * stopped when upload is called. We update the force level here so that + * when play_effect starts the timer, it will use the correct force value. + * + * MATCH WINDOWS: Send forces exactly as requested - no amplification! + * Windows sends weak forces (4-27 out of 127) and they work fine. + */ + { + s8 signed_level; + s32 scaled; + + /* Simple linear scaling from -32767..32767 to -127..127 */ + scaled = (level * 127) / 32767; + signed_level = (s8)scaled; + + T500RS_DBG("Upload constant: id=%d, level=%d -> %d (0x%02x)\n", effect->id, + level, signed_level, (u8)signed_level); + } + + return 0; +} + +/* Upload spring/damper/friction effect */ +static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; /* Use DMA-safe buffer */ + u8 effect_type; + int ret; + u8 effect_gain; + int right_strength, left_strength; + + /* Determine effect type and select appropriate gain */ + switch (effect->type) { + case FF_SPRING: + effect_type = 0x40; + effect_gain = t500rs->spring_gain; + break; + case FF_DAMPER: + effect_type = 0x41; + effect_gain = t500rs->damper_gain; + break; + case FF_FRICTION: + effect_type = 0x41; + effect_gain = t500rs->friction_gain; + break; + case FF_INERTIA: + effect_type = 0x41; + effect_gain = t500rs->inertia_gain; + break; + default: + return -EINVAL; + } + + /* Get effect parameters and apply per-effect gain */ + /* Condition effects use right_saturation and left_saturation (0-65535) */ + right_strength = effect->u.condition[0].right_saturation; + left_strength = effect->u.condition[0].left_saturation; + + /* Apply per-effect level scaling (0-100) */ + right_strength = (right_strength * effect_gain) / 100; + left_strength = (left_strength * effect_gain) / 100; + + /* Scale to device range (0-127) */ + right_strength = (right_strength * 127) / 65535; + left_strength = (left_strength * 127) / 65535; + + T500RS_DBG("Upload condition: id=%d, type=0x%02x, gain=%u%%, R=%d, L=%d\n", + effect->id, effect_type, effect_gain, right_strength, + left_strength); + + /* Report 0x05 - Condition parameters (coefficients) */ + memset(buf, 0, 15); + buf[0] = 0x05; + buf[1] = 0x0e; + buf[2] = 0x00; + buf[3] = (u8)right_strength; + buf[4] = (u8)left_strength; + buf[5] = 0x00; + buf[6] = 0x00; + buf[7] = 0x00; + buf[8] = 0x00; + buf[9] = (effect->type == FF_SPRING) ? 0x54 : 0x64; + buf[10] = (effect->type == FF_SPRING) ? 0x54 : 0x64; + ret = t500rs_send_usb(t500rs, buf, 11); + if (ret) + return ret; + + /* Report 0x05 - Condition parameters (deadband/center) */ + memset(buf, 0, 15); + buf[0] = 0x05; + buf[1] = 0x1c; + buf[2] = 0x00; + buf[3] = 0x00; /* Deadband */ + buf[4] = 0x00; /* Center */ + buf[5] = 0x00; + buf[6] = 0x00; + buf[7] = 0x00; + buf[8] = 0x00; + buf[9] = (effect->type == FF_SPRING) ? 0x46 : 0x64; + buf[10] = (effect->type == FF_SPRING) ? 0x54 : 0x64; + ret = t500rs_send_usb(t500rs, buf, 11); + if (ret) + return ret; + + /* Report 0x01 - Main effect upload */ + memset(buf, 0, 15); + buf[0] = 0x01; + buf[1] = effect->id; + buf[2] = effect_type; + buf[3] = 0x40; + buf[4] = 0x17; + buf[5] = 0x25; + buf[6] = 0x00; + buf[7] = 0xff; + buf[8] = 0xff; + buf[9] = 0x0e; + buf[10] = 0x00; + buf[11] = 0x1c; + buf[12] = 0x00; + buf[13] = 0x00; + buf[14] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 15); + if (ret) + return ret; + + return 0; +} + +/* Upload periodic effect (sine, square, triangle, saw) */ +static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; /* Use DMA-safe buffer */ + int ret; + u8 effect_type; + const char *type_name; + int magnitude = effect->u.periodic.magnitude; + u16 period = effect->u.periodic.period; + u8 mag; + + /* Use game-provided magnitude directly; base gain is applied via set_gain() + */ + + /* Determine waveform type */ + switch (effect->u.periodic.waveform) { + case FF_SQUARE: + effect_type = 0x20; + type_name = "square"; + break; + case FF_TRIANGLE: + effect_type = 0x21; + type_name = "triangle"; + break; + case FF_SINE: + effect_type = 0x22; + type_name = "sine"; + break; + case FF_SAW_UP: + effect_type = 0x23; + type_name = "sawtooth_up"; + break; + case FF_SAW_DOWN: + effect_type = 0x24; + type_name = "sawtooth_down"; + break; + default: + hid_err(t500rs->hdev, "Unknown periodic waveform: %d\n", + effect->u.periodic.waveform); + return -EINVAL; + } + + /* Magnitude - scale to 0-127 */ + mag = (abs(magnitude) * 127) / 32767; + + /* Period (frequency) - default to 100ms = 10 Hz if not set */ + if (period == 0) { + period = 100; + } + + T500RS_DBG("Upload %s: id=%d, magnitude=%d (0x%02x), period=%dms\n", + type_name, effect->id, magnitude, mag, period); + + /* Report 0x02 - Envelope */ + memset(buf, 0, 15); + buf[0] = 0x02; + buf[1] = 0x1c; + buf[2] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); + return ret; + } + + /* Report 0x01 - Main effect upload for periodic (set waveform/type) */ + memset(buf, 0, 15); + buf[0] = 0x01; + buf[1] = 0x00; /* Effect ID 0 */ + buf[2] = effect_type; /* Waveform type (0x20..0x24) */ + buf[3] = 0x40; + buf[4] = 0xff; + buf[5] = 0xff; + buf[6] = 0x00; + buf[7] = 0xff; + buf[8] = 0xff; + buf[9] = 0x0e; /* Parameter subtype reference */ + buf[10] = 0x00; + buf[11] = 0x1c; /* Envelope subtype reference */ + buf[12] = 0x00; + buf[13] = 0x00; + buf[14] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 15); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x01 (periodic main): %d\n", + ret); + return ret; + } + + /* Report 0x04 - Periodic parameters */ + memset(buf, 0, 15); + buf[0] = 0x04; + buf[1] = 0x0e; + buf[2] = 0x00; + buf[3] = mag; /* Magnitude */ + buf[4] = 0x00; /* Offset */ + buf[5] = 0x00; /* Phase */ + buf[6] = period & 0xff; /* Period low byte */ + buf[7] = (period >> 8) & 0xff; /* Period high byte */ + ret = t500rs_send_usb(t500rs, buf, 8); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x04: %d\n", ret); + return ret; + } + + /* Report 0x01 - Main effect upload */ + memset(buf, 0, 15); + buf[0] = 0x01; + buf[1] = effect->id; + buf[2] = effect_type; /* Waveform type */ + buf[3] = 0x40; + buf[4] = 0x17; + buf[5] = 0x25; + buf[6] = 0x00; + buf[7] = 0xff; + buf[8] = 0xff; + buf[9] = 0x0e; + buf[10] = 0x00; + buf[11] = 0x1c; + buf[12] = 0x00; + buf[13] = 0x00; + buf[14] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 15); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); + return ret; + } + + T500RS_DBG("%s effect %d uploaded\n", type_name, effect->id); + return 0; +} + +/* Upload ramp effect */ +static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; /* Use DMA-safe buffer */ + int ret; + int start_level = effect->u.ramp.start_level; + int end_level = effect->u.ramp.end_level; + u16 duration_ms = effect->replay.length; + u16 start_scaled; + + /* Scale to 0-255 */ + start_scaled = (abs(start_level) * 0xff) / 32767; + + T500RS_DBG("Upload ramp: id=%d, start=%d, end=%d, duration=%dms\n", + effect->id, start_level, end_level, duration_ms); + + /* Report 0x02 - Envelope */ + memset(buf, 0, 15); + buf[0] = 0x02; + buf[1] = 0x1c; + buf[2] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); + return ret; + } + + /* Report 0x04 - Ramp parameters */ + /* NOTE: T500RS doesn't support native ramp - just holds start level */ + memset(buf, 0, 15); + buf[0] = 0x04; + buf[1] = 0x0e; + buf[2] = start_scaled & 0xff; /* Start level low byte */ + buf[3] = (start_scaled >> 8) & 0xff; /* Start level high byte */ + buf[4] = start_scaled & 0xff; /* Current level (same as start) */ + buf[5] = (start_scaled >> 8) & 0xff; /* Current level high byte */ + buf[6] = duration_ms & 0xff; /* Duration low byte */ + buf[7] = (duration_ms >> 8) & 0xff; /* Duration high byte */ + buf[8] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x04: %d\n", ret); + return ret; + } + + /* Report 0x01 - Main effect upload */ + memset(buf, 0, 15); + buf[0] = 0x01; + buf[1] = effect->id; + buf[2] = 0x24; /* Ramp type (0x24 = sawtooth down / ramp) */ + buf[3] = 0x40; + buf[4] = duration_ms & 0xff; /* Duration low byte */ + buf[5] = (duration_ms >> 8) & 0xff; /* Duration high byte */ + buf[6] = 0x00; + buf[7] = 0xff; + buf[8] = 0xff; + buf[9] = 0x0e; + buf[10] = 0x00; + buf[11] = 0x1c; + buf[12] = 0x00; + buf[13] = 0x00; + buf[14] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 15); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); + return ret; + } + + T500RS_DBG("Ramp effect %d uploaded (simple mode)\n", effect->id); + return 0; +} + +/* Upload effect */ +static int t500rs_upload_effect(void *data, + const struct tmff2_effect_state *state) { + struct t500rs_device_entry *t500rs = data; + const struct ff_effect *effect = &state->effect; + + if (!t500rs) + return -ENODEV; + + switch (effect->type) { + case FF_CONSTANT: + return t500rs_upload_constant(t500rs, state); + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + return t500rs_upload_condition(t500rs, state); + case FF_PERIODIC: + return t500rs_upload_periodic(t500rs, state); + case FF_RAMP: + return t500rs_upload_ramp(t500rs, state); + default: + return -EINVAL; + } +} + +/* Play effect */ +static int t500rs_play_effect(void *data, + const struct tmff2_effect_state *state) { + struct t500rs_device_entry *t500rs = data; + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; /* Use DMA-safe buffer */ + int ret; + + if (!t500rs) + return -ENODEV; + + T500RS_DBG("Play effect: id=%d, type=0x%02x (FF_CONSTANT=0x%02x)\n", + effect->id, effect->type, FF_CONSTANT); + + /* For constant force: send one level update (0x03) then START (0x41) */ + if (effect->type == FF_CONSTANT) { + int level = effect->u.constant.level; + s8 signed_level; + s32 scaled; + + /* Simple linear scaling from -32767..32767 to -127..127 */ + scaled = (level * 127) / 32767; + signed_level = (s8)scaled; + + T500RS_DBG("Constant force: level=%d -> %d (0x%02x)\n", level, signed_level, + (u8)signed_level); + + /* Send Report 0x03 (force level) */ + buf[0] = 0x03; + buf[1] = 0x0e; + buf[2] = 0x00; + buf[3] = (u8)signed_level; + ret = t500rs_send_usb(t500rs, buf, 4); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x03: %d\n", ret); + return ret; + } + + /* Send Report 0x41 START */ + buf[0] = 0x41; + buf[1] = 0x00; /* Effect ID 0 */ + buf[2] = 0x41; /* START */ + buf[3] = 0x01; + return t500rs_send_usb(t500rs, buf, 4); + } + + /* For other effect types, send start command - Report 0x41 + * T500RS expects EffectID=0 for 0x41 commands as well. + */ + buf[0] = 0x41; + buf[1] = 0x00; /* Effect ID 0 to match device expectations */ + buf[2] = 0x41; /* START command */ + buf[3] = 0x01; + + T500RS_DBG("Sending START command (EffectID=0) for effect %d\n", effect->id); + return t500rs_send_usb(t500rs, buf, 4); +} + +/* Stop effect */ +static int t500rs_stop_effect(void *data, + const struct tmff2_effect_state *state) { + struct t500rs_device_entry *t500rs = data; + u8 *buf; + int ret; + + if (!t500rs) { + pr_err("t500rs_stop_effect: t500rs is NULL!\n"); + return -ENODEV; + } + + buf = t500rs->send_buffer; /* Use DMA-safe buffer */ + if (!buf) { + hid_err(t500rs->hdev, "Stop effect: send_buffer is NULL!\n"); + return -ENOMEM; + } + + T500RS_DBG("Stop effect: id=%d, type=%d\n", state->effect.id, + state->effect.type); + + /* For constant force: Windows-style STOP (0x41 00 00 01) */ + if (state->effect.type == FF_CONSTANT) { + buf[0] = 0x41; + buf[1] = 0x00; + buf[2] = 0x00; /* STOP */ + buf[3] = 0x01; + return t500rs_send_usb(t500rs, buf, 4); + } + + /* For other effect types, send stop command - Report 0x41 + * Use EffectID=0 to match device expectations for 0x41. + */ + buf[0] = 0x41; + buf[1] = 0x00; /* Effect ID 0 */ + buf[2] = 0x00; /* STOP command */ + buf[3] = 0x01; + + ret = t500rs_send_usb(t500rs, buf, 4); + T500RS_DBG("Stop effect (non-constant) returned: %d\n", ret); + return ret; +} + +/* Update effect - re-upload and update force level if constant force */ +static int t500rs_update_effect(void *data, + const struct tmff2_effect_state *state) { + struct t500rs_device_entry *t500rs = data; + const struct ff_effect *effect = &state->effect; + + if (!t500rs) + return -ENODEV; + + /* Do NOT re-upload here; Windows keeps the effect and only updates force + * level */ + /* This avoids redundant USB traffic and state churn */ + + /* Update constant force: send single 0x03 with new level */ + if (effect->type == FF_CONSTANT) { + int level = effect->u.constant.level; + s8 signed_level; + s32 scaled; + u8 *buf3; + + buf3 = t500rs->send_buffer; + if (!buf3) + return -ENOMEM; + + scaled = (level * 127) / 32767; + signed_level = (s8)scaled; + + buf3[0] = 0x03; + buf3[1] = 0x0e; + buf3[2] = 0x00; + buf3[3] = (u8)signed_level; + return t500rs_send_usb(t500rs, buf3, 4); + } + + return 0; +} + +/* Set autocenter */ +static int t500rs_set_autocenter(void *data, u16 autocenter) { + struct t500rs_device_entry *t500rs = data; + u8 *buf; + int ret; + u8 autocenter_percent; + + if (!t500rs) + return -ENODEV; + + autocenter_percent = (u8)((autocenter * 100) / 65535); + + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + + if (autocenter == 0) { + /* Disable autocenter: Report 0x40 0x04 0x00 */ + buf[0] = 0x40; + buf[1] = 0x04; + buf[2] = 0x00; /* Disable */ + buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 4); + if (ret) + return ret; + } else { + /* Enable autocenter: Report 0x40 0x04 0x01 */ + buf[0] = 0x40; + buf[1] = 0x04; + buf[2] = 0x01; /* Enable */ + buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 4); + if (ret) + return ret; + + /* Set autocenter strength: Report 0x40 0x03 [value] */ + buf[0] = 0x40; + buf[1] = 0x03; + buf[2] = autocenter_percent; /* 0-100 percentage */ + buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, buf, 4); + if (ret) + return ret; + } + + /* Apply settings: Report 0x42 0x05 */ + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_usb(t500rs, buf, 2); + if (ret) + return ret; + + return 0; +} + +/* Set wheel rotation range */ +static int t500rs_set_range(void *data, u16 range) { + struct t500rs_device_entry *t500rs = data; + u8 *buf; + int ret; + u16 range_value, current_value, target_value; + int step, i, num_steps; + + if (!t500rs) + return -ENODEV; + + /* Clamp range to maximum value only + * Allow testing values below 270° to find hardware minimum */ + if (range > 1080) { + hid_warn(t500rs->hdev, "Range %u too large, clamping to 1080\n", range); + range = 1080; + } + + /* Use DMA-safe preallocated buffer */ + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + + T500RS_DBG("Setting wheel range to %u degrees\n", range); + + /* Based on testing with actual hardware: + * The T500RS uses Report 0x40 0x11 [value_lo] [value_hi] to set rotation + * range + * + * Hardware testing showed: + * - Byte order is LITTLE-ENDIAN (low byte first) + * - Formula: value = range * 60 + * - Smooth transitions prevent hard mechanical ticking + * + * To smooth the transition, we send multiple intermediate values + * when the range change is large. + */ + target_value = range * 60; + current_value = t500rs->current_range * 60; + + /* Calculate number of steps based on the change magnitude + * Larger changes need more steps for smooth transition + * Use many small steps to prevent hard mechanical ticking */ + range_value = (target_value > current_value) ? (target_value - current_value) + : (current_value - target_value); + + /* Use approximately 1 step per 500 units of change, minimum 1, maximum 50 */ + num_steps = range_value / 500; + if (num_steps < 1) + num_steps = 1; + if (num_steps > 50) + num_steps = 50; + + step = (target_value - current_value) / num_steps; + + /* Send gradual range changes */ + for (i = 1; i <= num_steps; i++) { + if (i == num_steps) { + range_value = target_value; /* Ensure we hit exact target */ + } else { + range_value = current_value + (step * i); + } + + /* Send Report 0x40 0x11 [value_lo] [value_hi] to set range + * NOTE: This uses LITTLE-ENDIAN byte order (low byte first)! */ + buf[0] = 0x40; + buf[1] = 0x11; + buf[2] = range_value & 0xFF; /* Low byte first (little-endian) */ + buf[3] = (range_value >> 8) & 0xFF; /* High byte second */ + + ret = t500rs_send_usb(t500rs, buf, 4); + if (ret) { + hid_err(t500rs->hdev, "Failed to send range command: %d\n", ret); + return ret; + } + + T500RS_DBG("Range step %d/%d: value=0x%04x\n", i, num_steps, range_value); + + /* Very small delay between steps for smooth transition + * (only if not the last step) */ + /* Avoid explicit delays; USB stack handles pacing. */ + } + + /* Store current range for next transition */ + t500rs->current_range = range; + + /* Apply settings with Report 0x42 0x05 */ + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_usb(t500rs, buf, 2); + if (ret) { + hid_err(t500rs->hdev, "Failed to apply range settings: %d\n", ret); + return ret; + } + + T500RS_DBG("Range set to %u degrees (final value=0x%04x)\n", range, + target_value); + + return 0; +} + +/* Set spring level (0-100) - called from sysfs */ +static int t500rs_set_spring_level(void *data, u8 level) { + struct t500rs_device_entry *t500rs = data; + + if (!t500rs) + return -ENODEV; + + /* Clamp to valid range */ + if (level > 100) + level = 100; + + /* Update the spring gain setting */ + t500rs->spring_gain = level; + + T500RS_DBG("Spring level set to %u%%\n", level); + + /* Note: The new level will be applied when the next spring effect is + * uploaded/updated */ + return 0; +} + +/* Set damper level (0-100) - called from sysfs */ +static int t500rs_set_damper_level(void *data, u8 level) { + struct t500rs_device_entry *t500rs = data; + + if (!t500rs) + return -ENODEV; + + /* Clamp to valid range */ + if (level > 100) + level = 100; + + /* Update the damper gain setting */ + t500rs->damper_gain = level; + + T500RS_DBG("Damper level set to %u%%\n", level); + + /* Note: The new level will be applied when the next damper effect is + * uploaded/updated */ + return 0; +} + +/* Set friction level (0-100) - called from sysfs */ +static int t500rs_set_friction_level(void *data, u8 level) { + struct t500rs_device_entry *t500rs = data; + + if (!t500rs) + return -ENODEV; + + /* Clamp to valid range */ + if (level > 100) + level = 100; + + /* Update the friction gain setting */ + t500rs->friction_gain = level; + + T500RS_DBG("Friction level set to %u%%\n", level); + + /* Note: The new level will be applied when the next friction effect is + * uploaded/updated */ + return 0; +} + +/* Initialize T500RS device */ +static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { + struct t500rs_device_entry *t500rs; + struct usb_host_endpoint *ep; + u8 *init_buf; /* Will use send_buffer for DMA-safe transfers */ + int ret; + + /* Validate input parameters */ + if (!tmff2 || !tmff2->hdev || !tmff2->input_dev) { + + pr_err("t500rs: Invalid tmff2 structure\n"); + return -EINVAL; + } + + hid_dbg(tmff2->hdev, "T500RS: Initializing USB INTERRUPT mode\n"); + hid_info(tmff2->hdev, "T500RS driver version %s\n", T500RS_DRIVER_VERSION); + + /* Allocate device data */ + t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); + if (!t500rs) { + ret = -ENOMEM; + goto err_alloc; + } + + t500rs->hdev = tmff2->hdev; + t500rs->input_dev = tmff2->input_dev; + + /* Get USB device */ + if (!t500rs->hdev->dev.parent) { + hid_err(t500rs->hdev, "No parent device\n"); + ret = -ENODEV; + goto err_endpoint; + } + + t500rs->usbif = to_usb_interface(t500rs->hdev->dev.parent); + if (!t500rs->usbif) { + hid_err(t500rs->hdev, "Failed to get USB interface\n"); + ret = -ENODEV; + goto err_endpoint; + } + + t500rs->usbdev = interface_to_usbdev(t500rs->usbif); + if (!t500rs->usbdev) { + hid_err(t500rs->hdev, "Failed to get USB device\n"); + ret = -ENODEV; + goto err_endpoint; + } + + /* Find INTERRUPT OUT endpoint (should be endpoint 1) */ + if (t500rs->usbif->cur_altsetting->desc.bNumEndpoints < 2) { + hid_err(t500rs->hdev, "Not enough USB endpoints\n"); + ret = -ENODEV; + goto err_endpoint; + } + + ep = &t500rs->usbif->cur_altsetting->endpoint[1]; + t500rs->ep_out = ep->desc.bEndpointAddress; + + T500RS_DBG("Found INTERRUPT OUT endpoint: 0x%02x\n", t500rs->ep_out); + + /* Allocate send buffer */ + t500rs->buffer_length = T500RS_BUFFER_LENGTH; + t500rs->send_buffer = kzalloc(t500rs->buffer_length, GFP_KERNEL); + if (!t500rs->send_buffer) { + ret = -ENOMEM; + goto err_buffer; + } + + /* Initialize per-effect levels to 100% (0-100 range) */ + t500rs->spring_gain = 100; + t500rs->damper_gain = 100; + t500rs->friction_gain = 100; + t500rs->inertia_gain = 100; + + /* Initialize current range to default (900°) for smooth transitions */ + t500rs->current_range = 900; + + /* Store device data in tmff2 */ + tmff2->data = t500rs; + + /* Use send_buffer for all USB transfers (DMA-safe) */ + init_buf = t500rs->send_buffer; + + T500RS_DBG("Sending initialization sequence...\n"); + + /* Report 0x42 - Init (15 bytes) */ + memset(init_buf, 0, 15); + init_buf[0] = 0x42; + init_buf[1] = 0x01; + ret = t500rs_send_usb(t500rs, init_buf, 15); + if (ret) { + hid_warn(t500rs->hdev, "Init command 1 (0x42) failed: %d\n", ret); + } + + /* Report 0x0a - Config 1 (15 bytes) */ + memset(init_buf, 0, 15); + init_buf[0] = 0x0a; + init_buf[1] = 0x04; + init_buf[2] = 0x90; + init_buf[3] = 0x03; + ret = t500rs_send_usb(t500rs, init_buf, 15); + if (ret) { + hid_warn(t500rs->hdev, "Init command 2 (0x0a config1) failed: %d\n", ret); + } + + /* Report 0x0a - Config 2 (15 bytes) */ + memset(init_buf, 0, 15); + init_buf[0] = 0x0a; + init_buf[1] = 0x04; + init_buf[2] = 0x12; + init_buf[3] = 0x10; + ret = t500rs_send_usb(t500rs, init_buf, 15); + if (ret) { + hid_warn(t500rs->hdev, "Init command 3 (0x0a config2) failed: %d\n", ret); + } + + /* Report 0x0a - Config 3 (15 bytes) */ + memset(init_buf, 0, 15); + init_buf[0] = 0x0a; + init_buf[1] = 0x04; + init_buf[2] = 0x00; + init_buf[3] = 0x06; + ret = t500rs_send_usb(t500rs, init_buf, 15); + if (ret) { + hid_warn(t500rs->hdev, "Init command 4 (0x0a config3) failed: %d\n", ret); + } + + /* Report 0x40 - Enable FFB (4 bytes) */ + /* CRITICAL FIX: Use Windows parameters 42 7b instead of 55 d5 */ + memset(init_buf, 0, 4); + init_buf[0] = 0x40; + init_buf[1] = 0x11; + init_buf[2] = 0x42; /* Changed from 0x55 to match Windows! */ + init_buf[3] = 0x7b; /* Changed from 0xd5 to match Windows! */ + ret = t500rs_send_usb(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 5 (0x40 enable) failed: %d\n", ret); + } + + /* Report 0x42 - Additional init (2 bytes) */ + memset(init_buf, 0, 4); + init_buf[0] = 0x42; + init_buf[1] = 0x04; + ret = t500rs_send_usb(t500rs, init_buf, 2); + if (ret) { + hid_warn(t500rs->hdev, "Init command 6 (0x42) failed: %d\n", ret); + } + + /* Report 0x40 - Config (4 bytes) */ + memset(init_buf, 0, 4); + init_buf[0] = 0x40; + init_buf[1] = 0x04; + init_buf[2] = 0x00; + init_buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 7 (0x40 config) failed: %d\n", ret); + } + + /* Report 0x43 - Set global gain (2 bytes) */ + /* CRITICAL FIX: Set gain to maximum (0xFF = 100%), not 0x00! */ + memset(init_buf, 0, 4); + init_buf[0] = 0x43; + init_buf[1] = 0xFF; /* Maximum gain - was 0x00 which DISABLED all forces! */ + ret = t500rs_send_usb(t500rs, init_buf, 2); + if (ret) { + hid_warn(t500rs->hdev, "Init command 8 (0x43) failed: %d\n", ret); + } + + /* Report 0x41 - Clear effects (4 bytes) */ + memset(init_buf, 0, 4); + init_buf[0] = 0x41; + init_buf[1] = 0x00; + init_buf[2] = 0x00; + init_buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 9 (0x41 clear) failed: %d\n", ret); + } + + /* Report 0x40 - Final setup (4 bytes) */ + memset(init_buf, 0, 4); + init_buf[0] = 0x40; + init_buf[1] = 0x08; + init_buf[2] = 0x00; + init_buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 10 (0x40 final) failed: %d\n", ret); + } + + /* Report 0x40 - Set mode (4 bytes) */ + memset(init_buf, 0, 4); + init_buf[0] = 0x40; + init_buf[1] = 0x03; + init_buf[2] = 0x0d; + init_buf[3] = 0x00; + ret = t500rs_send_usb(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 11 (0x40 mode) failed: %d\n", ret); + } + + /* Disable autocenter spring properly */ + /* Report 0x05 - Set spring coefficients to 0 */ + memset(init_buf, 0, 15); + init_buf[0] = 0x05; + init_buf[1] = 0x0e; + init_buf[2] = 0x00; + init_buf[3] = 0x00; /* Right coefficient = 0 */ + init_buf[4] = 0x00; /* Left coefficient = 0 */ + init_buf[9] = 0x00; /* Right saturation = 0 */ + init_buf[10] = 0x00; /* Left saturation = 0 */ + ret = t500rs_send_usb(t500rs, init_buf, 11); + if (ret) { + hid_warn(t500rs->hdev, "Disable autocenter (0x05 0x0e) failed: %d\n", ret); + } + + /* Report 0x05 - Set deadband and center */ + memset(init_buf, 0, 15); + init_buf[0] = 0x05; + init_buf[1] = 0x1c; + init_buf[2] = 0x00; + init_buf[3] = 0x00; /* Deadband = 0 */ + init_buf[4] = 0x00; /* Center = 0 */ + init_buf[9] = 0x00; /* Right saturation = 0 */ + init_buf[10] = 0x00; /* Left saturation = 0 */ + ret = t500rs_send_usb(t500rs, init_buf, 11); + if (ret) { + hid_warn(t500rs->hdev, "Disable autocenter (0x05 0x1c) failed: %d\n", ret); + } + + /* Stop autocenter effect (effect ID 15) */ + memset(init_buf, 0, 4); + init_buf[0] = 0x41; + init_buf[1] = 15; /* Autocenter effect ID */ + init_buf[2] = 0x00; /* STOP */ + init_buf[3] = 0x01; + ret = t500rs_send_usb(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Stop autocenter effect failed: %d\n", ret); + } else { + T500RS_DBG("Autocenter fully disabled\n"); + } + + hid_info(t500rs->hdev, + "T500RS initialized successfully (USB INTERRUPT mode)\n"); + T500RS_DBG("Endpoint: 0x%02x, Buffer: %zu bytes\n", t500rs->ep_out, + t500rs->buffer_length); + + return 0; + +err_buffer: + kfree(t500rs->send_buffer); +err_endpoint: + kfree(t500rs); +err_alloc: + return ret; +} + +/* Cleanup T500RS device */ +static int t500rs_wheel_destroy(void *data) { + struct t500rs_device_entry *t500rs = data; + + if (!t500rs) + return 0; + + T500RS_DBG("T500RS: Cleaning up\n"); + + /* Free resources */ + kfree(t500rs->send_buffer); + kfree(t500rs); + + return 0; +} + +/* Populate API callbacks */ +int t500rs_populate_api(struct tmff2_device_entry *tmff2) { + int i; + + tmff2->play_effect = t500rs_play_effect; + tmff2->upload_effect = t500rs_upload_effect; + tmff2->update_effect = t500rs_update_effect; + tmff2->stop_effect = t500rs_stop_effect; + + tmff2->set_gain = t500rs_set_gain; + tmff2->set_autocenter = t500rs_set_autocenter; + tmff2->set_range = t500rs_set_range; + tmff2->set_spring_level = t500rs_set_spring_level; + tmff2->set_damper_level = t500rs_set_damper_level; + tmff2->set_friction_level = t500rs_set_friction_level; + + tmff2->wheel_init = t500rs_wheel_init; + tmff2->wheel_destroy = t500rs_wheel_destroy; + + tmff2->params = t500rs_params; + tmff2->max_effects = T500RS_MAX_EFFECTS; + + /* Copy supported effects array */ + for (i = 0; t500rs_effects[i] != -1 && i < FF_CNT; i++) + tmff2->supported_effects[i] = t500rs_effects[i]; + tmff2->supported_effects[i] = -1; + + return 0; +} diff --git a/tools/build-reload.sh b/tools/build-reload.sh new file mode 100755 index 0000000..bf2bea8 --- /dev/null +++ b/tools/build-reload.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# Build, reload, and verify T500RS driver modules +# Usage: sudo ./build_reload_verify.sh [insmod params] + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}T500RS Driver Build, Reload, and Verify Script${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}ERROR: Please run as root (use sudo)${NC}" + exit 1 +fi + +# Resolve repo root and local .ko path +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +REPO_ROOT="$(cd "$SCRIPT_DIR"/.. && pwd)" +LOCAL_KO="$REPO_ROOT/hid-tmff-new.ko" + +# Step 1: Build +echo -e "${YELLOW}[1/8] Building driver...${NC}" +if make -C "$REPO_ROOT" -j4; then + echo -e "${GREEN} ✓ Build succeeded${NC}" +else + echo -e "${RED} ✗ Build failed — aborting${NC}" + exit 1 +fi + +if [ ! -f "$LOCAL_KO" ]; then + echo -e "${RED} ✗ Module not found after build: $LOCAL_KO${NC}" + exit 1 +fi + +# Step 2: Clean up T500RS device and sysfs files +echo -e "${YELLOW}[2/8] Cleaning up T500RS device...${NC}" +# Find T500RS HID device in sysfs +T500RS_DEVICE=$(find /sys/bus/hid/devices/ -name "*044F:B65E*" 2>/dev/null | head -1) +if [ -n "$T500RS_DEVICE" ]; then + DEVICE_NAME=$(basename "$T500RS_DEVICE") + echo " - Found HID device: $DEVICE_NAME" + # Manually remove leftover sysfs files (from failed probe) + echo " - Removing leftover sysfs files..." + for attr in gain range spring_level damper_level friction_level alternate_modes; do + if [ -e "$T500RS_DEVICE/$attr" ]; then + rm -f "$T500RS_DEVICE/$attr" 2>/dev/null || true + fi + done + # Unbind from current driver if bound + if [ -e "$T500RS_DEVICE/driver" ]; then + CURRENT_DRIVER=$(basename $(readlink "$T500RS_DEVICE/driver")) + echo " - Unbinding from $CURRENT_DRIVER..." + echo "$DEVICE_NAME" > "$T500RS_DEVICE/driver/unbind" 2>/dev/null || true + sleep 0.5 + fi + echo -e "${GREEN} ✓ Device cleaned up${NC}" +else + echo " - T500RS HID device not found" +fi +echo "" + +# Step 3: Unload modules +echo -e "${YELLOW}[3/8] Unloading modules...${NC}" +# Try to unload in reverse dependency order +if lsmod | grep -q "hid_tmff_new"; then + echo " - Removing hid_tmff_new..." + modprobe -r hid_tmff_new || { + echo -e "${RED} WARNING: Failed to remove hid_tmff_new (may be in use)${NC}" + echo " Trying to force removal..." + rmmod -f hid_tmff_new 2>/dev/null || true + } +else + echo " - hid_tmff_new not loaded" +fi + +if lsmod | grep -q "usb_tminit_new"; then + echo " - Removing usb_tminit_new..." + modprobe -r usb_tminit_new || true +else + echo " - usb_tminit_new not loaded" +fi + +if lsmod | grep -q "hid_tminit_new"; then + echo " - Removing hid_tminit_new..." + modprobe -r hid_tminit_new || true +else + echo " - hid_tminit_new not loaded" +fi + +echo -e "${GREEN} ✓ Modules unloaded${NC}" +echo "" + +# Step 4: Wait a moment for cleanup +echo -e "${YELLOW}[4/8] Waiting for cleanup...${NC}" +sleep 2 +echo -e "${GREEN} ✓ Done${NC}" +echo "" + +# Step 5: Load init modules +echo -e "${YELLOW}[5/8] Loading init modules...${NC}" +modprobe hid_tminit_new && echo -e "${GREEN} ✓ hid_tminit_new loaded${NC}" || echo -e "${RED} ✗ Failed to load hid_tminit_new${NC}" +modprobe usb_tminit_new && echo -e "${GREEN} ✓ usb_tminit_new loaded${NC}" || echo -e "${RED} ✗ Failed to load usb_tminit_new${NC}" +echo "" + +# Step 6: Wait for init to complete +echo -e "${YELLOW}[6/8] Waiting for device initialization...${NC}" +sleep 3 +echo -e "${GREEN} ✓ Done${NC}" +echo "" + +# Step 7: Load main driver (from local build directory) +echo -e "${YELLOW}[7/8] Loading main driver from local build...${NC}" +if [ -f "$LOCAL_KO" ]; then + echo " - Loading $LOCAL_KO with params: $*" + insmod "$LOCAL_KO" "$@" && echo -e "${GREEN} ✓ hid_tmff_new loaded from local build${NC}" || { + echo -e "${RED} ✗ Failed to load hid_tmff_new${NC}" + exit 1 + } +else + echo -e "${RED} ✗ Module not found: $LOCAL_KO${NC}" + echo " Run 'make' first to build the module" + exit 1 +fi +echo "" + +# Step 8: Verify loaded module matches local build +echo -e "${YELLOW}[8/8] Verifying loaded module matches local build...${NC}" +LOCAL_SRCVER=$(modinfo -F srcversion "$LOCAL_KO" 2>/dev/null || echo "unknown") +LOADED_SRCVER=$(cat /sys/module/hid_tmff_new/srcversion 2>/dev/null || echo "none") +INSTALLED_PATH=$(modinfo -n hid_tmff_new 2>/dev/null || echo "unknown") + +printf " - Local .ko srcversion: %s\n" "$LOCAL_SRCVER" +printf " - Loaded module srcversion:%s\n" "$LOADED_SRCVER" +printf " - Installed candidate: %s\n" "$INSTALLED_PATH" + +if [ "$LOCAL_SRCVER" != "$LOADED_SRCVER" ]; then + echo -e "${RED} ✗ Mismatch: loaded module is not the local build${NC}" + exit 1 +else + echo -e "${GREEN} ✓ Loaded module matches local build${NC}" +fi + +echo "" + +# Show T500RS logs +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE}T500RS Device Status:${NC}" +echo -e "${BLUE}========================================${NC}" +if dmesg | grep -E "T500RS driver version|T500RS initialized successfully" | tail -n 10; then + : +fi + +# Check for errors +if dmesg | tail -50 | grep -qi "error\|fail\|bug\|oops"; then + echo -e "${RED}⚠ WARNING: Errors detected in kernel log!${NC}" + dmesg | tail -50 | grep -i "error\|fail\|bug\|oops" | tail -10 + echo "" +fi + +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN}Build, reload, and verification complete!${NC}" +echo -e "${GREEN}========================================${NC}" + diff --git a/udev/99-thrustmaster.rules b/udev/99-thrustmaster.rules index e96cb02..c37b410 100644 --- a/udev/99-thrustmaster.rules +++ b/udev/99-thrustmaster.rules @@ -27,3 +27,29 @@ SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b692", RUN+="/us # TSPC SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b689", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b689", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" + +# T500RS initialization mode +KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" +KERNEL=="event*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" +KERNEL=="js*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" +SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" + +# T500RS PC mode +# IMPORTANT FOR PROTON/STEAM GAMES: +# If you have multiple joystick devices (js0, js1, etc.) that cause conflicts, +# use the event device directly in Steam launch options: +# SDL_JOYSTICK_DEVICE=/dev/input/eventXX %command% +# Find your event number with: cat /proc/bus/input/devices | grep -A 5 "Thrustmaster" +KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" +KERNEL=="event*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" +KERNEL=="js*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" +SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" +SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODALIAS=="hid:b0003g*v0000044Fp0000B65E", RUN+="/bin/sh -c 'echo %k > /sys/bus/hid/drivers/tmff2/bind'" +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" + +# T500RS sysfs attributes permissions (for GUI and non-root access) +# Set permissions on all sysfs attributes after device is bound to driver +SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/bin/sh -c 'sleep 0.5; chmod 0666 /sys/bus/hid/devices/%k/gain /sys/bus/hid/devices/%k/range /sys/bus/hid/devices/%k/autocenter /sys/bus/hid/devices/%k/t500rs_* 2>/dev/null || true'" \ No newline at end of file From dcb3704ba0213cadc6b10fa9644fcaa1d894e3ac Mon Sep 17 00:00:00 2001 From: caz Date: Wed, 12 Nov 2025 01:05:36 +0100 Subject: [PATCH 2/4] Merge candidate/pr175 feedbacks (#9) --- Kbuild | 4 +- Makefile | 12 +- docs/FFBEFFECTS.md | 2 +- docs/FFB_T500RS.md | 85 ++++ src/hid-tmff2.c | 54 +-- src/hid-tmff2.h | 4 - src/tmt500rs/hid-tmt500rs-usb.c | 629 ++++++++++++++------------- tools/build-reload.sh | 172 -------- udev/71-thrustmaster-steamdeck.rules | 4 + udev/99-thrustmaster.rules | 26 -- 10 files changed, 451 insertions(+), 541 deletions(-) create mode 100644 docs/FFB_T500RS.md delete mode 100755 tools/build-reload.sh diff --git a/Kbuild b/Kbuild index b282e47..c3f8eca 100644 --- a/Kbuild +++ b/Kbuild @@ -7,5 +7,5 @@ hid-tmff-new-y := \ src/tmtsxw/hid-tmtsxw.o \ src/tmt500rs/hid-tmt500rs-usb.o -# Pass through the T500RS version define from Makefile (original branch style) -ccflags-y += $(T500RS_VERSION_DEF) +# Pass through the global TMFF2 version define from Makefile +ccflags-y += $(TMFF2_VERSION_DEF) diff --git a/Makefile b/Makefile index 4226c41..5a107d6 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,17 @@ KDIR ?= /lib/modules/$(shell uname -r)/build -# Auto-generated build-time version for T500RS -T500RS_BASE_VERSION ?= 0.1 +# Auto-generated global build-time version for TMFF2 +TMFF2_BASE_VERSION ?= 0.1 GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null || echo "local") BUILD_HASH := $(shell date +%s | sha1sum | cut -c1-7) -T500RS_VERSION ?= $(T500RS_BASE_VERSION)-$(GIT_HASH)+b$(BUILD_HASH) -export T500RS_VERSION_DEF := -DT500RS_DRIVER_VERSION=\"$(T500RS_VERSION)\" +TMFF2_VERSION ?= $(TMFF2_BASE_VERSION)-$(GIT_HASH)+b$(BUILD_HASH) +export TMFF2_VERSION_DEF := -DTMFF2_DRIVER_VERSION=\"$(TMFF2_VERSION)\" all: deps/hid-tminit - @echo "T500RS build version: $(T500RS_VERSION)" - @echo " - base: $(T500RS_BASE_VERSION), commit: $(GIT_HASH), build: $(BUILD_HASH)" + @echo "TMFF2 build version: $(TMFF2_VERSION)" + @echo " - base: $(TMFF2_BASE_VERSION), commit: $(GIT_HASH), build: $(BUILD_HASH)" $(MAKE) -C $(KDIR) M=$(shell pwd) modules install: deps/hid-tminit diff --git a/docs/FFBEFFECTS.md b/docs/FFBEFFECTS.md index a9a313a..a9a5de5 100644 --- a/docs/FFBEFFECTS.md +++ b/docs/FFBEFFECTS.md @@ -1025,4 +1025,4 @@ binary hex ff00.0021 = 0 ff00.0021 = 0 ff00.0021 = 0 -``` +``` \ No newline at end of file diff --git a/docs/FFB_T500RS.md b/docs/FFB_T500RS.md new file mode 100644 index 0000000..f7e73ac --- /dev/null +++ b/docs/FFB_T500RS.md @@ -0,0 +1,85 @@ +## T500RS USB FFB Protocol (observed) + +This page documents the packet formats and ordering used by the T500RS when driven via the USB interrupt endpoint (0x01 OUT). It mirrors the style of docs/FFBEFFECTS.md for other wheels. + +Key rules +- Little‑endian for 16‑bit fields +- DMA‑safe buffer length 32 bytes; typical messages are 2, 4, 9 or 15 bytes +- EffectID semantics (CRITICAL): + - All Report 0x01 main‑effect uploads MUST use EffectID = 0x00 on T500RS + - Report 0x41 START/STOP also uses EffectID = 0x00, except the init‑time STOP for autocenter which targets a fixed ID (15) + - Using per‑effect IDs with 0x01 breaks playback (constant force fails completely) + +Report glossary +- 0x01 main upload (15 bytes) + - Layout (unknown bytes kept for completeness): + - b0 id = 0x01 + - b1 effect_id = 0x00 (MUST) + - b2 type: 0x00 constant; 0x20..0x24 periodic/ramp + - b3 0x40 + - b4 0xff (or duration L for ramp path) + - b5 0xff (or duration H for ramp path) + - b6 0x00 + - b7 0xff + - b8 0xff + - b9 0x0e (parameter subtype) + - b10 0x00 + - b11 0x1c (envelope subtype) + - b12 0x00 + - b13 0x00 + - b14 0x00 +- 0x02 envelope (9 bytes) + - b0 0x02, b1 0x1c, b2 0x00 + - b3..4 attack_length (le16) + - b5 attack_level (0..255) + - b6..7 fade_length (le16) + - b8 fade_level (0..255) +- 0x03 constant level (4 bytes) + - b0 0x03, b1 0x0e, b2 0x00 + - b3 level s8 (−127..127) +- 0x04 periodic params (8 bytes) + - b0 0x04, b1 0x0e, b2 0x00 + - b3 magnitude u7 (0..127) + - b4 offset = 0, b5 phase = 0 + - b6..7 period (le16, ms) +- 0x04 ramp params (9 bytes) + - b0 0x04, b1 0x0e + - b2..3 start (le16), b4..5 cur_val (le16), b6..7 duration (le16), b8 0x00 +- 0x05 condition params (11 bytes) + - set 1 (coeff/saturation): b0 0x05, b1 0x0e, b2 0x00, b3 right_strength, b4 left_strength, b9/b10 subtype values vary by type (spring vs damper/friction) + - set 2 (deadband/center): b0 0x05, b1 0x1c, b2 0x00, b3 deadband, b4 center, b9/b10 subtype values +- 0x40 config (4 bytes) + - 0x40 0x04 enable/disable autocenter, 0x40 0x03 set autocenter strength, 0x40 0x11 set range (le16), etc. +- 0x41 start/stop/clear (4 bytes) + - id=0x41, effect_id=0x00 (MUST), command=0x41 START or 0x00 STOP, arg=0x01 + - Exception: init‑time STOP for autocenter uses effect_id=15 +- 0x42 apply/apply‑like, 0x43 gain + +Effect upload and play ordering +- Constant (FF_CONSTANT) + 1) 0x02 envelope + 2) 0x01 main (type=0x00, effect_id=0x00) + Play: 0x03 level, then 0x41 START (effect_id=0x00) +- Periodic (FF_SINE/FF_SQUARE/FF_TRIANGLE/FF_SAW) + 1) 0x02 envelope + 2) 0x01 main (type=0x20..0x24, effect_id=0x00) + 3) 0x04 periodic params (magnitude/period) + 4) 0x01 main (type repeated, effect_id=0x00) + Play: 0x41 START (effect_id=0x00) +- Condition (FF_SPRING/FF_DAMPER/FF_FRICTION/FF_INERTIA) + 1) 0x05 coeff/saturation (0x0e) + 2) 0x05 deadband/center (0x1c) + 3) 0x01 main (type reflects subkind, effect_id=0x00) + Play: 0x41 START (effect_id=0x00) +- Ramp (FF_RAMP) + 1) 0x02 envelope + 2) 0x04 ramp params (device behaves like hold of start level with duration) + 3) 0x01 main (type=0x24, effect_id=0x00) + Play: 0x41 START (effect_id=0x00) + +Notes +- All 0x01 uploads must use EffectID=0x00. This was validated on hardware; using per‑effect IDs causes constant force to fail completely and can break other effects. +- 16‑bit values are little‑endian (lo byte first). +- Magnitudes/levels are expected in device ranges (0..127 for periodic magnitude, s8 for constant level). Scaling helpers in code perform conversions from Linux ranges. +- Autocenter is disabled by zeroing spring coefficients via 0x05 messages and/or via 0x40 commands, then explicitly stopping autocenter (ID 15) during init. + diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index 3aa33de..caf0d6f 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -5,8 +5,6 @@ #include #include "hid-tmff2.h" -/* Share t500rs_log_level across compilation units (level 3 = unknown-only) */ -extern int t500rs_log_level; /* Known vendor/opcode IDs managed by the driver (TX side) */ static inline int tmff2_is_known_vendor_id(unsigned char id) @@ -376,15 +374,26 @@ static void tmff2_work_handler(struct work_struct *w) effect_delay = state->effect.replay.delay; effect_length = state->effect.replay.length; + /* If playing with a finite length, stop when (delay + length) elapses */ if (test_bit(FF_EFFECT_PLAYING, &state->flags) && effect_length) { - if ((time_now - state->start_time) >= + if ((time_now - state->start_time) >= (effect_delay + effect_length) * state->count) { __clear_bit(FF_EFFECT_PLAYING, &state->flags); __clear_bit(FF_EFFECT_QUEUE_UPDATE, &state->flags); - + /* Request a STOP in process context */ + __set_bit(FF_EFFECT_QUEUE_STOP, &actions); state->count = 0; } } + /* Delay handling for start: only trigger START after replay.delay */ + if (test_bit(FF_EFFECT_QUEUE_START, &state->flags)) { + if ((time_now - state->start_time) >= effect_delay) { + __set_bit(FF_EFFECT_QUEUE_START, &actions); + __clear_bit(FF_EFFECT_QUEUE_START, &state->flags); + /* effect is playing since we're starting it now */ + __set_bit(FF_EFFECT_PLAYING, &state->flags); + } /* else: keep START pending until delay elapsed */ + } if (test_bit(FF_EFFECT_QUEUE_UPLOAD, &state->flags)) { __set_bit(FF_EFFECT_QUEUE_UPLOAD, &actions); @@ -809,7 +818,6 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) #if LINUX_VERSION_CODE < KERNEL_VERSION(6,12,0) static __u8 *tmff2_report_fixup(struct hid_device *hdev, __u8 *rdesc, - unsigned int *rsize) #else static const __u8 *tmff2_report_fixup(struct hid_device *hdev, __u8 *rdesc, @@ -881,35 +889,6 @@ static const struct hid_device_id tmff2_devices[] = { {} }; -MODULE_DEVICE_TABLE(hid, tmff2_devices); -static int tmff2_raw_event(struct hid_device *hdev, struct hid_report *report, - __u8 *data, int size) -{ - /* At level 3: ignore normal input reports (axes/buttons) and idless reports; - * only dump vendor/feature-like unknowns. */ - if (t500rs_log_level >= 3 && report && data && size > 0) { - /* Skip input traffic entirely (these are the steering updates you saw). */ - if (report->type == HID_INPUT_REPORT) - return 0; - - /* Use the real Report ID, not data[0] (id==0 means no Report ID). */ - if (report->id == 0) - return 0; - - if (!tmff2_is_known_vendor_id((unsigned char)report->id)) { - char hex[3 * 64 + 4]; - int i, off = 0, max = size > 64 ? 64 : size; - for (i = 0; i < max && off + 3 < sizeof(hex); i++) - off += scnprintf(hex + off, sizeof(hex) - off, "%02x ", data[i]); - if (size > max) - scnprintf(hex + off, sizeof(hex) - off, "..."); - hid_info(hdev, "HID RX UNKNOWN [type=%d id=0x%02x len=%d]: %s\n", - report->type, report->id, size, hex); - } - } - return 0; /* always pass through */ -} - static struct hid_driver tmff2_driver = { .name = "tmff2", @@ -917,8 +896,13 @@ static struct hid_driver tmff2_driver = { .probe = tmff2_probe, .remove = tmff2_remove, .report_fixup = tmff2_report_fixup, - .raw_event = tmff2_raw_event, }; module_hid_driver(tmff2_driver); + +#ifndef TMFF2_DRIVER_VERSION +#define TMFF2_DRIVER_VERSION "dev" +#endif +MODULE_VERSION(TMFF2_DRIVER_VERSION); + MODULE_LICENSE("GPL"); diff --git a/src/hid-tmff2.h b/src/hid-tmff2.h index 405ef0c..1d95e38 100644 --- a/src/hid-tmff2.h +++ b/src/hid-tmff2.h @@ -98,9 +98,6 @@ struct tmff2_device_entry { ssize_t (*alt_mode_show)(void *data, char *buf); ssize_t (*alt_mode_store)(void *data, const char *buf, size_t count); int (*set_autocenter)(void *data, uint16_t autocenter); - int (*set_spring_level)(void *data, u8 level); - int (*set_damper_level)(void *data, u8 level); - int (*set_friction_level)(void *data, u8 level); __u8 *(*wheel_fixup)(struct hid_device *hdev, __u8 *rdesc, unsigned int *rsize); @@ -119,7 +116,6 @@ int tsxw_populate_api(struct tmff2_device_entry *tmff2); #define TMT300RS_PS3_ADV_ID 0xb66f #define TMT300RS_PS4_NORM_ID 0xb66d -#define TMT500RS_INIT_ID 0xb65d #define TMT500RS_PC_ID 0xb65e #define TMT248_PC_ID 0xb696 diff --git a/src/tmt500rs/hid-tmt500rs-usb.c b/src/tmt500rs/hid-tmt500rs-usb.c index db7edda..fd4dbf9 100644 --- a/src/tmt500rs/hid-tmt500rs-usb.c +++ b/src/tmt500rs/hid-tmt500rs-usb.c @@ -17,16 +17,6 @@ #include "../hid-tmff2.h" -/* Build-time injected version (from Makefile/Kbuild) */ -#ifndef T500RS_DRIVER_VERSION -#ifdef T500RS_DRIVER_VERSION_STR -#define __T500_STR_HELPER(x) #x -#define __T500_STR(x) __T500_STR_HELPER(x) -#define T500RS_DRIVER_VERSION __T500_STR(T500RS_DRIVER_VERSION_STR) -#else -#define T500RS_DRIVER_VERSION "0.1-local" -#endif -#endif /* T500RS Constants */ #define T500RS_MAX_EFFECTS 16 @@ -39,12 +29,70 @@ /* Gain scaling */ #define GAIN_MAX 65535 -/* Logging verbosity (0=minimal, 1=verbose) */ -int t500rs_log_level; -module_param(t500rs_log_level, int, 0644); -MODULE_PARM_DESC(t500rs_log_level, - "Log level: 0=minimal, 1=verbose, 2=usb-dump TX, 3=usb-dump " - "UNKNOWN only (TX+RX)"); + +/* Packet structs (packed) to reduce magic bytes on T500RS protocol */ +struct t500rs_r02_envelope { + u8 id; /* 0x02 */ + u8 subtype; /* 0x1c */ + u8 zero; /* 0x00 */ + __le16 attack_len; + u8 attack_lvl; /* 0..255 */ + __le16 fade_len; + u8 fade_lvl; /* 0..255 */ +} __packed; + +struct t500rs_r03_const { + u8 id; /* 0x03 */ + u8 code; /* 0x0e */ + u8 zero; /* 0x00 */ + s8 level; /* -127..127 */ +} __packed; + +struct t500rs_r04_periodic { + u8 id; /* 0x04 */ + u8 code; /* 0x0e */ + u8 zero; /* 0x00 */ + u8 magnitude; /* 0..127 */ + u8 offset; /* 0 */ + u8 phase; /* 0 */ + __le16 period; /* ms */ +} __packed; + +struct t500rs_r04_ramp { + u8 id; /* 0x04 */ + u8 code; /* 0x0e */ + __le16 start; + __le16 cur_val; /* same as start */ + __le16 duration; /* ms */ + u8 zero; /* 0 */ +} __packed; + +struct t500rs_r41_cmd { + u8 id; /* 0x41 */ + u8 effect_id; /* usually 0 on T500RS */ + u8 command; /* 0x41 START, 0x00 STOP, 0x00 clear in init */ + u8 arg; /* 0x01 */ +} __packed; + +/* Generic 0x01 main upload (15 bytes) — keep fields generic while unknown) */ +struct t500rs_r01_main { + u8 id; /* 0x01 */ + u8 effect_id; /* see ID semantics note */ + u8 type; /* 0x00 constant, 0x20..0x24 periodic/ramp */ + u8 b3; + u8 b4; + u8 b5; + u8 b6; + u8 b7; + u8 b8; + u8 b9; + u8 b10; + u8 b11; + u8 b12; + u8 b13; + u8 b14; +} __packed; + /* Helper: classify whether a TX buffer is a known/managed report * Known first bytes (report IDs / opcodes) we intentionally emit: @@ -85,13 +133,56 @@ static inline int t500rs_is_known_tx(const unsigned char *data, size_t len) { } } +/* Scale envelope level (0..32767) to device 8-bit (0..255) */ +static inline u8 t500rs_scale_env_level(u16 level) +{ + if (level > 32767) + level = 32767; + return (u8)((level * 255) / 32767); +} + +/* Scale constant level (-32767..32767) to signed 8-bit (-127..127) */ +static inline s8 t500rs_scale_const_level_s8(int level) +{ + if (level > 32767) level = 32767; + if (level < -32767) level = -32767; + return (s8)((level * 127) / 32767); +} + +/* Scale magnitude (0..32767 or signed) to 7-bit (0..127) */ +static inline u8 t500rs_scale_mag_u7(int magnitude) +{ + if (magnitude < 0) magnitude = -magnitude; + if (magnitude > 32767) magnitude = 32767; + return (u8)((magnitude * 127) / 32767); +} + +/* Fill Report 0x02 (envelope) buffer for T500RS: 9 bytes total + * buf[0]=0x02, buf[1]=0x1c, buf[2]=0x00, + * buf[3..4]=attack_length (le16), buf[5]=attack_level (u8 0..255), + * buf[6..7]=fade_length (le16), buf[8]=fade_level (u8 0..255) + */ +static inline void t500rs_fill_envelope_u02(u8 *buf, const struct ff_envelope *env) +{ + u16 a_len = env ? env->attack_length : 0; + u16 f_len = env ? env->fade_length : 0; + u8 a_lvl = env ? t500rs_scale_env_level(env->attack_level) : 0; + u8 f_lvl = env ? t500rs_scale_env_level(env->fade_level) : 0; + + struct t500rs_r02_envelope *r = (struct t500rs_r02_envelope *)buf; + memset(r, 0, sizeof(*r)); + r->id = 0x02; + r->subtype = 0x1c; + r->zero = 0x00; + r->attack_len = cpu_to_le16(a_len); + r->attack_lvl = a_lvl; + r->fade_len = cpu_to_le16(f_len); + r->fade_lvl = f_lvl; +} + + /* Debug logging helper (requires local variable named 't500rs') */ -#define T500RS_DBG(fmt, ...) \ - do { \ - /* Suppress normal debug at level 3 (unknown-only mode) */ \ - if (t500rs_log_level > 0 && t500rs_log_level < 3) \ - hid_info(t500rs->hdev, fmt, ##__VA_ARGS__); \ - } while (0) +#define T500RS_DBG(fmt, ...) hid_dbg(t500rs->hdev, fmt, ##__VA_ARGS__) /* T500RS device data */ struct t500rs_device_entry { @@ -105,12 +196,6 @@ struct t500rs_device_entry { u8 *send_buffer; size_t buffer_length; - /* Per-effect levels (0-100) from base driver's sysfs callbacks */ - u8 spring_gain; - u8 damper_gain; - u8 friction_gain; - u8 inertia_gain; - /* Current wheel range for smooth transitions */ u16 current_range; /* Current rotation range in degrees */ }; @@ -162,18 +247,6 @@ static int t500rs_send_usb(struct t500rs_device_entry *t500rs, const u8 *data, int ret, transferred; if (!t500rs || !data || len == 0 || len > T500RS_BUFFER_LENGTH) return -EINVAL; - if (t500rs_log_level > 1) { - char hex[3 * T500RS_BUFFER_LENGTH + 1]; - size_t i, off = 0; - for (i = 0; i < len && off + 3 < sizeof(hex); i++) - off += scnprintf(hex + off, sizeof(hex) - off, "%02x ", data[i]); - if (t500rs_log_level == 2) { - hid_info(t500rs->hdev, "USB TX [%zu]: %s\n", len, hex); - } else if (t500rs_log_level >= 3) { - if (!t500rs_is_known_tx(data, len)) - hid_info(t500rs->hdev, "USB TX UNKNOWN [%zu]: %s\n", len, hex); - } - } ret = usb_interrupt_msg(t500rs->usbdev, usb_sndintpipe(t500rs->usbdev, t500rs->ep_out), @@ -195,40 +268,50 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, T500RS_DBG("Upload constant: id=%d, level=%d\n", effect->id, level); - /* NO DEADZONE - Send all forces exactly as requested, matching Windows - * behavior */ - /* Report 0x02 - Envelope (attack/fade) */ - memset(buf, 0, 15); - buf[0] = 0x02; - buf[1] = 0x1c; /* Subtype 0x1c */ - buf[2] = 0x00; - T500RS_DBG("Sending Report 0x02 (envelope)...\n"); + t500rs_fill_envelope_u02(buf, &effect->u.constant.envelope); + T500RS_DBG("Sending Report 0x02 (envelope): a_len=%u a_lvl=%u f_len=%u f_lvl=%u\n", + effect->u.constant.envelope.attack_length, + t500rs_scale_env_level(effect->u.constant.envelope.attack_level), + effect->u.constant.envelope.fade_length, + t500rs_scale_env_level(effect->u.constant.envelope.fade_level)); ret = t500rs_send_usb(t500rs, buf, 9); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); return ret; } + /* + * IMPORTANT: T500RS Effect ID (buf[1]) semantics + * - All Report 0x01 "main effect upload" messages MUST use EffectID=0x00. + * Using per-effect IDs here breaks playback (e.g., constant force). + * - Report 0x41 START/STOP also requires EffectID=0x00, except special + * cases like stopping autocenter by its fixed ID during init. + * This matches behavior observed from the Windows driver and on-device tests. + */ + /* Report 0x01 - Main effect upload - MATCH WINDOWS DRIVER EXACTLY! */ - memset(buf, 0, 15); - buf[0] = 0x01; - buf[1] = 0x00; /* Effect ID 0 (Windows uses 0, not effect->id) */ - buf[2] = 0x00; /* Constant force type */ - buf[3] = 0x40; - buf[4] = 0xff; /* Windows uses 0xff (was 0x69) */ - buf[5] = 0xff; /* Windows uses 0xff (was 0x23) */ - buf[6] = 0x00; - buf[7] = 0xff; - buf[8] = 0xff; - buf[9] = 0x0e; /* Parameter subtype reference */ - buf[10] = 0x00; - buf[11] = 0x1c; /* Envelope subtype reference */ - buf[12] = 0x00; - buf[13] = 0x00; - buf[14] = 0x00; + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; /* Device expects Effect ID 0 for 0x01 on T500RS */ + m->type = 0x00; /* Constant force type */ + m->b3 = 0x40; + m->b4 = 0xff; /* Windows uses 0xff (was 0x69) */ + m->b5 = 0xff; /* Windows uses 0xff (was 0x23) */ + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = 0x0e; /* Parameter subtype reference */ + m->b10 = 0x00; + m->b11 = 0x1c; /* Envelope subtype reference */ + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } T500RS_DBG("Sending Report 0x01 (duration/control)...\n"); - ret = t500rs_send_usb(t500rs, buf, 15); + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); return ret; @@ -246,11 +329,7 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, */ { s8 signed_level; - s32 scaled; - - /* Simple linear scaling from -32767..32767 to -127..127 */ - scaled = (level * 127) / 32767; - signed_level = (s8)scaled; + signed_level = t500rs_scale_const_level_s8(level); T500RS_DBG("Upload constant: id=%d, level=%d -> %d (0x%02x)\n", effect->id, level, signed_level, (u8)signed_level); @@ -273,19 +352,19 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, switch (effect->type) { case FF_SPRING: effect_type = 0x40; - effect_gain = t500rs->spring_gain; + effect_gain = spring_level; break; case FF_DAMPER: effect_type = 0x41; - effect_gain = t500rs->damper_gain; + effect_gain = damper_level; break; case FF_FRICTION: effect_type = 0x41; - effect_gain = t500rs->friction_gain; + effect_gain = friction_level; break; case FF_INERTIA: effect_type = 0x41; - effect_gain = t500rs->inertia_gain; + effect_gain = 100; break; default: return -EINVAL; @@ -342,24 +421,29 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, if (ret) return ret; + /* NOTE: On T500RS, Report 0x01 MUST use EffectID=0x00; enforce it here. */ + /* Report 0x01 - Main effect upload */ - memset(buf, 0, 15); - buf[0] = 0x01; - buf[1] = effect->id; - buf[2] = effect_type; - buf[3] = 0x40; - buf[4] = 0x17; - buf[5] = 0x25; - buf[6] = 0x00; - buf[7] = 0xff; - buf[8] = 0xff; - buf[9] = 0x0e; - buf[10] = 0x00; - buf[11] = 0x1c; - buf[12] = 0x00; - buf[13] = 0x00; - buf[14] = 0x00; - ret = t500rs_send_usb(t500rs, buf, 15); + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; + m->type = effect_type; + m->b3 = 0x40; + m->b4 = 0x17; + m->b5 = 0x25; + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = 0x0e; + m->b10 = 0x00; + m->b11 = 0x1c; + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) return ret; @@ -409,8 +493,8 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, return -EINVAL; } - /* Magnitude - scale to 0-127 */ - mag = (abs(magnitude) * 127) / 32767; + /* Magnitude - scale to 0-127 with saturation */ + mag = t500rs_scale_mag_u7(magnitude); /* Period (frequency) - default to 100ms = 10 Hz if not set */ if (period == 0) { @@ -421,34 +505,36 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, type_name, effect->id, magnitude, mag, period); /* Report 0x02 - Envelope */ - memset(buf, 0, 15); - buf[0] = 0x02; - buf[1] = 0x1c; - buf[2] = 0x00; + t500rs_fill_envelope_u02(buf, &effect->u.periodic.envelope); ret = t500rs_send_usb(t500rs, buf, 9); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); return ret; } + /* NOTE: Device requires EffectID=0 for 0x01 uploads; see ID semantics above. */ + /* Report 0x01 - Main effect upload for periodic (set waveform/type) */ - memset(buf, 0, 15); - buf[0] = 0x01; - buf[1] = 0x00; /* Effect ID 0 */ - buf[2] = effect_type; /* Waveform type (0x20..0x24) */ - buf[3] = 0x40; - buf[4] = 0xff; - buf[5] = 0xff; - buf[6] = 0x00; - buf[7] = 0xff; - buf[8] = 0xff; - buf[9] = 0x0e; /* Parameter subtype reference */ - buf[10] = 0x00; - buf[11] = 0x1c; /* Envelope subtype reference */ - buf[12] = 0x00; - buf[13] = 0x00; - buf[14] = 0x00; - ret = t500rs_send_usb(t500rs, buf, 15); + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; /* Effect ID 0 required for T500RS 0x01 reports */ + m->type = effect_type; /* Waveform type (0x20..0x24) */ + m->b3 = 0x40; + m->b4 = 0xff; + m->b5 = 0xff; + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = 0x0e; /* Parameter subtype reference */ + m->b10 = 0x00; + m->b11 = 0x1c; /* Envelope subtype reference */ + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x01 (periodic main): %d\n", ret); @@ -456,39 +542,46 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, } /* Report 0x04 - Periodic parameters */ - memset(buf, 0, 15); - buf[0] = 0x04; - buf[1] = 0x0e; - buf[2] = 0x00; - buf[3] = mag; /* Magnitude */ - buf[4] = 0x00; /* Offset */ - buf[5] = 0x00; /* Phase */ - buf[6] = period & 0xff; /* Period low byte */ - buf[7] = (period >> 8) & 0xff; /* Period high byte */ - ret = t500rs_send_usb(t500rs, buf, 8); + { + struct t500rs_r04_periodic *p = (struct t500rs_r04_periodic *)buf; + memset(p, 0, sizeof(*p)); + p->id = 0x04; + p->code = 0x0e; + p->zero = 0x00; + p->magnitude = mag; + p->offset = 0x00; + p->phase = 0x00; + p->period = cpu_to_le16(period); + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r04_periodic)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x04: %d\n", ret); return ret; } + /* NOTE: On T500RS, all 0x01 uploads MUST use EffectID=0x00; enforce consistently. */ + /* Report 0x01 - Main effect upload */ - memset(buf, 0, 15); - buf[0] = 0x01; - buf[1] = effect->id; - buf[2] = effect_type; /* Waveform type */ - buf[3] = 0x40; - buf[4] = 0x17; - buf[5] = 0x25; - buf[6] = 0x00; - buf[7] = 0xff; - buf[8] = 0xff; - buf[9] = 0x0e; - buf[10] = 0x00; - buf[11] = 0x1c; - buf[12] = 0x00; - buf[13] = 0x00; - buf[14] = 0x00; - ret = t500rs_send_usb(t500rs, buf, 15); + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; + m->type = effect_type; /* Waveform type */ + m->b3 = 0x40; + m->b4 = 0x17; + m->b5 = 0x25; + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = 0x0e; + m->b10 = 0x00; + m->b11 = 0x1c; + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); return ret; @@ -516,10 +609,7 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, effect->id, start_level, end_level, duration_ms); /* Report 0x02 - Envelope */ - memset(buf, 0, 15); - buf[0] = 0x02; - buf[1] = 0x1c; - buf[2] = 0x00; + t500rs_fill_envelope_u02(buf, &effect->u.ramp.envelope); ret = t500rs_send_usb(t500rs, buf, 9); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); @@ -528,40 +618,45 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, /* Report 0x04 - Ramp parameters */ /* NOTE: T500RS doesn't support native ramp - just holds start level */ - memset(buf, 0, 15); - buf[0] = 0x04; - buf[1] = 0x0e; - buf[2] = start_scaled & 0xff; /* Start level low byte */ - buf[3] = (start_scaled >> 8) & 0xff; /* Start level high byte */ - buf[4] = start_scaled & 0xff; /* Current level (same as start) */ - buf[5] = (start_scaled >> 8) & 0xff; /* Current level high byte */ - buf[6] = duration_ms & 0xff; /* Duration low byte */ - buf[7] = (duration_ms >> 8) & 0xff; /* Duration high byte */ - buf[8] = 0x00; - ret = t500rs_send_usb(t500rs, buf, 9); + { + struct t500rs_r04_ramp *rr = (struct t500rs_r04_ramp *)buf; + memset(rr, 0, sizeof(*rr)); + rr->id = 0x04; + rr->code = 0x0e; + rr->start = cpu_to_le16(start_scaled); + rr->cur_val = cpu_to_le16(start_scaled); + rr->duration = cpu_to_le16(duration_ms); + rr->zero = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r04_ramp)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x04: %d\n", ret); return ret; } + /* NOTE: On T500RS, Report 0x01 MUST use EffectID=0x00; enforce it here. */ + /* Report 0x01 - Main effect upload */ - memset(buf, 0, 15); - buf[0] = 0x01; - buf[1] = effect->id; - buf[2] = 0x24; /* Ramp type (0x24 = sawtooth down / ramp) */ - buf[3] = 0x40; - buf[4] = duration_ms & 0xff; /* Duration low byte */ - buf[5] = (duration_ms >> 8) & 0xff; /* Duration high byte */ - buf[6] = 0x00; - buf[7] = 0xff; - buf[8] = 0xff; - buf[9] = 0x0e; - buf[10] = 0x00; - buf[11] = 0x1c; - buf[12] = 0x00; - buf[13] = 0x00; - buf[14] = 0x00; - ret = t500rs_send_usb(t500rs, buf, 15); + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; + m->type = 0x24; /* Ramp type (0x24 = sawtooth down / ramp) */ + m->b3 = 0x40; + m->b4 = duration_ms & 0xff; /* Duration low byte */ + m->b5 = (duration_ms >> 8) & 0xff; /* Duration high byte */ + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = 0x0e; + m->b10 = 0x00; + m->b11 = 0x1c; + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); return ret; @@ -615,44 +710,49 @@ static int t500rs_play_effect(void *data, if (effect->type == FF_CONSTANT) { int level = effect->u.constant.level; s8 signed_level; - s32 scaled; - - /* Simple linear scaling from -32767..32767 to -127..127 */ - scaled = (level * 127) / 32767; - signed_level = (s8)scaled; + signed_level = t500rs_scale_const_level_s8(level); T500RS_DBG("Constant force: level=%d -> %d (0x%02x)\n", level, signed_level, (u8)signed_level); /* Send Report 0x03 (force level) */ - buf[0] = 0x03; - buf[1] = 0x0e; - buf[2] = 0x00; - buf[3] = (u8)signed_level; - ret = t500rs_send_usb(t500rs, buf, 4); + { + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; + r3->id = 0x03; + r3->code = 0x0e; + r3->zero = 0x00; + r3->level = signed_level; + ret = t500rs_send_usb(t500rs, (u8 *)r3, sizeof(*r3)); + } if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x03: %d\n", ret); return ret; } /* Send Report 0x41 START */ - buf[0] = 0x41; - buf[1] = 0x00; /* Effect ID 0 */ - buf[2] = 0x41; /* START */ - buf[3] = 0x01; - return t500rs_send_usb(t500rs, buf, 4); + { + struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; + r41->effect_id = 0x00; + r41->command = 0x41; + r41->arg = 0x01; + } + return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); } /* For other effect types, send start command - Report 0x41 * T500RS expects EffectID=0 for 0x41 commands as well. */ - buf[0] = 0x41; - buf[1] = 0x00; /* Effect ID 0 to match device expectations */ - buf[2] = 0x41; /* START command */ - buf[3] = 0x01; + { + struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; + r41->effect_id = 0x00; + r41->command = 0x41; + r41->arg = 0x01; + } T500RS_DBG("Sending START command (EffectID=0) for effect %d\n", effect->id); - return t500rs_send_usb(t500rs, buf, 4); + return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); } /* Stop effect */ @@ -678,22 +778,28 @@ static int t500rs_stop_effect(void *data, /* For constant force: Windows-style STOP (0x41 00 00 01) */ if (state->effect.type == FF_CONSTANT) { - buf[0] = 0x41; - buf[1] = 0x00; - buf[2] = 0x00; /* STOP */ - buf[3] = 0x01; - return t500rs_send_usb(t500rs, buf, 4); + { + struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; + r41->effect_id = 0x00; + r41->command = 0x00; + r41->arg = 0x01; + } + return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); } /* For other effect types, send stop command - Report 0x41 * Use EffectID=0 to match device expectations for 0x41. */ - buf[0] = 0x41; - buf[1] = 0x00; /* Effect ID 0 */ - buf[2] = 0x00; /* STOP command */ - buf[3] = 0x01; + { + struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; + r41->effect_id = 0x00; + r41->command = 0x00; + r41->arg = 0x01; + } - ret = t500rs_send_usb(t500rs, buf, 4); + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); T500RS_DBG("Stop effect (non-constant) returned: %d\n", ret); return ret; } @@ -715,21 +821,22 @@ static int t500rs_update_effect(void *data, if (effect->type == FF_CONSTANT) { int level = effect->u.constant.level; s8 signed_level; - s32 scaled; u8 *buf3; buf3 = t500rs->send_buffer; if (!buf3) return -ENOMEM; - scaled = (level * 127) / 32767; - signed_level = (s8)scaled; + signed_level = t500rs_scale_const_level_s8(level); - buf3[0] = 0x03; - buf3[1] = 0x0e; - buf3[2] = 0x00; - buf3[3] = (u8)signed_level; - return t500rs_send_usb(t500rs, buf3, 4); + { + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf3; + r3->id = 0x03; + r3->code = 0x0e; + r3->zero = 0x00; + r3->level = signed_level; + } + return t500rs_send_usb(t500rs, buf3, sizeof(struct t500rs_r03_const)); } return 0; @@ -891,68 +998,6 @@ static int t500rs_set_range(void *data, u16 range) { return 0; } -/* Set spring level (0-100) - called from sysfs */ -static int t500rs_set_spring_level(void *data, u8 level) { - struct t500rs_device_entry *t500rs = data; - - if (!t500rs) - return -ENODEV; - - /* Clamp to valid range */ - if (level > 100) - level = 100; - - /* Update the spring gain setting */ - t500rs->spring_gain = level; - - T500RS_DBG("Spring level set to %u%%\n", level); - - /* Note: The new level will be applied when the next spring effect is - * uploaded/updated */ - return 0; -} - -/* Set damper level (0-100) - called from sysfs */ -static int t500rs_set_damper_level(void *data, u8 level) { - struct t500rs_device_entry *t500rs = data; - - if (!t500rs) - return -ENODEV; - - /* Clamp to valid range */ - if (level > 100) - level = 100; - - /* Update the damper gain setting */ - t500rs->damper_gain = level; - - T500RS_DBG("Damper level set to %u%%\n", level); - - /* Note: The new level will be applied when the next damper effect is - * uploaded/updated */ - return 0; -} - -/* Set friction level (0-100) - called from sysfs */ -static int t500rs_set_friction_level(void *data, u8 level) { - struct t500rs_device_entry *t500rs = data; - - if (!t500rs) - return -ENODEV; - - /* Clamp to valid range */ - if (level > 100) - level = 100; - - /* Update the friction gain setting */ - t500rs->friction_gain = level; - - T500RS_DBG("Friction level set to %u%%\n", level); - - /* Note: The new level will be applied when the next friction effect is - * uploaded/updated */ - return 0; -} /* Initialize T500RS device */ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { @@ -969,7 +1014,6 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } hid_dbg(tmff2->hdev, "T500RS: Initializing USB INTERRUPT mode\n"); - hid_info(tmff2->hdev, "T500RS driver version %s\n", T500RS_DRIVER_VERSION); /* Allocate device data */ t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); @@ -1022,12 +1066,6 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { goto err_buffer; } - /* Initialize per-effect levels to 100% (0-100 range) */ - t500rs->spring_gain = 100; - t500rs->damper_gain = 100; - t500rs->friction_gain = 100; - t500rs->inertia_gain = 100; - /* Initialize current range to default (900°) for smooth transitions */ t500rs->current_range = 900; @@ -1124,12 +1162,14 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } /* Report 0x41 - Clear effects (4 bytes) */ - memset(init_buf, 0, 4); - init_buf[0] = 0x41; - init_buf[1] = 0x00; - init_buf[2] = 0x00; - init_buf[3] = 0x00; - ret = t500rs_send_usb(t500rs, init_buf, 4); + { + struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)init_buf; + r41->id = 0x41; + r41->effect_id = 0x00; + r41->command = 0x00; /* CLEAR */ + r41->arg = 0x00; + } + ret = t500rs_send_usb(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); if (ret) { hid_warn(t500rs->hdev, "Init command 9 (0x41 clear) failed: %d\n", ret); } @@ -1186,12 +1226,14 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } /* Stop autocenter effect (effect ID 15) */ - memset(init_buf, 0, 4); - init_buf[0] = 0x41; - init_buf[1] = 15; /* Autocenter effect ID */ - init_buf[2] = 0x00; /* STOP */ - init_buf[3] = 0x01; - ret = t500rs_send_usb(t500rs, init_buf, 4); + { + struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)init_buf; + r41->id = 0x41; + r41->effect_id = 15; /* Autocenter effect ID */ + r41->command = 0x00; /* STOP */ + r41->arg = 0x01; + } + ret = t500rs_send_usb(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); if (ret) { hid_warn(t500rs->hdev, "Stop autocenter effect failed: %d\n", ret); } else { @@ -1241,9 +1283,6 @@ int t500rs_populate_api(struct tmff2_device_entry *tmff2) { tmff2->set_gain = t500rs_set_gain; tmff2->set_autocenter = t500rs_set_autocenter; tmff2->set_range = t500rs_set_range; - tmff2->set_spring_level = t500rs_set_spring_level; - tmff2->set_damper_level = t500rs_set_damper_level; - tmff2->set_friction_level = t500rs_set_friction_level; tmff2->wheel_init = t500rs_wheel_init; tmff2->wheel_destroy = t500rs_wheel_destroy; diff --git a/tools/build-reload.sh b/tools/build-reload.sh deleted file mode 100755 index bf2bea8..0000000 --- a/tools/build-reload.sh +++ /dev/null @@ -1,172 +0,0 @@ -#!/bin/bash -# Build, reload, and verify T500RS driver modules -# Usage: sudo ./build_reload_verify.sh [insmod params] - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}T500RS Driver Build, Reload, and Verify Script${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -# Check if running as root -if [ "$EUID" -ne 0 ]; then - echo -e "${RED}ERROR: Please run as root (use sudo)${NC}" - exit 1 -fi - -# Resolve repo root and local .ko path -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO_ROOT="$(cd "$SCRIPT_DIR"/.. && pwd)" -LOCAL_KO="$REPO_ROOT/hid-tmff-new.ko" - -# Step 1: Build -echo -e "${YELLOW}[1/8] Building driver...${NC}" -if make -C "$REPO_ROOT" -j4; then - echo -e "${GREEN} ✓ Build succeeded${NC}" -else - echo -e "${RED} ✗ Build failed — aborting${NC}" - exit 1 -fi - -if [ ! -f "$LOCAL_KO" ]; then - echo -e "${RED} ✗ Module not found after build: $LOCAL_KO${NC}" - exit 1 -fi - -# Step 2: Clean up T500RS device and sysfs files -echo -e "${YELLOW}[2/8] Cleaning up T500RS device...${NC}" -# Find T500RS HID device in sysfs -T500RS_DEVICE=$(find /sys/bus/hid/devices/ -name "*044F:B65E*" 2>/dev/null | head -1) -if [ -n "$T500RS_DEVICE" ]; then - DEVICE_NAME=$(basename "$T500RS_DEVICE") - echo " - Found HID device: $DEVICE_NAME" - # Manually remove leftover sysfs files (from failed probe) - echo " - Removing leftover sysfs files..." - for attr in gain range spring_level damper_level friction_level alternate_modes; do - if [ -e "$T500RS_DEVICE/$attr" ]; then - rm -f "$T500RS_DEVICE/$attr" 2>/dev/null || true - fi - done - # Unbind from current driver if bound - if [ -e "$T500RS_DEVICE/driver" ]; then - CURRENT_DRIVER=$(basename $(readlink "$T500RS_DEVICE/driver")) - echo " - Unbinding from $CURRENT_DRIVER..." - echo "$DEVICE_NAME" > "$T500RS_DEVICE/driver/unbind" 2>/dev/null || true - sleep 0.5 - fi - echo -e "${GREEN} ✓ Device cleaned up${NC}" -else - echo " - T500RS HID device not found" -fi -echo "" - -# Step 3: Unload modules -echo -e "${YELLOW}[3/8] Unloading modules...${NC}" -# Try to unload in reverse dependency order -if lsmod | grep -q "hid_tmff_new"; then - echo " - Removing hid_tmff_new..." - modprobe -r hid_tmff_new || { - echo -e "${RED} WARNING: Failed to remove hid_tmff_new (may be in use)${NC}" - echo " Trying to force removal..." - rmmod -f hid_tmff_new 2>/dev/null || true - } -else - echo " - hid_tmff_new not loaded" -fi - -if lsmod | grep -q "usb_tminit_new"; then - echo " - Removing usb_tminit_new..." - modprobe -r usb_tminit_new || true -else - echo " - usb_tminit_new not loaded" -fi - -if lsmod | grep -q "hid_tminit_new"; then - echo " - Removing hid_tminit_new..." - modprobe -r hid_tminit_new || true -else - echo " - hid_tminit_new not loaded" -fi - -echo -e "${GREEN} ✓ Modules unloaded${NC}" -echo "" - -# Step 4: Wait a moment for cleanup -echo -e "${YELLOW}[4/8] Waiting for cleanup...${NC}" -sleep 2 -echo -e "${GREEN} ✓ Done${NC}" -echo "" - -# Step 5: Load init modules -echo -e "${YELLOW}[5/8] Loading init modules...${NC}" -modprobe hid_tminit_new && echo -e "${GREEN} ✓ hid_tminit_new loaded${NC}" || echo -e "${RED} ✗ Failed to load hid_tminit_new${NC}" -modprobe usb_tminit_new && echo -e "${GREEN} ✓ usb_tminit_new loaded${NC}" || echo -e "${RED} ✗ Failed to load usb_tminit_new${NC}" -echo "" - -# Step 6: Wait for init to complete -echo -e "${YELLOW}[6/8] Waiting for device initialization...${NC}" -sleep 3 -echo -e "${GREEN} ✓ Done${NC}" -echo "" - -# Step 7: Load main driver (from local build directory) -echo -e "${YELLOW}[7/8] Loading main driver from local build...${NC}" -if [ -f "$LOCAL_KO" ]; then - echo " - Loading $LOCAL_KO with params: $*" - insmod "$LOCAL_KO" "$@" && echo -e "${GREEN} ✓ hid_tmff_new loaded from local build${NC}" || { - echo -e "${RED} ✗ Failed to load hid_tmff_new${NC}" - exit 1 - } -else - echo -e "${RED} ✗ Module not found: $LOCAL_KO${NC}" - echo " Run 'make' first to build the module" - exit 1 -fi -echo "" - -# Step 8: Verify loaded module matches local build -echo -e "${YELLOW}[8/8] Verifying loaded module matches local build...${NC}" -LOCAL_SRCVER=$(modinfo -F srcversion "$LOCAL_KO" 2>/dev/null || echo "unknown") -LOADED_SRCVER=$(cat /sys/module/hid_tmff_new/srcversion 2>/dev/null || echo "none") -INSTALLED_PATH=$(modinfo -n hid_tmff_new 2>/dev/null || echo "unknown") - -printf " - Local .ko srcversion: %s\n" "$LOCAL_SRCVER" -printf " - Loaded module srcversion:%s\n" "$LOADED_SRCVER" -printf " - Installed candidate: %s\n" "$INSTALLED_PATH" - -if [ "$LOCAL_SRCVER" != "$LOADED_SRCVER" ]; then - echo -e "${RED} ✗ Mismatch: loaded module is not the local build${NC}" - exit 1 -else - echo -e "${GREEN} ✓ Loaded module matches local build${NC}" -fi - -echo "" - -# Show T500RS logs -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}T500RS Device Status:${NC}" -echo -e "${BLUE}========================================${NC}" -if dmesg | grep -E "T500RS driver version|T500RS initialized successfully" | tail -n 10; then - : -fi - -# Check for errors -if dmesg | tail -50 | grep -qi "error\|fail\|bug\|oops"; then - echo -e "${RED}⚠ WARNING: Errors detected in kernel log!${NC}" - dmesg | tail -50 | grep -i "error\|fail\|bug\|oops" | tail -10 - echo "" -fi - -echo -e "${GREEN}========================================${NC}" -echo -e "${GREEN}Build, reload, and verification complete!${NC}" -echo -e "${GREEN}========================================${NC}" - diff --git a/udev/71-thrustmaster-steamdeck.rules b/udev/71-thrustmaster-steamdeck.rules index 3f3ea1e..67d8bd2 100644 --- a/udev/71-thrustmaster-steamdeck.rules +++ b/udev/71-thrustmaster-steamdeck.rules @@ -20,3 +20,7 @@ KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b692", MODE="0660 # TSPC KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b689", MODE="0660", TAG+="uaccess" + + +# T500RS PC mode +KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0660", TAG+="uaccess" diff --git a/udev/99-thrustmaster.rules b/udev/99-thrustmaster.rules index c37b410..e96cb02 100644 --- a/udev/99-thrustmaster.rules +++ b/udev/99-thrustmaster.rules @@ -27,29 +27,3 @@ SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b692", RUN+="/us # TSPC SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b689", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b689", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" - -# T500RS initialization mode -KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" -KERNEL=="event*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" -KERNEL=="js*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" -SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", MODE="0666", TAG+="uaccess" -SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" -SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65d", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" - -# T500RS PC mode -# IMPORTANT FOR PROTON/STEAM GAMES: -# If you have multiple joystick devices (js0, js1, etc.) that cause conflicts, -# use the event device directly in Steam launch options: -# SDL_JOYSTICK_DEVICE=/dev/input/eventXX %command% -# Find your event number with: cat /proc/bus/input/devices | grep -A 5 "Thrustmaster" -KERNEL=="hidraw*", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" -KERNEL=="event*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" -KERNEL=="js*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" -SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODE="0666", TAG+="uaccess" -SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", MODALIAS=="hid:b0003g*v0000044Fp0000B65E", RUN+="/bin/sh -c 'echo %k > /sys/bus/hid/drivers/tmff2/bind'" -SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" -SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" - -# T500RS sysfs attributes permissions (for GUI and non-root access) -# Set permissions on all sysfs attributes after device is bound to driver -SUBSYSTEM=="hid", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/bin/sh -c 'sleep 0.5; chmod 0666 /sys/bus/hid/devices/%k/gain /sys/bus/hid/devices/%k/range /sys/bus/hid/devices/%k/autocenter /sys/bus/hid/devices/%k/t500rs_* 2>/dev/null || true'" \ No newline at end of file From 9c8c1f0b38dba9f55a609261cfb6e594007489e8 Mon Sep 17 00:00:00 2001 From: caz Date: Wed, 12 Nov 2025 16:14:14 +0100 Subject: [PATCH 3/4] Candidate/driver alignment on par other drivers and captures (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver alignment on par other drivers and captures Motivation - Align the T500RS driver’s runtime behavior with the T300/TS‑X pattern and with Windows usbmon captures to improve correctness and reduce in‑game dropouts. - Ensure parameter updates don’t force re‑uploads, and preserve the device’s idiosyncrasies (EffectID=0 for 0x01/0x41, fixed constant linkage). What’s changed - Update path parity (parameter‑only updates; no STOP/0x01 re‑upload): * FF_CONSTANT: send only Report 0x03 (code=0x0e) with new level. * FF_PERIODIC: send only Report 0x04 with new magnitude + frequency (Hz×100). * FF_SPRING/FF_DAMPER/FF_FRICTION/FF_INERTIA (conditions): send only two Report 0x05 packets (coeff/saturation via param_sub; deadband/center via env_sub_first). * FF_RAMP: send only Report 0x04 with start/cur_val/duration. - Subtype rules: * Constant force uses fixed linkage (Report 0x03 code=0x0e; 0x01 b9=0x0e, b11=0x1c; 0x02 subtype=0x1c). * Periodic/ramp/condition continue to use per‑effect subtypes (param_sub = 0x0e + 0x1c×index; env_sub_first = 0x1c + 0x1c×index). - Upload sequence parity with captures: * Pre‑upload STOP (0x41 00 01), dual 0x02 envelopes, 0x01 first/second, and effect‑specific parameter packets (0x03/0x04/0x05) as observed on Windows. - Protocol correctness fixes and clarifications: * Periodic parameter uses frequency in Hz×100 (not period in ms). * EffectID semantics enforced: 0x01/0x41 use EffectID=0 (exception: init STOP for autocenter). - Quality‑of‑life: * Range setting simplified to a single 0x40 0x11 command (removed smoothing). * Wine compatibility: ignore AC=100% requests to avoid permanent autocenter in titles like LFS; documented rationale. --- docs/FFB_T500RS.md | 100 ++++- src/hid-tmff2.c | 34 +- src/tmt500rs/hid-tmt500rs-usb.c | 629 +++++++++++++++++++++----------- udev/99-thrustmaster.rules | 4 + 4 files changed, 522 insertions(+), 245 deletions(-) diff --git a/docs/FFB_T500RS.md b/docs/FFB_T500RS.md index f7e73ac..9660f2d 100644 --- a/docs/FFB_T500RS.md +++ b/docs/FFB_T500RS.md @@ -2,6 +2,17 @@ This page documents the packet formats and ordering used by the T500RS when driven via the USB interrupt endpoint (0x01 OUT). It mirrors the style of docs/FFBEFFECTS.md for other wheels. + +Quick reference (Windows parity, at-a-glance) +- EffectID rule: All 0x01 uploads and 0x41 START/STOP use EffectID=0x00 +- Subtypes per effect index n: param = 0x0e + 0x1c×n; envelope = 0x1c + 0x1c×n; second envelope = first + 0x1c +- Periodic 0x04 uses frequency (Hz×100), not period (ms) +- Upload sequences (per effect): + - Constant: STOP → 0x02 → 0x01 → 0x02 → 0x03 → 0x01 → START + - Periodic (sine/square/triangle/saw): STOP → 0x02 → 0x01 → 0x02 → 0x04 → 0x01 → START + - Ramp: STOP → 0x02 → 0x01 → 0x02 → 0x04 → 0x01 → START + - Condition (spring/damper/friction/inertia): STOP → 0x05(set1) → 0x05(set2) → 0x01 → START + Key rules - Little‑endian for 16‑bit fields - DMA‑safe buffer length 32 bytes; typical messages are 2, 4, 9 or 15 bytes @@ -10,6 +21,14 @@ Key rules - Report 0x41 START/STOP also uses EffectID = 0x00, except the init‑time STOP for autocenter which targets a fixed ID (15) - Using per‑effect IDs with 0x01 breaks playback (constant force fails completely) +Special case — constant force subtypes + +- For FF_CONSTANT, the device uses fixed subtypes to link the 0x03 level update: + - Report 0x02 subtype = 0x1c + - Report 0x01 b9 = 0x0e (parameter), b11 = 0x1c (envelope) + - Report 0x03 code = 0x0e +- Using per‑effect subtypes for constant breaks level updates (no force felt). + Report glossary - 0x01 main upload (15 bytes) - Layout (unknown bytes kept for completeness): @@ -22,9 +41,9 @@ Report glossary - b6 0x00 - b7 0xff - b8 0xff - - b9 0x0e (parameter subtype) + - b9 param subtype = 0x0e + 0x1c×index - b10 0x00 - - b11 0x1c (envelope subtype) + - b11 envelope subtype = 0x1c + 0x1c×index - b12 0x00 - b13 0x00 - b14 0x00 @@ -41,7 +60,7 @@ Report glossary - b0 0x04, b1 0x0e, b2 0x00 - b3 magnitude u7 (0..127) - b4 offset = 0, b5 phase = 0 - - b6..7 period (le16, ms) + - b6..7 frequency (le16, Hz×100) (e.g., 10 Hz → 0x03e8) - 0x04 ramp params (9 bytes) - b0 0x04, b1 0x0e - b2..3 start (le16), b4..5 cur_val (le16), b6..7 duration (le16), b8 0x00 @@ -56,27 +75,76 @@ Report glossary - 0x42 apply/apply‑like, 0x43 gain Effect upload and play ordering + +- Subtype system (applies to 0x01/0x02/0x04/0x05 references) + - Envelope subtype base = 0x1c; Parameter subtype base = 0x0e + - For effect index n: envelope = 0x1c + 0x1c×n; parameter = 0x0e + 0x1c×n + - In 0x01: b9 = parameter subtype; b11 = envelope subtype + +- Global (Windows parity) + - Precede uploads with 0x41 STOP (command=0x00, arg=0x01), effect_id=0x00, to clear state + - Constant (FF_CONSTANT) - 1) 0x02 envelope - 2) 0x01 main (type=0x00, effect_id=0x00) - Play: 0x03 level, then 0x41 START (effect_id=0x00) + 1) 0x41 STOP + 2) 0x02 envelope (first; subtype = 0x1c + 0x1c×index) + 3) 0x01 main (first; b9/b11 reference current param/envelope subtypes) + 4) 0x02 envelope (second; subtype += 0x1c) + 5) 0x03 level + 6) 0x01 main (second; update references) + Play: 0x41 START (effect_id=0x00) + - Periodic (FF_SINE/FF_SQUARE/FF_TRIANGLE/FF_SAW) - 1) 0x02 envelope - 2) 0x01 main (type=0x20..0x24, effect_id=0x00) - 3) 0x04 periodic params (magnitude/period) - 4) 0x01 main (type repeated, effect_id=0x00) + 1) 0x41 STOP + 2) 0x02 envelope (first) + 3) 0x01 main (first; type=0x20..0x24, effect_id=0x00) + 4) 0x02 envelope (second; subtype += 0x1c) + 5) 0x04 periodic params (magnitude/frequency) + 6) 0x01 main (second; type repeated) Play: 0x41 START (effect_id=0x00) + - Condition (FF_SPRING/FF_DAMPER/FF_FRICTION/FF_INERTIA) - 1) 0x05 coeff/saturation (0x0e) - 2) 0x05 deadband/center (0x1c) - 3) 0x01 main (type reflects subkind, effect_id=0x00) + 1) 0x41 STOP + 2) 0x05 coeff/saturation (param subtype = 0x0e + 0x1c×index) + 3) 0x05 deadband/center (envelope subtype = 0x1c + 0x1c×index) + 4) 0x01 main (type reflects subkind, effect_id=0x00; b9/b11 reference above) Play: 0x41 START (effect_id=0x00) + - Ramp (FF_RAMP) - 1) 0x02 envelope - 2) 0x04 ramp params (device behaves like hold of start level with duration) - 3) 0x01 main (type=0x24, effect_id=0x00) + 1) 0x41 STOP + 2) 0x02 envelope (first) + 3) 0x01 main (first; type=0x24, effect_id=0x00) + 4) 0x02 envelope (second; subtype += 0x1c) + 5) 0x04 ramp params (device behaves like hold of start level with duration) + 6) 0x01 main (second) Play: 0x41 START (effect_id=0x00) + +Live update behavior (parameter‑only; Windows parity) + +- Overview + - Parameter updates do not re‑upload effects or send STOP; they modify parameters in place. + - This reduces USB traffic and avoids momentary dropouts during gameplay. + - 0x01/0x41 still follow the EffectID=0 rule; 0x04/0x05 carry subtypes, not effect IDs. + +- Constant (FF_CONSTANT) + - Send only 0x03 with new level; code must be 0x0e (fixed linkage to constant path). + - No 0x41 STOP and no 0x01 re‑upload for updates. + +- Periodic (FF_SINE/FF_SQUARE/FF_TRIANGLE/FF_SAW) + - Send only 0x04 with new magnitude and frequency. + - Frequency field is Hz×100 (e.g., 10 Hz → 1000); offset/phase remain 0 unless changed. + - Use per‑effect param_sub = 0x0e + 0x1c×index. + +- Condition (FF_SPRING/FF_DAMPER/FF_FRICTION/FF_INERTIA) + - Send 0x05 (coeff/saturation) with param_sub, then 0x05 (deadband/center) with env_sub_first. + - Subtypes are the same as in the upload path; spring uses subtype bytes 0x54, others 0x64 for the tail fields. + - No 0x01 re‑upload. + +- Ramp (FF_RAMP) + - Send only 0x04 (start/cur_val/duration, last byte 0x00) using param_sub. + - Device behavior approximates ramp via holding/stepping the start value over duration; matching observed Windows behavior. + + Notes - All 0x01 uploads must use EffectID=0x00. This was validated on hardware; using per‑effect IDs causes constant force to fail completely and can break other effects. - 16‑bit values are little‑endian (lo byte first). diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index caf0d6f..1d80289 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -6,20 +6,6 @@ #include "hid-tmff2.h" -/* Known vendor/opcode IDs managed by the driver (TX side) */ -static inline int tmff2_is_known_vendor_id(unsigned char id) -{ - switch (id) { - case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: - case 0x40: case 0x41: case 0x42: case 0x43: - case 0x0a: - return 1; - default: - return 0; - } -} - - int open_mode = 1; module_param(open_mode, int, 0660); MODULE_PARM_DESC(open_mode, @@ -256,6 +242,22 @@ static ssize_t gain_store(struct device *dev, } gain = value; + + /* Rationale: two-level gain model + * - The input API's set_gain (pg) is the in-game gain (0..GAIN_MAX). + * - This driver also exposes a device/system gain via sysfs param `gain`. + * - The device callback receives the product: (pg * gain) / GAIN_MAX. + * See worker at tmff2->set_gain(... (pg * gain) / GAIN_MAX ). + * When the sysfs `gain` changes, we trigger a recompute by pushing + * pending_gain_value = GAIN_MAX here so the effective device gain becomes + * exactly the sysfs value (GAIN_MAX * gain / GAIN_MAX == gain) and future + * in-game set_gain calls continue to multiply in. + * + * References: + * - docs/FFBEFFECTS.md: section "FF_GAIN" shows a dedicated device gain path. + * - docs/FFB_T500RS.md: Report glossary mentions 0x43 (gain), i.e., device-side + * gain separate from per-effect magnitudes; drivers should expose both levels. + */ if (tmff2->set_gain) { unsigned long flags; spin_lock_irqsave(&tmff2->lock, flags); @@ -376,7 +378,7 @@ static void tmff2_work_handler(struct work_struct *w) effect_length = state->effect.replay.length; /* If playing with a finite length, stop when (delay + length) elapses */ if (test_bit(FF_EFFECT_PLAYING, &state->flags) && effect_length) { - if ((time_now - state->start_time) >= + if ((time_now - state->start_time) >= (effect_delay + effect_length) * state->count) { __clear_bit(FF_EFFECT_PLAYING, &state->flags); __clear_bit(FF_EFFECT_QUEUE_UPDATE, &state->flags); @@ -853,7 +855,6 @@ static void tmff2_remove(struct hid_device *hdev) if (tmff2->params & PARAM_DAMPER_LEVEL) device_remove_file(dev, &dev_attr_damper_level); - if (tmff2->params & PARAM_SPRING_LEVEL) device_remove_file(dev, &dev_attr_spring_level); @@ -889,6 +890,7 @@ static const struct hid_device_id tmff2_devices[] = { {} }; +MODULE_DEVICE_TABLE(hid, tmff2_devices); static struct hid_driver tmff2_driver = { .name = "tmff2", diff --git a/src/tmt500rs/hid-tmt500rs-usb.c b/src/tmt500rs/hid-tmt500rs-usb.c index fd4dbf9..862aba2 100644 --- a/src/tmt500rs/hid-tmt500rs-usb.c +++ b/src/tmt500rs/hid-tmt500rs-usb.c @@ -17,7 +17,6 @@ #include "../hid-tmff2.h" - /* T500RS Constants */ #define T500RS_MAX_EFFECTS 16 #define T500RS_BUFFER_LENGTH 32 /* USB endpoint max packet size */ @@ -29,42 +28,41 @@ /* Gain scaling */ #define GAIN_MAX 65535 - /* Packet structs (packed) to reduce magic bytes on T500RS protocol */ struct t500rs_r02_envelope { - u8 id; /* 0x02 */ - u8 subtype; /* 0x1c */ - u8 zero; /* 0x00 */ + u8 id; /* 0x02 */ + u8 subtype; /* 0x1c */ + u8 zero; /* 0x00 */ __le16 attack_len; - u8 attack_lvl; /* 0..255 */ + u8 attack_lvl; /* 0..255 */ __le16 fade_len; - u8 fade_lvl; /* 0..255 */ + u8 fade_lvl; /* 0..255 */ } __packed; struct t500rs_r03_const { - u8 id; /* 0x03 */ - u8 code; /* 0x0e */ - u8 zero; /* 0x00 */ - s8 level; /* -127..127 */ + u8 id; /* 0x03 */ + u8 code; /* 0x0e */ + u8 zero; /* 0x00 */ + s8 level; /* -127..127 */ } __packed; struct t500rs_r04_periodic { - u8 id; /* 0x04 */ - u8 code; /* 0x0e */ - u8 zero; /* 0x00 */ - u8 magnitude; /* 0..127 */ - u8 offset; /* 0 */ - u8 phase; /* 0 */ - __le16 period; /* ms */ + u8 id; /* 0x04 */ + u8 code; /* 0x0e */ + u8 zero; /* 0x00 */ + u8 magnitude; /* 0..127 */ + u8 offset; /* 0 */ + u8 phase; /* 0 */ + __le16 period; /* frequency (Hz×100) */ } __packed; struct t500rs_r04_ramp { - u8 id; /* 0x04 */ - u8 code; /* 0x0e */ + u8 id; /* 0x04 */ + u8 code; /* 0x0e */ __le16 start; - __le16 cur_val; /* same as start */ - __le16 duration; /* ms */ - u8 zero; /* 0 */ + __le16 cur_val; /* same as start */ + __le16 duration; /* ms */ + u8 zero; /* 0 */ } __packed; struct t500rs_r41_cmd { @@ -93,7 +91,6 @@ struct t500rs_r01_main { u8 b14; } __packed; - /* Helper: classify whether a TX buffer is a known/managed report * Known first bytes (report IDs / opcodes) we intentionally emit: * 0x01,0x02,0x03,0x04,0x05,0x40,0x41,0x42,0x43,0x0a @@ -134,26 +131,27 @@ static inline int t500rs_is_known_tx(const unsigned char *data, size_t len) { } /* Scale envelope level (0..32767) to device 8-bit (0..255) */ -static inline u8 t500rs_scale_env_level(u16 level) -{ +static inline u8 t500rs_scale_env_level(u16 level) { if (level > 32767) level = 32767; return (u8)((level * 255) / 32767); } /* Scale constant level (-32767..32767) to signed 8-bit (-127..127) */ -static inline s8 t500rs_scale_const_level_s8(int level) -{ - if (level > 32767) level = 32767; - if (level < -32767) level = -32767; +static inline s8 t500rs_scale_const_level_s8(int level) { + if (level > 32767) + level = 32767; + if (level < -32767) + level = -32767; return (s8)((level * 127) / 32767); } /* Scale magnitude (0..32767 or signed) to 7-bit (0..127) */ -static inline u8 t500rs_scale_mag_u7(int magnitude) -{ - if (magnitude < 0) magnitude = -magnitude; - if (magnitude > 32767) magnitude = 32767; +static inline u8 t500rs_scale_mag_u7(int magnitude) { + if (magnitude < 0) + magnitude = -magnitude; + if (magnitude > 32767) + magnitude = 32767; return (u8)((magnitude * 127) / 32767); } @@ -162,17 +160,17 @@ static inline u8 t500rs_scale_mag_u7(int magnitude) * buf[3..4]=attack_length (le16), buf[5]=attack_level (u8 0..255), * buf[6..7]=fade_length (le16), buf[8]=fade_level (u8 0..255) */ -static inline void t500rs_fill_envelope_u02(u8 *buf, const struct ff_envelope *env) -{ +static inline void +t500rs_fill_envelope_u02(u8 *buf, const struct ff_envelope *env, u8 subtype) { u16 a_len = env ? env->attack_length : 0; u16 f_len = env ? env->fade_length : 0; u8 a_lvl = env ? t500rs_scale_env_level(env->attack_level) : 0; - u8 f_lvl = env ? t500rs_scale_env_level(env->fade_level) : 0; + u8 f_lvl = env ? t500rs_scale_env_level(env->fade_level) : 0; struct t500rs_r02_envelope *r = (struct t500rs_r02_envelope *)buf; memset(r, 0, sizeof(*r)); r->id = 0x02; - r->subtype = 0x1c; + r->subtype = subtype; r->zero = 0x00; r->attack_len = cpu_to_le16(a_len); r->attack_lvl = a_lvl; @@ -180,11 +178,11 @@ static inline void t500rs_fill_envelope_u02(u8 *buf, const struct ff_envelope *e r->fade_lvl = f_lvl; } - /* Debug logging helper (requires local variable named 't500rs') */ #define T500RS_DBG(fmt, ...) hid_dbg(t500rs->hdev, fmt, ##__VA_ARGS__) /* T500RS device data */ +#define T500RS_MAX_EFFECTS 16 struct t500rs_device_entry { struct hid_device *hdev; struct input_dev *input_dev; @@ -198,6 +196,16 @@ struct t500rs_device_entry { /* Current wheel range for smooth transitions */ u16 current_range; /* Current rotation range in degrees */ + + /* Cache last-sent condition coefficients per effect ID to avoid re-sending + * identical 0x05 (sub=0x0e) coefficient updates at high cadence. ACC spams + * condition updates with identical values at low speed, and reapplying them + * causes tactile rumble on T500RS. Zero-initialized via kzalloc. */ + u8 last_cond_rcoef[T500RS_MAX_EFFECTS]; + u8 last_cond_lcoef[T500RS_MAX_EFFECTS]; + u8 last_cond_b9[T500RS_MAX_EFFECTS]; + u8 last_cond_b10[T500RS_MAX_EFFECTS]; + u8 last_cond_valid[T500RS_MAX_EFFECTS]; }; /* Supported parameters */ @@ -207,8 +215,9 @@ static const unsigned long t500rs_params = /* Supported effects */ static const signed short t500rs_effects[] = { - FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, - FF_PERIODIC, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; + FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, + FF_PERIODIC, FF_SINE, FF_TRIANGLE, FF_SQUARE, FF_SAW_UP, + FF_SAW_DOWN, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; /* Forward declarations to avoid implicit declarations before worker uses them */ @@ -256,6 +265,25 @@ static int t500rs_send_usb(struct t500rs_device_entry *t500rs, const u8 *data, return (transferred == len) ? 0 : -EIO; } +/* Send pre-upload STOP (Report 0x41 with effect_id=0, command=0x00, arg=0x01) + * Matches Windows behavior of clearing the slot before (re)uploading. + */ +static inline int t500rs_send_pre_stop(struct t500rs_device_entry *t500rs) { + u8 *buf; + struct t500rs_r41_cmd *r41; + if (!t500rs) + return -ENODEV; + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; + r41->effect_id = 0x00; + r41->command = 0x00; /* STOP/CLEAR */ + r41->arg = 0x01; + return t500rs_send_usb(t500rs, buf, sizeof(*r41)); +} + /* Upload constant force effect */ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, const struct tmff2_effect_state *state) { @@ -265,16 +293,17 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, int level = effect->u.constant.level; /* Note: Gain is applied in play_effect, not here */ - T500RS_DBG("Upload constant: id=%d, level=%d\n", effect->id, level); + /* Pre-upload STOP to clear the slot (Windows parity) */ + ret = t500rs_send_pre_stop(t500rs); + if (ret) { + hid_err(t500rs->hdev, "Pre-upload STOP failed: %d\n", ret); + return ret; + } + /* Report 0x02 - Envelope (attack/fade) */ - t500rs_fill_envelope_u02(buf, &effect->u.constant.envelope); - T500RS_DBG("Sending Report 0x02 (envelope): a_len=%u a_lvl=%u f_len=%u f_lvl=%u\n", - effect->u.constant.envelope.attack_length, - t500rs_scale_env_level(effect->u.constant.envelope.attack_level), - effect->u.constant.envelope.fade_length, - t500rs_scale_env_level(effect->u.constant.envelope.fade_level)); + t500rs_fill_envelope_u02(buf, &effect->u.constant.envelope, 0x1c); ret = t500rs_send_usb(t500rs, buf, 9); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); @@ -289,7 +318,6 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, * cases like stopping autocenter by its fixed ID during init. * This matches behavior observed from the Windows driver and on-device tests. */ - /* Report 0x01 - Main effect upload - MATCH WINDOWS DRIVER EXACTLY! */ { struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; @@ -303,38 +331,73 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, m->b6 = 0x00; m->b7 = 0xff; m->b8 = 0xff; - m->b9 = 0x0e; /* Parameter subtype reference */ + m->b9 = 0x0e; /* Parameter subtype reference (fixed) */ m->b10 = 0x00; - m->b11 = 0x1c; /* Envelope subtype reference */ + m->b11 = 0x1c; /* Envelope subtype reference (fixed) */ m->b12 = 0x00; m->b13 = 0x00; m->b14 = 0x00; } - T500RS_DBG("Sending Report 0x01 (duration/control)...\n"); + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); return ret; } - T500RS_DBG("Constant effect %d uploaded (simple sequence)\n", effect->id); + /* Report 0x02 - Envelope (second; subtype = first + 0x1c) */ + t500rs_fill_envelope_u02(buf, &effect->u.constant.envelope, 0x1c); + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x02 (second): %d\n", ret); + return ret; + } - /* CRITICAL FIX : Always update the force level when uploading. - * Game calls stop/upload/play in rapid succession, so the timer might be - * stopped when upload is called. We update the force level here so that - * when play_effect starts the timer, it will use the correct force value. - * - * MATCH WINDOWS: Send forces exactly as requested - no amplification! - * Windows sends weak forces (4-27 out of 127) and they work fine. - */ + /* Report 0x03 - Constant force level (param subtype) */ { - s8 signed_level; - signed_level = t500rs_scale_const_level_s8(level); + s8 signed_level = t500rs_scale_const_level_s8(level); + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; + r3->id = 0x03; + r3->code = 0x0e; + r3->zero = 0x00; + r3->level = signed_level; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r03_const)); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x03 (const level): %d\n", + ret); + return ret; + } - T500RS_DBG("Upload constant: id=%d, level=%d -> %d (0x%02x)\n", effect->id, - level, signed_level, (u8)signed_level); + /* Report 0x01 - Main effect upload (second; references second envelope + * subtype) */ + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; /* Device expects Effect ID 0 for 0x01 on T500RS */ + m->type = 0x00; /* Constant force type */ + m->b3 = 0x40; + m->b4 = 0xff; /* Windows uses 0xff (was 0x69) */ + m->b5 = 0xff; /* Windows uses 0xff (was 0x23) */ + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = 0x0e; /* Parameter subtype reference (fixed) */ + m->b10 = 0x00; + m->b11 = 0x1c; /* Envelope subtype reference (fixed) */ + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x01 (second): %d\n", ret); + return ret; } + T500RS_DBG("Constant effect %d uploaded (dual 0x02 + dual 0x01 sequence)\n", + effect->id); return 0; } @@ -348,6 +411,11 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, u8 effect_gain; int right_strength, left_strength; + /* Subtype indices derived from effect->id to match Windows subtype system */ + unsigned int idx = (unsigned int)effect->id; + u8 param_sub = (u8)(0x0e + (0x1c * idx)); + u8 env_sub_first = (u8)(0x1c + (0x1c * idx)); + /* Determine effect type and select appropriate gain */ switch (effect->type) { case FF_SPRING: @@ -387,10 +455,17 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, effect->id, effect_type, effect_gain, right_strength, left_strength); + /* Pre-upload STOP to clear the slot (Windows parity) */ + ret = t500rs_send_pre_stop(t500rs); + if (ret) { + hid_err(t500rs->hdev, "Pre-upload STOP failed: %d\n", ret); + return ret; + } + /* Report 0x05 - Condition parameters (coefficients) */ memset(buf, 0, 15); buf[0] = 0x05; - buf[1] = 0x0e; + buf[1] = param_sub; buf[2] = 0x00; buf[3] = (u8)right_strength; buf[4] = (u8)left_strength; @@ -407,7 +482,7 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, /* Report 0x05 - Condition parameters (deadband/center) */ memset(buf, 0, 15); buf[0] = 0x05; - buf[1] = 0x1c; + buf[1] = env_sub_first; buf[2] = 0x00; buf[3] = 0x00; /* Deadband */ buf[4] = 0x00; /* Center */ @@ -422,7 +497,6 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, return ret; /* NOTE: On T500RS, Report 0x01 MUST use EffectID=0x00; enforce it here. */ - /* Report 0x01 - Main effect upload */ { struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; @@ -436,9 +510,9 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, m->b6 = 0x00; m->b7 = 0xff; m->b8 = 0xff; - m->b9 = 0x0e; + m->b9 = param_sub; m->b10 = 0x00; - m->b11 = 0x1c; + m->b11 = env_sub_first; m->b12 = 0x00; m->b13 = 0x00; m->b14 = 0x00; @@ -462,9 +536,6 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, u16 period = effect->u.periodic.period; u8 mag; - /* Use game-provided magnitude directly; base gain is applied via set_gain() - */ - /* Determine waveform type */ switch (effect->u.periodic.waveform) { case FF_SQUARE: @@ -495,17 +566,39 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, /* Magnitude - scale to 0-127 with saturation */ mag = t500rs_scale_mag_u7(magnitude); + /* Subtype indices derived from effect->id to match Windows subtype system */ + unsigned int idx = (unsigned int)effect->id; + u8 param_sub = (u8)(0x0e + (0x1c * idx)); + u8 env_sub_first = (u8)(0x1c + (0x1c * idx)); + u8 env_sub_second = (u8)(env_sub_first + 0x1c); - /* Period (frequency) - default to 100ms = 10 Hz if not set */ + /* Period (ms) -> device frequency (Hz×100). Default to 100ms = 10 Hz if unset */ if (period == 0) { period = 100; } + { + u32 freq_hz100 = 100000U / period; + if (freq_hz100 < 1U) + freq_hz100 = 1U; + if (freq_hz100 > 65535U) + freq_hz100 = 65535U; + T500RS_DBG("Upload %s: id=%d, magnitude=%d (0x%02x), period=%dms -> " + "freq=%u (Hz×100)\n", + type_name, effect->id, magnitude, mag, period, + (unsigned)freq_hz100); + /* Reuse 'period' variable to carry converted frequency to the packet write below */ + period = (u16)freq_hz100; + } - T500RS_DBG("Upload %s: id=%d, magnitude=%d (0x%02x), period=%dms\n", - type_name, effect->id, magnitude, mag, period); + /* Pre-upload STOP to clear the slot (Windows parity) */ + ret = t500rs_send_pre_stop(t500rs); + if (ret) { + hid_err(t500rs->hdev, "Pre-upload STOP failed: %d\n", ret); + return ret; + } - /* Report 0x02 - Envelope */ - t500rs_fill_envelope_u02(buf, &effect->u.periodic.envelope); + /* Report 0x02 - Envelope (first) */ + t500rs_fill_envelope_u02(buf, &effect->u.periodic.envelope, env_sub_first); ret = t500rs_send_usb(t500rs, buf, 9); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); @@ -513,13 +606,12 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, } /* NOTE: Device requires EffectID=0 for 0x01 uploads; see ID semantics above. */ - /* Report 0x01 - Main effect upload for periodic (set waveform/type) */ { struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; memset(m, 0, sizeof(*m)); m->id = 0x01; - m->effect_id = 0x00; /* Effect ID 0 required for T500RS 0x01 reports */ + m->effect_id = 0x00; /* Effect ID 0 required for T500RS 0x01 reports */ m->type = effect_type; /* Waveform type (0x20..0x24) */ m->b3 = 0x40; m->b4 = 0xff; @@ -527,9 +619,9 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, m->b6 = 0x00; m->b7 = 0xff; m->b8 = 0xff; - m->b9 = 0x0e; /* Parameter subtype reference */ + m->b9 = param_sub; /* Parameter subtype reference (per-effect) */ m->b10 = 0x00; - m->b11 = 0x1c; /* Envelope subtype reference */ + m->b11 = env_sub_first; /* Envelope subtype reference (first) */ m->b12 = 0x00; m->b13 = 0x00; m->b14 = 0x00; @@ -540,13 +632,20 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, ret); return ret; } + /* Report 0x02 - Envelope (second) */ + t500rs_fill_envelope_u02(buf, &effect->u.periodic.envelope, env_sub_second); + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x02 (second): %d\n", ret); + return ret; + } /* Report 0x04 - Periodic parameters */ { struct t500rs_r04_periodic *p = (struct t500rs_r04_periodic *)buf; memset(p, 0, sizeof(*p)); p->id = 0x04; - p->code = 0x0e; + p->code = param_sub; p->zero = 0x00; p->magnitude = mag; p->offset = 0x00; @@ -559,9 +658,8 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, return ret; } - /* NOTE: On T500RS, all 0x01 uploads MUST use EffectID=0x00; enforce consistently. */ - - /* Report 0x01 - Main effect upload */ + /* NOTE: On T500RS, all 0x01 uploads MUST use EffectID=0x00 */ + /* Report 0x01 - Main effect upload (second) */ { struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; memset(m, 0, sizeof(*m)); @@ -574,9 +672,9 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, m->b6 = 0x00; m->b7 = 0xff; m->b8 = 0xff; - m->b9 = 0x0e; + m->b9 = param_sub; m->b10 = 0x00; - m->b11 = 0x1c; + m->b11 = env_sub_second; m->b12 = 0x00; m->b13 = 0x00; m->b14 = 0x00; @@ -602,27 +700,75 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, u16 duration_ms = effect->replay.length; u16 start_scaled; + /* Subtype indices derived from effect->id to match Windows subtype system */ + unsigned int idx = (unsigned int)effect->id; + u8 param_sub = (u8)(0x0e + (0x1c * idx)); + u8 env_sub_first = (u8)(0x1c + (0x1c * idx)); + u8 env_sub_second = (u8)(env_sub_first + 0x1c); + /* Scale to 0-255 */ start_scaled = (abs(start_level) * 0xff) / 32767; T500RS_DBG("Upload ramp: id=%d, start=%d, end=%d, duration=%dms\n", effect->id, start_level, end_level, duration_ms); + /* Pre-upload STOP to clear the slot (Windows parity) */ + ret = t500rs_send_pre_stop(t500rs); + if (ret) { + hid_err(t500rs->hdev, "Pre-upload STOP failed: %d\n", ret); + return ret; + } + /* Report 0x02 - Envelope */ - t500rs_fill_envelope_u02(buf, &effect->u.ramp.envelope); + t500rs_fill_envelope_u02(buf, &effect->u.ramp.envelope, env_sub_first); ret = t500rs_send_usb(t500rs, buf, 9); if (ret) { hid_err(t500rs->hdev, "Failed to send Report 0x02: %d\n", ret); return ret; } + /* NOTE: On T500RS, Report 0x01 MUST use EffectID=0x00 */ + /* Report 0x01 - Main effect upload (first) */ + { + struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; + memset(m, 0, sizeof(*m)); + m->id = 0x01; + m->effect_id = 0x00; + m->type = 0x24; /* Ramp type (0x24 = sawtooth down / ramp) */ + m->b3 = 0x40; + m->b4 = duration_ms & 0xff; /* Duration low byte */ + m->b5 = (duration_ms >> 8) & 0xff; /* Duration high byte */ + m->b6 = 0x00; + m->b7 = 0xff; + m->b8 = 0xff; + m->b9 = param_sub; + m->b10 = 0x00; + m->b11 = env_sub_first; /* first envelope subtype */ + m->b12 = 0x00; + m->b13 = 0x00; + m->b14 = 0x00; + } + ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x01 (ramp first): %d\n", ret); + return ret; + } + + /* Report 0x02 - Envelope (second) */ + t500rs_fill_envelope_u02(buf, &effect->u.ramp.envelope, env_sub_second); + ret = t500rs_send_usb(t500rs, buf, 9); + if (ret) { + hid_err(t500rs->hdev, "Failed to send Report 0x02 (second): %d\n", ret); + return ret; + } + /* Report 0x04 - Ramp parameters */ /* NOTE: T500RS doesn't support native ramp - just holds start level */ { struct t500rs_r04_ramp *rr = (struct t500rs_r04_ramp *)buf; memset(rr, 0, sizeof(*rr)); rr->id = 0x04; - rr->code = 0x0e; + rr->code = param_sub; rr->start = cpu_to_le16(start_scaled); rr->cur_val = cpu_to_le16(start_scaled); rr->duration = cpu_to_le16(duration_ms); @@ -634,9 +780,7 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, return ret; } - /* NOTE: On T500RS, Report 0x01 MUST use EffectID=0x00; enforce it here. */ - - /* Report 0x01 - Main effect upload */ + /* Report 0x01 - Main effect upload (second) */ { struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; memset(m, 0, sizeof(*m)); @@ -649,20 +793,21 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, m->b6 = 0x00; m->b7 = 0xff; m->b8 = 0xff; - m->b9 = 0x0e; + m->b9 = param_sub; m->b10 = 0x00; - m->b11 = 0x1c; + m->b11 = env_sub_second; /* second envelope subtype */ m->b12 = 0x00; m->b13 = 0x00; m->b14 = 0x00; } ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); if (ret) { - hid_err(t500rs->hdev, "Failed to send Report 0x01: %d\n", ret); + hid_err(t500rs->hdev, "Failed to send Report 0x01 (ramp second): %d\n", + ret); return ret; } - T500RS_DBG("Ramp effect %d uploaded (simple mode)\n", effect->id); + T500RS_DBG("Ramp effect %d uploaded (dual 0x02 + dual 0x01)\n", effect->id); return 0; } @@ -809,37 +954,146 @@ static int t500rs_update_effect(void *data, const struct tmff2_effect_state *state) { struct t500rs_device_entry *t500rs = data; const struct ff_effect *effect = &state->effect; + u8 *buf; if (!t500rs) return -ENODEV; - /* Do NOT re-upload here; Windows keeps the effect and only updates force - * level */ - /* This avoids redundant USB traffic and state churn */ - - /* Update constant force: send single 0x03 with new level */ - if (effect->type == FF_CONSTANT) { - int level = effect->u.constant.level; - s8 signed_level; - u8 *buf3; - - buf3 = t500rs->send_buffer; - if (!buf3) - return -ENOMEM; - - signed_level = t500rs_scale_const_level_s8(level); + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; - { - struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf3; + /* Do NOT re-upload here; Windows keeps the effect and we only update parameters */ + switch (effect->type) { + case FF_CONSTANT: { + int level = effect->u.constant.level; + s8 signed_level = t500rs_scale_const_level_s8(level); + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; r3->id = 0x03; r3->code = 0x0e; r3->zero = 0x00; r3->level = signed_level; + return t500rs_send_usb(t500rs, (u8 *)r3, sizeof(*r3)); + } + case FF_PERIODIC: { + u8 mag = t500rs_scale_mag_u7(effect->u.periodic.magnitude); + u16 period = effect->u.periodic.period; + unsigned int idx = (unsigned int)effect->id; + u8 param_sub = (u8)(0x0e + (0x1c * idx)); + + if (period == 0) + period = 100; + { + u32 freq_hz100 = 100000U / period; + if (freq_hz100 < 1U) freq_hz100 = 1U; + if (freq_hz100 > 65535U) freq_hz100 = 65535U; + period = (u16)freq_hz100; + } + + { + struct t500rs_r04_periodic *p = (struct t500rs_r04_periodic *)buf; + memset(p, 0, sizeof(*p)); + p->id = 0x04; + p->code = param_sub; + p->zero = 0x00; + p->magnitude = mag; + p->offset = 0x00; + p->phase = 0x00; + p->period = cpu_to_le16(period); + } + return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r04_periodic)); } - return t500rs_send_usb(t500rs, buf3, sizeof(struct t500rs_r03_const)); + case FF_RAMP: { + int start_level = effect->u.ramp.start_level; + u16 duration_ms = effect->replay.length; + unsigned int idx = (unsigned int)effect->id; + u8 param_sub = (u8)(0x0e + (0x1c * idx)); + u16 start_scaled = (abs(start_level) * 0xff) / 32767; + struct t500rs_r04_ramp *rr = (struct t500rs_r04_ramp *)buf; + memset(rr, 0, sizeof(*rr)); + rr->id = 0x04; + rr->code = param_sub; + rr->start = cpu_to_le16(start_scaled); + rr->cur_val = cpu_to_le16(start_scaled); + rr->duration = cpu_to_le16(duration_ms); + rr->zero = 0x00; + return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r04_ramp)); + } + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: { + unsigned int idx = (unsigned int)effect->id; + u8 param_sub = (u8)(0x0e + (0x1c * idx)); + u8 effect_gain; + int right_strength = effect->u.condition[0].right_saturation; + int left_strength = effect->u.condition[0].left_saturation; + + switch (effect->type) { + case FF_SPRING: effect_gain = spring_level; break; + case FF_DAMPER: effect_gain = damper_level; break; + case FF_FRICTION: effect_gain = friction_level; break; + case FF_INERTIA: effect_gain = 100; break; + default: effect_gain = 100; break; + } + + right_strength = (right_strength * effect_gain) / 100; + left_strength = (left_strength * effect_gain) / 100; + + right_strength = (right_strength * 127) / 65535; + left_strength = (left_strength * 127) / 65535; + + /* + * Rationale: ACC (and similar) may spam condition updates at low speed with + * the exact same parameters. Re-sending 0x05 (sub=0x0e) at high cadence + * makes T500RS micro-pulse/rumble. Also, sending 0x05 (sub=0x1c) on update + * is unnecessary when deadband/center haven't changed and can exacerbate + * the issue. Therefore: + * - Only send 0x05 (sub=0x0e) when coefficients actually change + * - Skip 0x05 (sub=0x1c) entirely on updates + */ + { + u8 rcoef = (u8)right_strength; + u8 lcoef = (u8)left_strength; + u8 b9 = (effect->type == FF_SPRING) ? 0x54 : 0x64; + u8 b10 = (effect->type == FF_SPRING) ? 0x54 : 0x64; + + if (idx < T500RS_MAX_EFFECTS && t500rs->last_cond_valid[idx] && + t500rs->last_cond_rcoef[idx] == rcoef && + t500rs->last_cond_lcoef[idx] == lcoef && + t500rs->last_cond_b9[idx] == b9 && + t500rs->last_cond_b10[idx] == b10) { + /* Coefficients unchanged: no-op (avoid rumble) */ + return 0; + } + + memset(buf, 0, 15); + buf[0] = 0x05; + buf[1] = param_sub; + buf[2] = 0x00; + buf[3] = rcoef; + buf[4] = lcoef; + buf[5] = buf[6] = buf[7] = buf[8] = 0x00; + buf[9] = b9; + buf[10] = b10; + if (t500rs_send_usb(t500rs, buf, 11)) + return -EIO; + + if (idx < T500RS_MAX_EFFECTS) { + t500rs->last_cond_rcoef[idx] = rcoef; + t500rs->last_cond_lcoef[idx] = lcoef; + t500rs->last_cond_b9[idx] = b9; + t500rs->last_cond_b10[idx] = b10; + t500rs->last_cond_valid[idx] = 1; + } + } + + /* Skip 0x05 (sub=0x1c) on updates by design; see rationale above. */ + return 0; + } + default: + return 0; } - - return 0; } /* Set autocenter */ @@ -854,6 +1108,17 @@ static int t500rs_set_autocenter(void *data, u16 autocenter) { autocenter_percent = (u8)((autocenter * 100) / 65535); + /* Wine compatibility: Some games (e.g., LFS under Wine) set autocenter to 100%% + * at startup and never release it. That leaves a permanent strong centering force + * which masks/overpowers other forces. To avoid this, ignore requests that try to + * set maximum autocenter (100%%). Disabling (0) is still honored; lower values are + * allowed. */ + if (autocenter_percent >= 100) { + hid_warn(t500rs->hdev, + "Ignoring 100%% autocenter request (Wine/LFS compatibility)"); + return 0; + } + buf = t500rs->send_buffer; if (!buf) return -ENOMEM; @@ -902,8 +1167,7 @@ static int t500rs_set_range(void *data, u16 range) { struct t500rs_device_entry *t500rs = data; u8 *buf; int ret; - u16 range_value, current_value, target_value; - int step, i, num_steps; + u16 range_value; if (!t500rs) return -ENODEV; @@ -922,65 +1186,22 @@ static int t500rs_set_range(void *data, u16 range) { T500RS_DBG("Setting wheel range to %u degrees\n", range); - /* Based on testing with actual hardware: - * The T500RS uses Report 0x40 0x11 [value_lo] [value_hi] to set rotation - * range - * - * Hardware testing showed: - * - Byte order is LITTLE-ENDIAN (low byte first) - * - Formula: value = range * 60 - * - Smooth transitions prevent hard mechanical ticking - * - * To smooth the transition, we send multiple intermediate values - * when the range change is large. - */ - target_value = range * 60; - current_value = t500rs->current_range * 60; - - /* Calculate number of steps based on the change magnitude - * Larger changes need more steps for smooth transition - * Use many small steps to prevent hard mechanical ticking */ - range_value = (target_value > current_value) ? (target_value - current_value) - : (current_value - target_value); - - /* Use approximately 1 step per 500 units of change, minimum 1, maximum 50 */ - num_steps = range_value / 500; - if (num_steps < 1) - num_steps = 1; - if (num_steps > 50) - num_steps = 50; - - step = (target_value - current_value) / num_steps; - - /* Send gradual range changes */ - for (i = 1; i <= num_steps; i++) { - if (i == num_steps) { - range_value = target_value; /* Ensure we hit exact target */ - } else { - range_value = current_value + (step * i); - } + /* Device expects LITTLE-ENDIAN and value = range * 60. */ + range_value = range * 60; - /* Send Report 0x40 0x11 [value_lo] [value_hi] to set range - * NOTE: This uses LITTLE-ENDIAN byte order (low byte first)! */ - buf[0] = 0x40; - buf[1] = 0x11; - buf[2] = range_value & 0xFF; /* Low byte first (little-endian) */ - buf[3] = (range_value >> 8) & 0xFF; /* High byte second */ + /* Send Report 0x40 0x11 [value_lo] [value_hi] to set range */ + buf[0] = 0x40; + buf[1] = 0x11; + buf[2] = range_value & 0xFF; /* Low byte first (little-endian) */ + buf[3] = (range_value >> 8) & 0xFF; /* High byte second */ - ret = t500rs_send_usb(t500rs, buf, 4); - if (ret) { - hid_err(t500rs->hdev, "Failed to send range command: %d\n", ret); - return ret; - } - - T500RS_DBG("Range step %d/%d: value=0x%04x\n", i, num_steps, range_value); - - /* Very small delay between steps for smooth transition - * (only if not the last step) */ - /* Avoid explicit delays; USB stack handles pacing. */ + ret = t500rs_send_usb(t500rs, buf, 4); + if (ret) { + hid_err(t500rs->hdev, "Failed to send range command: %d\n", ret); + return ret; } - /* Store current range for next transition */ + /* Store current range */ t500rs->current_range = range; /* Apply settings with Report 0x42 0x05 */ @@ -993,12 +1214,11 @@ static int t500rs_set_range(void *data, u16 range) { } T500RS_DBG("Range set to %u degrees (final value=0x%04x)\n", range, - target_value); + range_value); return 0; } - /* Initialize T500RS device */ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { struct t500rs_device_entry *t500rs; @@ -1066,7 +1286,7 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { goto err_buffer; } - /* Initialize current range to default (900°) for smooth transitions */ + /* Initialize current range to default (900°) */ t500rs->current_range = 900; /* Store device data in tmff2 */ @@ -1120,19 +1340,18 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } /* Report 0x40 - Enable FFB (4 bytes) */ - /* CRITICAL FIX: Use Windows parameters 42 7b instead of 55 d5 */ memset(init_buf, 0, 4); init_buf[0] = 0x40; init_buf[1] = 0x11; - init_buf[2] = 0x42; /* Changed from 0x55 to match Windows! */ - init_buf[3] = 0x7b; /* Changed from 0xd5 to match Windows! */ + init_buf[2] = 0x42; + init_buf[3] = 0x7b; ret = t500rs_send_usb(t500rs, init_buf, 4); if (ret) { hid_warn(t500rs->hdev, "Init command 5 (0x40 enable) failed: %d\n", ret); } /* Report 0x42 - Additional init (2 bytes) */ - memset(init_buf, 0, 4); + memset(init_buf, 0, 2); init_buf[0] = 0x42; init_buf[1] = 0x04; ret = t500rs_send_usb(t500rs, init_buf, 2); @@ -1144,6 +1363,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { memset(init_buf, 0, 4); init_buf[0] = 0x40; init_buf[1] = 0x04; + /* b2..b3 = 0x0000 -> disable autocenter (Windows parity). + * Keep explicit zeros even though memset() clears them, to document the wire image. */ init_buf[2] = 0x00; init_buf[3] = 0x00; ret = t500rs_send_usb(t500rs, init_buf, 4); @@ -1152,10 +1373,9 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } /* Report 0x43 - Set global gain (2 bytes) */ - /* CRITICAL FIX: Set gain to maximum (0xFF = 100%), not 0x00! */ - memset(init_buf, 0, 4); + memset(init_buf, 0, 2); init_buf[0] = 0x43; - init_buf[1] = 0xFF; /* Maximum gain - was 0x00 which DISABLED all forces! */ + init_buf[1] = 0xFF; ret = t500rs_send_usb(t500rs, init_buf, 2); if (ret) { hid_warn(t500rs->hdev, "Init command 8 (0x43) failed: %d\n", ret); @@ -1166,7 +1386,7 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)init_buf; r41->id = 0x41; r41->effect_id = 0x00; - r41->command = 0x00; /* CLEAR */ + r41->command = 0x00; r41->arg = 0x00; } ret = t500rs_send_usb(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); @@ -1174,10 +1394,11 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { hid_warn(t500rs->hdev, "Init command 9 (0x41 clear) failed: %d\n", ret); } - /* Report 0x40 - Final setup (4 bytes) */ + /* Report 0x40 - Finalization (4 bytes) */ memset(init_buf, 0, 4); init_buf[0] = 0x40; init_buf[1] = 0x08; + /* b2..b3 must be 0x0000 (reserved); explicit for clarity / Windows parity */ init_buf[2] = 0x00; init_buf[3] = 0x00; ret = t500rs_send_usb(t500rs, init_buf, 4); @@ -1190,27 +1411,13 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { init_buf[0] = 0x40; init_buf[1] = 0x03; init_buf[2] = 0x0d; + /* b3 reserved = 0x00 (Windows parity) */ init_buf[3] = 0x00; ret = t500rs_send_usb(t500rs, init_buf, 4); if (ret) { hid_warn(t500rs->hdev, "Init command 11 (0x40 mode) failed: %d\n", ret); } - - /* Disable autocenter spring properly */ - /* Report 0x05 - Set spring coefficients to 0 */ - memset(init_buf, 0, 15); - init_buf[0] = 0x05; - init_buf[1] = 0x0e; - init_buf[2] = 0x00; - init_buf[3] = 0x00; /* Right coefficient = 0 */ - init_buf[4] = 0x00; /* Left coefficient = 0 */ - init_buf[9] = 0x00; /* Right saturation = 0 */ - init_buf[10] = 0x00; /* Left saturation = 0 */ - ret = t500rs_send_usb(t500rs, init_buf, 11); - if (ret) { - hid_warn(t500rs->hdev, "Disable autocenter (0x05 0x0e) failed: %d\n", ret); - } - + /* Report 0x05 - Set deadband and center */ memset(init_buf, 0, 15); init_buf[0] = 0x05; @@ -1229,8 +1436,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { { struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)init_buf; r41->id = 0x41; - r41->effect_id = 15; /* Autocenter effect ID */ - r41->command = 0x00; /* STOP */ + r41->effect_id = 15; /* Autocenter effect ID */ + r41->command = 0x00; /* STOP */ r41->arg = 0x01; } ret = t500rs_send_usb(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); @@ -1245,6 +1452,11 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { T500RS_DBG("Endpoint: 0x%02x, Buffer: %zu bytes\n", t500rs->ep_out, t500rs->buffer_length); + /* Advertise capabilities now that init succeeded */ + tmff2->params = t500rs_params; + tmff2->max_effects = T500RS_MAX_EFFECTS; + memcpy(tmff2->supported_effects, t500rs_effects, sizeof(t500rs_effects)); + return 0; err_buffer: @@ -1273,7 +1485,6 @@ static int t500rs_wheel_destroy(void *data) { /* Populate API callbacks */ int t500rs_populate_api(struct tmff2_device_entry *tmff2) { - int i; tmff2->play_effect = t500rs_play_effect; tmff2->upload_effect = t500rs_upload_effect; @@ -1287,13 +1498,5 @@ int t500rs_populate_api(struct tmff2_device_entry *tmff2) { tmff2->wheel_init = t500rs_wheel_init; tmff2->wheel_destroy = t500rs_wheel_destroy; - tmff2->params = t500rs_params; - tmff2->max_effects = T500RS_MAX_EFFECTS; - - /* Copy supported effects array */ - for (i = 0; t500rs_effects[i] != -1 && i < FF_CNT; i++) - tmff2->supported_effects[i] = t500rs_effects[i]; - tmff2->supported_effects[i] = -1; - return 0; } diff --git a/udev/99-thrustmaster.rules b/udev/99-thrustmaster.rules index e96cb02..25b8b59 100644 --- a/udev/99-thrustmaster.rules +++ b/udev/99-thrustmaster.rules @@ -12,6 +12,10 @@ SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b66f", RUN+="/us SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b66d", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b66d", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" +# T500RS +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" + # T248 + T128 SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b696", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b696", RUN+="/usr/bin/jscal -s 11,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" From 768afca7a94fff0833a904a3b93f39fec6c342d0 Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Fri, 14 Nov 2025 01:18:02 +0100 Subject: [PATCH 4/4] Another pass after comments review - Using effect + old to manage updates instead of custom implementation - Removed un-necessary functions - Documentation tidy-up - FFB_T500RS effects documentation rewritten, examples added - Init sequence simplified (on-par with windows captures). It was initially put in place and kept because it was "working" - Dropped the redundant second 0x02 and second 0x01 in constant upload path - Constant upload, play, and update paths now sends signed level and direction instead of scaling the raw level - changed T500RS_DBG to take the struct t500rs_device_entry * explicitly - Updated makefile to support build without being git repo (DKMS, tarball, release package) --- Makefile | 15 +- docs/FFBEFFECTS.md | 2 +- docs/FFB_T500RS.md | 382 +++++++++++++++++++++++++++-- src/tmt500rs/hid-tmt500rs-usb.c | 413 +++++++++++--------------------- 4 files changed, 517 insertions(+), 295 deletions(-) diff --git a/Makefile b/Makefile index 5a107d6..11487fe 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,20 @@ KDIR ?= /lib/modules/$(shell uname -r)/build # Auto-generated global build-time version for TMFF2 TMFF2_BASE_VERSION ?= 0.1 -GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null || echo "local") + +# Allow packagers / CI to provide a fixed hash or full version: +# make GIT_HASH=deadbee +# make TMFF2_VERSION=0.1-1 +# +# Only derive GIT_HASH from git if none was provided and this is a git checkout. +ifeq ($(origin GIT_HASH), undefined) + GIT_HASH := $(shell if command -v git >/dev/null 2>&1 && [ -d .git ]; then \ + git rev-parse --short=7 HEAD 2>/dev/null; \ + else \ + echo local; \ + fi) +endif + BUILD_HASH := $(shell date +%s | sha1sum | cut -c1-7) TMFF2_VERSION ?= $(TMFF2_BASE_VERSION)-$(GIT_HASH)+b$(BUILD_HASH) diff --git a/docs/FFBEFFECTS.md b/docs/FFBEFFECTS.md index a9a5de5..a9a313a 100644 --- a/docs/FFBEFFECTS.md +++ b/docs/FFBEFFECTS.md @@ -1025,4 +1025,4 @@ binary hex ff00.0021 = 0 ff00.0021 = 0 ff00.0021 = 0 -``` \ No newline at end of file +``` diff --git a/docs/FFB_T500RS.md b/docs/FFB_T500RS.md index 9660f2d..be612bc 100644 --- a/docs/FFB_T500RS.md +++ b/docs/FFB_T500RS.md @@ -1,14 +1,28 @@ -## T500RS USB FFB Protocol (observed) +## T500RS USB FFB Protocol (observed from Windows captures) This page documents the packet formats and ordering used by the T500RS when driven via the USB interrupt endpoint (0x01 OUT). It mirrors the style of docs/FFBEFFECTS.md for other wheels. Quick reference (Windows parity, at-a-glance) - EffectID rule: All 0x01 uploads and 0x41 START/STOP use EffectID=0x00 -- Subtypes per effect index n: param = 0x0e + 0x1c×n; envelope = 0x1c + 0x1c×n; second envelope = first + 0x1c -- Periodic 0x04 uses frequency (Hz×100), not period (ms) +- Command type codes (first byte on interrupt OUT endpoint 0x01): + - 0x01 = duration/control (main upload) + - 0x02 = envelope (attack/fade) + - 0x03 = constant force level + - 0x04 = periodic/ramp parameters + - 0x05 = condition (spring/damper/friction/inertia coefficients) + - 0x41 = START/STOP + - 0x42 = initialize/reset + - 0x43 = global gain/autocenter +- Effect type codes in 0x01 byte 2: + - 0x00 = constant + - 0x20 = square, 0x21 = triangle, 0x22 = sine + - 0x23 = sawtooth up, 0x24 = sawtooth down / ramp + - 0x40 = spring, 0x41 = damper/friction/inertia +- Subtypes per effect index n: param = 0x0e + 0x1c * n; envelope = 0x1c + 0x1c * n; second envelope = first + 0x1c +- Periodic 0x04 uses frequency (Hz*100), not period (ms) - Upload sequences (per effect): - - Constant: STOP → 0x02 → 0x01 → 0x02 → 0x03 → 0x01 → START + - Constant: STOP → 0x02 → 0x01 → 0x03 → START - Periodic (sine/square/triangle/saw): STOP → 0x02 → 0x01 → 0x02 → 0x04 → 0x01 → START - Ramp: STOP → 0x02 → 0x01 → 0x02 → 0x04 → 0x01 → START - Condition (spring/damper/friction/inertia): STOP → 0x05(set1) → 0x05(set2) → 0x01 → START @@ -17,9 +31,29 @@ Key rules - Little‑endian for 16‑bit fields - DMA‑safe buffer length 32 bytes; typical messages are 2, 4, 9 or 15 bytes - EffectID semantics (CRITICAL): - - All Report 0x01 main‑effect uploads MUST use EffectID = 0x00 on T500RS - - Report 0x41 START/STOP also uses EffectID = 0x00, except the init‑time STOP for autocenter which targets a fixed ID (15) - - Using per‑effect IDs with 0x01 breaks playback (constant force fails completely) + - All Report 0x01 main‑effect uploads MUST use EffectID = 0x00 on T500RS. + - Report 0x41 START/STOP also uses EffectID = 0x00, except the init‑time STOP for autocenter which targets a fixed ID (15). + - If you send non‑zero `effect_id` values on 0x01/0x41 the wheel does **not** reset or crash, but constant force effects produce **no torque at all** and other effects become unreliable. This matches our hardware tests and is why the driver hard‑codes `EffectID=0`. + +Why EffectID is always 0x00 on T500RS +------------------------------------- + +On the T300/TX/T248 family the `effect id` field in USB reports selects a +hardware effect slot: different IDs can be uploaded and started +independently. + +On T500RS, captures from the Windows driver look different: + +- Every Report 0x01 upload uses `effect_id = 0x00`. +- Every Report 0x41 START/STOP uses `effect_id = 0x00` as well, except the + init-time STOP that targets the built-in autocenter at a fixed ID (15). + +The wheel still runs several logical effects at the same time, but their +"slots" are encoded in the parameter/envelope subtypes +(`0x0e + 0x1c * n` / `0x1c + 0x1c * n`) instead of in `effect_id`. Trying to +use per-effect IDs on 0x01 breaks constant force completely, which is why the +Linux driver hard-codes `EffectID=0` for all uploads and START/STOP commands. + Special case — constant force subtypes @@ -33,17 +67,18 @@ Report glossary - 0x01 main upload (15 bytes) - Layout (unknown bytes kept for completeness): - b0 id = 0x01 - - b1 effect_id = 0x00 (MUST) + - b1 effect_id = 0x00 (MUST; see "EffectID semantics" above for rationale and failure modes) - b2 type: 0x00 constant; 0x20..0x24 periodic/ramp - b3 0x40 - - b4 0xff (or duration L for ramp path) - - b5 0xff (or duration H for ramp path) + - b4/b5: + - constant & periodic: 0xffff (effect runs until an explicit 0x41 STOP; tmff2 enforces `replay.length` in software) + - ramp: duration in ms (lo/hi), matching Windows captures - b6 0x00 - b7 0xff - b8 0xff - - b9 param subtype = 0x0e + 0x1c×index + - b9 param subtype = 0x0e + 0x1c * index - b10 0x00 - - b11 envelope subtype = 0x1c + 0x1c×index + - b11 envelope subtype = 0x1c + 0x1c * index - b12 0x00 - b13 0x00 - b14 0x00 @@ -60,7 +95,7 @@ Report glossary - b0 0x04, b1 0x0e, b2 0x00 - b3 magnitude u7 (0..127) - b4 offset = 0, b5 phase = 0 - - b6..7 frequency (le16, Hz×100) (e.g., 10 Hz → 0x03e8) + - b6..7 frequency (le16, Hz*100) (e.g., 10 Hz → 0x03e8) - 0x04 ramp params (9 bytes) - b0 0x04, b1 0x0e - b2..3 start (le16), b4..5 cur_val (le16), b6..7 duration (le16), b8 0x00 @@ -70,15 +105,61 @@ Report glossary - 0x40 config (4 bytes) - 0x40 0x04 enable/disable autocenter, 0x40 0x03 set autocenter strength, 0x40 0x11 set range (le16), etc. - 0x41 start/stop/clear (4 bytes) - - id=0x41, effect_id=0x00 (MUST), command=0x41 START or 0x00 STOP, arg=0x01 + - id=0x41, effect_id=0x00 (MUST; same rule as above — non‑zero IDs leave constant force mute/misbehaving), command=0x41 START or 0x00 STOP, arg=0x01 - Exception: init‑time STOP for autocenter uses effect_id=15 - 0x42 apply/apply‑like, 0x43 gain +Control and initialization commands (0x40/0x41/0x42/0x43) +-------------------------------------------------------- + +These small fixed-size packets control range, autocenter, gain and effect +lifecycle. Their exact values come from Windows USB captures but are stable +across games and control panel tests. + +- 0x40 config (4 bytes) + - General layout: `40 aa bb cc` + - Observed patterns: + - `40 04 00 00` — disable autocenter + - `40 04 01 00` — enable autocenter + - `40 03 xx 00` — set autocenter strength (`xx` = 0..100 in Linux; Windows uses 0..7f) + - `40 11 vv vv` — range / enable: + - On init: `40 11 42 7b` (magic value that enables FFB) + - For range changes: `40 11 value_lo value_hi` with `value = range_degrees * 60` (le16), + e.g. 360° → 0x5460 + - These commands do not involve `effect_id`; they configure the base. + +- 0x41 start/stop (4 bytes) + - Layout: `41 id cmd arg` + - `id` = effect slot (always 0 on T500RS, except autocenter stop which + uses 15) + - `cmd` = 0x00 STOP, 0x41 START + - `arg` = 1 (normal use on T500RS) + - Examples: + - `41 00 00 01` — STOP current effect on slot 0 (used before uploads) + - `41 00 41 01` — START current effect on slot 0 + - `41 0f 00 01` — STOP built-in autocenter at init time + +- 0x42 initialize/apply (2 bytes) + - Layout: `42 ss` + - `42 05` — simple "apply settings" helper: + - Sent once at init to bring the base into a known state + - Sent after changing autocenter or steering range + - No extra payload; `ss` selects the subcommand. + +- 0x43 gain helper (2 bytes) + - Layout: `43 gg` + - In init, Linux sends `43 ff` once to set maximum device gain. + - The FFB gain callback rescales Linux gain 0..65535 into one byte `gg` and + sends `43 gg`. + - Higher-level code still exposes the standard FFB gain control; this report + is how the hardware receives the gain value. + + Effect upload and play ordering - Subtype system (applies to 0x01/0x02/0x04/0x05 references) - Envelope subtype base = 0x1c; Parameter subtype base = 0x0e - - For effect index n: envelope = 0x1c + 0x1c×n; parameter = 0x0e + 0x1c×n + - For effect index n: envelope = 0x1c + 0x1c * n; parameter = 0x0e + 0x1c * n - In 0x01: b9 = parameter subtype; b11 = envelope subtype - Global (Windows parity) @@ -86,13 +167,15 @@ Effect upload and play ordering - Constant (FF_CONSTANT) 1) 0x41 STOP - 2) 0x02 envelope (first; subtype = 0x1c + 0x1c×index) - 3) 0x01 main (first; b9/b11 reference current param/envelope subtypes) - 4) 0x02 envelope (second; subtype += 0x1c) - 5) 0x03 level - 6) 0x01 main (second; update references) + 2) 0x02 envelope (subtype = 0x1c, fixed for constant) + 3) 0x01 main (b9=0x0e, b11=0x1c) + 4) 0x03 level Play: 0x41 START (effect_id=0x00) + Windows captures show an extra 0x02 + 0x01 pair at the end of the + sequence, but with constant's fixed subtypes the wheel behaves the same, + so the Linux driver uses the simpler 0x02 -> 0x01 -> 0x03 sequence. + - Periodic (FF_SINE/FF_SQUARE/FF_TRIANGLE/FF_SAW) 1) 0x41 STOP 2) 0x02 envelope (first) @@ -104,8 +187,8 @@ Effect upload and play ordering - Condition (FF_SPRING/FF_DAMPER/FF_FRICTION/FF_INERTIA) 1) 0x41 STOP - 2) 0x05 coeff/saturation (param subtype = 0x0e + 0x1c×index) - 3) 0x05 deadband/center (envelope subtype = 0x1c + 0x1c×index) + 2) 0x05 coeff/saturation (param subtype = 0x0e + 0x1c * index) + 3) 0x05 deadband/center (envelope subtype = 0x1c + 0x1c * index) 4) 0x01 main (type reflects subkind, effect_id=0x00; b9/b11 reference above) Play: 0x41 START (effect_id=0x00) @@ -118,6 +201,229 @@ Effect upload and play ordering 6) 0x01 main (second) Play: 0x41 START (effect_id=0x00) +Worked examples (packet-level) +------------------------------ + +The following example shows the constant-force upload and play sequence in the +same "bytes + comments" style as docs/FFBEFFECTS.md. Values are illustrative, +not the only valid ones, but they match the layouts described above and the +Linux driver implementation. + +### Example: FF_CONSTANT upload + START (effect index 0) + +1) Pre-upload STOP (clear slot 0): + +``` +41 00 00 01 +41 - report id (0x41) +00 - effect_id (slot 0, required on T500RS) +00 - command = STOP/CLEAR +01 - arg = 1 +``` + +2) Envelope (Report 0x02, first): + +``` +02 1c 00 00 00 00 00 00 00 +02 - report id +1c - envelope subtype for index 0 +00 - reserved +00 00 - attack_length +00 - attack_level +00 00 - fade_length +00 - fade_level +``` + +3) Main upload (Report 0x01, first): + +``` +01 00 00 40 ff ff 00 ff ff 0e 00 1c 00 00 00 +01 - id +00 - effect_id (slot 0) +00 - type = constant +40 - control byte (matches Windows driver) +ff ff 00 ff ff - duration/flags as observed on Windows +0e - parameter subtype (constant path) +00 - reserved +1c - envelope subtype +00 00 00 - padding / unused +``` + +4) Constant level (Report 0x03): + +``` +03 0e 00 40 +03 - id (constant level) +0e - code (links to constant subtype) +00 - reserved +40 - level (+64 in s8) +``` + +On Windows you can see an extra 0x01 with the same layout at the end of +the sequence, but with constant's fixed subtypes the Linux driver omits +that final 0x01 and just uses `0x02 -> 0x01 -> 0x03`. + +5) Play the effect (Report 0x41 START): + +``` +41 00 41 01 +41 - report id +00 - effect_id (slot 0) +41 - command = START +01 - arg = 1 +``` + +This demonstrates why the documentation insists on EffectID=0x00: the +per-effect index lives in the subtypes, while the effect slot itself is always +0 on T500RS. + +### Example: FF_SINE periodic (Control Panel "boing") + +Sequence taken from a Windows USB capture of the Control Panel "boing" +effect. It shows a sine-wave periodic effect with a slightly more complex +envelope and the dual-0x01 / dual-0x02 pattern. + +1) Envelope (index 0): + +``` +02 1c 00 95 00 3f e5 01 00 +02 - envelope command +1c - envelope subtype for index 0 +00 95 00 - attack length (24-bit LE, milliseconds; default used by Windows) +3f - attack level (~63/127) +e5 01 00 - fade length (24-bit LE, milliseconds) +00 - fade level +``` + +2) STOP before the main upload: + +``` +41 00 00 01 +``` + +3) Main upload (index 0, first): + +``` +01 00 22 40 bc 02 00 2c 01 0e 00 1c 00 00 00 +01 - main upload +00 - effect_id (slot 0) +22 - type = sine wave (see effect type table above) +40 - control/flags +bc 02 - duration (0x02bc ≈ 700 ms) +00 2c 01 - additional flags/fields as seen in captures +0e - parameter subtype for index 0 +00 - reserved +1c - envelope subtype for index 0 +00 00 00 - padding +``` + +4) Second envelope (index 1) and periodic params: + +``` +02 38 00 95 00 3f e5 01 00 +02 - envelope command +38 - envelope subtype for index 1 (0x1c + 0x1c * 1) +... + +04 2a 00 20 00 00 21 00 +04 - periodic params +2a - parameter subtype for index 1 (0x0e + 0x1c * 1) +00 - reserved +20 00 - magnitude (0x0020 → medium strength) +00 - offset +21 00 - frequency (0x0021 → ≈ 0.33 Hz, since value = Hz*100) +``` + +5) Main upload (index 1, second) and START: + +``` +01 01 22 40 bc 02 00 2c 01 2a 00 38 00 00 00 +... +41 01 41 01 +``` + +In Linux we always use `effect_id = 0` for the 0x01/0x41 packets, but keep the +same subtype pattern `(param = 0x0e + 0x1c*n, envelope = 0x1c + 0x1c*n)`. + +### Example: FF_RAMP (linear ramp) + +From a Windows USB capture of a linear ramp effect: + +1) Envelope: + +``` +02 1c 00 00 00 00 00 00 00 +``` + +2) Ramp parameters (Report 0x04): + +``` +04 0e 00 00 00 00 69 23 +04 - ramp/periodic command +0e - parameter subtype (index 0) +00 - reserved +00 00 - start level +00 00 - "current" level (same as start for a simple ramp) +69 23 - duration in ms (0x2369 ≈ 9.1 s) used by Windows +``` + +3) Main upload: + +``` +01 00 24 40 69 23 00 ff ff 0e 00 1c 00 00 00 +01 - main upload +00 - effect_id +24 - type = sawtooth down / ramp +40 - control/flags +69 23 - duration in ms; Linux fills this from `replay.length` for ramp, but uses 0xffff for constant/periodic and relies on a software timer + 0x41 STOP. +... +``` + +### Example: Condition spring (FF_SPRING) + +From a Windows USB capture of a spring effect (Control Panel test): + +1) Condition coefficients (Report 0x05, first packet): + +``` +05 0e 00 64 64 00 00 00 00 54 54 +05 - condition command +0e - parameter subtype (index 0) +00 - reserved +64 - positive coefficient (100) +64 - negative coefficient (100) +00 00 00 00 - reserved +54 54 - positive/negative saturation bytes (spring pattern) +``` + +2) Deadband/center (Report 0x05, second packet): + +``` +05 1c 00 00 00 00 00 00 00 46 54 +05 - condition command +1c - envelope subtype (index 0) +00 - reserved +00 00 - center (0) +00 00 00 - reserved +00 - deadband +46 54 - tail bytes; differ between spring/damper/friction/inertia in captures +``` + +3) Main upload and START: + +``` +01 00 40 40 17 25 00 ff ff 0e 00 1c 00 00 00 +01 - main upload, type 0x40 = spring +... +41 00 41 01 +``` + +This matches the general condition sequence described earlier: +STOP → 0x05 (coeff/sat) → 0x05 (deadband/center) → 0x01 → START, with the +per-effect index encoded only in the subtypes, not in `effect_id`. + + + Live update behavior (parameter‑only; Windows parity) @@ -132,8 +438,8 @@ Live update behavior (parameter‑only; Windows parity) - Periodic (FF_SINE/FF_SQUARE/FF_TRIANGLE/FF_SAW) - Send only 0x04 with new magnitude and frequency. - - Frequency field is Hz×100 (e.g., 10 Hz → 1000); offset/phase remain 0 unless changed. - - Use per‑effect param_sub = 0x0e + 0x1c×index. + - Frequency field is Hz*100 (e.g., 10 Hz → 1000); offset/phase remain 0 unless changed. + - Use per‑effect param_sub = 0x0e + 0x1c * index. - Condition (FF_SPRING/FF_DAMPER/FF_FRICTION/FF_INERTIA) - Send 0x05 (coeff/saturation) with param_sub, then 0x05 (deadband/center) with env_sub_first. @@ -144,10 +450,36 @@ Live update behavior (parameter‑only; Windows parity) - Send only 0x04 (start/cur_val/duration, last byte 0x00) using param_sub. - Device behavior approximates ramp via holding/stepping the start value over duration; matching observed Windows behavior. +Failure modes and common pitfalls +--------------------------------- + +These are the places where ignoring the rules above does not simply "do nothing" but leads to confusing behaviour on real hardware. + +- EffectID ≠ 0 on 0x01/0x41 + - Symptom: constant force produces **no torque at all** and other effects become unreliable. + - What still works: the wheel keeps enumerating and does not reset/crash; packets are accepted. + - Mitigation: always use `effect_id = 0x00` on T500RS for 0x01 uploads and 0x41 START/STOP. The driver hard‑codes this. + +- Changing constant subtypes (0x02/0x01/0x03) + - Symptom: 0x03 level updates stop affecting the rim; games appear to upload constants but you never feel them. + - Cause: T500RS uses a fixed linkage for constant (`0x02 subtype = 0x1c`, `0x01 b9 = 0x0e`, `0x01 b11 = 0x1c`, `0x03 code = 0x0e`). Using per‑effect subtypes breaks that linkage. + - Mitigation: treat the constant path as single‑slot and keep those subtype bytes exactly as documented. + +- Relying on hardware duration for constant/periodic + - Symptom: if you program a finite duration into 0x01 b4/b5 and **do not** send a 0x41 STOP from software, effects can keep running much longer than expected. + - Cause: captures show Windows using `0xffff` for constant/periodic and managing lifetimes via explicit STOP; non‑0xffff semantics are not documented and appear to be ignored or device‑specific. + - Mitigation: for constant/periodic, use `b4/b5 = 0xffff` and enforce `replay.length` in software with a STOP when your timer expires (the Linux driver follows this model). + +- Forgetting 0x42 05 after range/autocenter changes + - Symptom: new range or autocenter values sometimes do not take full effect until another operation happens. + - Cause: `0x42 05` acts as a generic "apply settings" trigger for previous 0x40 commands. + - Mitigation: after sending `0x40 11` (range) or `0x40 03/04` (autocenter), follow up with a `42 05` packet, as the driver does. + + Notes - All 0x01 uploads must use EffectID=0x00. This was validated on hardware; using per‑effect IDs causes constant force to fail completely and can break other effects. - 16‑bit values are little‑endian (lo byte first). -- Magnitudes/levels are expected in device ranges (0..127 for periodic magnitude, s8 for constant level). Scaling helpers in code perform conversions from Linux ranges. +- Magnitudes/levels are expected in device ranges (0..127 for periodic magnitude, s8 for constant level). Scaling helpers in code perform conversions from Linux ranges; for FF_CONSTANT the helper also folds `ff_effect.direction` into the signed level (projection onto the wheel axis) before sending 0x03. - Autocenter is disabled by zeroing spring coefficients via 0x05 messages and/or via 0x40 commands, then explicitly stopping autocenter (ID 15) during init. diff --git a/src/tmt500rs/hid-tmt500rs-usb.c b/src/tmt500rs/hid-tmt500rs-usb.c index 862aba2..2757378 100644 --- a/src/tmt500rs/hid-tmt500rs-usb.c +++ b/src/tmt500rs/hid-tmt500rs-usb.c @@ -53,7 +53,7 @@ struct t500rs_r04_periodic { u8 magnitude; /* 0..127 */ u8 offset; /* 0 */ u8 phase; /* 0 */ - __le16 period; /* frequency (Hz×100) */ + __le16 period; /* frequency (Hz*100) */ } __packed; struct t500rs_r04_ramp { @@ -91,45 +91,6 @@ struct t500rs_r01_main { u8 b14; } __packed; -/* Helper: classify whether a TX buffer is a known/managed report - * Known first bytes (report IDs / opcodes) we intentionally emit: - * 0x01,0x02,0x03,0x04,0x05,0x40,0x41,0x42,0x43,0x0a - */ -static inline int t500rs_is_known_tx(const unsigned char *data, size_t len) { - unsigned char r, s; - if (!data || !len) - return 1; - r = data[0]; - s = (len > 1) ? data[1] : 0; - switch (r) { - case 0x01: - return len == 15; /* main effect upload */ - case 0x02: - return len == 9 && s == 0x1c; /* envelope */ - case 0x03: - return len == 4 && s == 0x0e && - ((len > 2) ? data[2] == 0x00 : 0); /* const force level */ - case 0x04: - return (s == 0x0e) && (len == 8 || len == 9); /* periodic/ramp params */ - case 0x05: - return (len == 11) && (s == 0x0e || s == 0x1c) && - ((len > 2) ? data[2] == 0x00 : 0); - case 0x40: - return (len == 4) && (s == 0x03 || s == 0x04 || s == 0x08 || s == 0x11); - case 0x41: - return len == 4; /* play/stop/clear */ - case 0x42: - return ((len == 2) && (s == 0x05 || s == 0x04)) || - ((len == 15) && s == 0x01); - case 0x43: - return len == 2; /* global gain */ - case 0x0a: - return (len == 15) && s == 0x04; /* config */ - default: - return 0; - } -} - /* Scale envelope level (0..32767) to device 8-bit (0..255) */ static inline u8 t500rs_scale_env_level(u16 level) { if (level > 32767) @@ -146,6 +107,19 @@ static inline s8 t500rs_scale_const_level_s8(int level) { return (s8)((level * 127) / 32767); } +/* Apply effect direction to a constant level and convert to s8. + * Mirrors t300rs_calculate_constant_level()'s projection semantics but + * keeps the full T500RS range and uses t500rs_scale_const_level_s8() for + * clamping and conversion. + */ +static inline s8 t500rs_scale_const_with_direction(int level, u16 direction) { + int projected; + + projected = (level * fixp_sin16(direction * 360 / 0x10000)) / 0x7fff; + + return t500rs_scale_const_level_s8(projected); +} + /* Scale magnitude (0..32767 or signed) to 7-bit (0..127) */ static inline u8 t500rs_scale_mag_u7(int magnitude) { if (magnitude < 0) @@ -178,8 +152,8 @@ t500rs_fill_envelope_u02(u8 *buf, const struct ff_envelope *env, u8 subtype) { r->fade_lvl = f_lvl; } -/* Debug logging helper (requires local variable named 't500rs') */ -#define T500RS_DBG(fmt, ...) hid_dbg(t500rs->hdev, fmt, ##__VA_ARGS__) +/* Debug logging helper: pass struct t500rs_device_entry * explicitly */ +#define T500RS_DBG(dev, fmt, ...) hid_dbg((dev)->hdev, fmt, ##__VA_ARGS__) /* T500RS device data */ #define T500RS_MAX_EFFECTS 16 @@ -196,16 +170,6 @@ struct t500rs_device_entry { /* Current wheel range for smooth transitions */ u16 current_range; /* Current rotation range in degrees */ - - /* Cache last-sent condition coefficients per effect ID to avoid re-sending - * identical 0x05 (sub=0x0e) coefficient updates at high cadence. ACC spams - * condition updates with identical values at low speed, and reapplying them - * causes tactile rumble on T500RS. Zero-initialized via kzalloc. */ - u8 last_cond_rcoef[T500RS_MAX_EFFECTS]; - u8 last_cond_lcoef[T500RS_MAX_EFFECTS]; - u8 last_cond_b9[T500RS_MAX_EFFECTS]; - u8 last_cond_b10[T500RS_MAX_EFFECTS]; - u8 last_cond_valid[T500RS_MAX_EFFECTS]; }; /* Supported parameters */ @@ -293,7 +257,7 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, int level = effect->u.constant.level; /* Note: Gain is applied in play_effect, not here */ - T500RS_DBG("Upload constant: id=%d, level=%d\n", effect->id, level); + T500RS_DBG(t500rs, "Upload constant: id=%d, level=%d\n", effect->id, level); /* Pre-upload STOP to clear the slot (Windows parity) */ ret = t500rs_send_pre_stop(t500rs); @@ -318,7 +282,7 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, * cases like stopping autocenter by its fixed ID during init. * This matches behavior observed from the Windows driver and on-device tests. */ - /* Report 0x01 - Main effect upload - MATCH WINDOWS DRIVER EXACTLY! */ + /* Report 0x01 - Main effect upload (constant, fixed subtypes) */ { struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; memset(m, 0, sizeof(*m)); @@ -326,8 +290,8 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, m->effect_id = 0x00; /* Device expects Effect ID 0 for 0x01 on T500RS */ m->type = 0x00; /* Constant force type */ m->b3 = 0x40; - m->b4 = 0xff; /* Windows uses 0xff (was 0x69) */ - m->b5 = 0xff; /* Windows uses 0xff (was 0x23) */ + m->b4 = 0xff; /* Windows uses 0xff */ + m->b5 = 0xff; /* Windows uses 0xff */ m->b6 = 0x00; m->b7 = 0xff; m->b8 = 0xff; @@ -345,17 +309,9 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, return ret; } - /* Report 0x02 - Envelope (second; subtype = first + 0x1c) */ - t500rs_fill_envelope_u02(buf, &effect->u.constant.envelope, 0x1c); - ret = t500rs_send_usb(t500rs, buf, 9); - if (ret) { - hid_err(t500rs->hdev, "Failed to send Report 0x02 (second): %d\n", ret); - return ret; - } - - /* Report 0x03 - Constant force level (param subtype) */ + /* Report 0x03 - Constant force level (param subtype, honours direction) */ { - s8 signed_level = t500rs_scale_const_level_s8(level); + s8 signed_level = t500rs_scale_const_with_direction(level, effect->direction); struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; r3->id = 0x03; r3->code = 0x0e; @@ -369,34 +325,8 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, return ret; } - /* Report 0x01 - Main effect upload (second; references second envelope - * subtype) */ - { - struct t500rs_r01_main *m = (struct t500rs_r01_main *)buf; - memset(m, 0, sizeof(*m)); - m->id = 0x01; - m->effect_id = 0x00; /* Device expects Effect ID 0 for 0x01 on T500RS */ - m->type = 0x00; /* Constant force type */ - m->b3 = 0x40; - m->b4 = 0xff; /* Windows uses 0xff (was 0x69) */ - m->b5 = 0xff; /* Windows uses 0xff (was 0x23) */ - m->b6 = 0x00; - m->b7 = 0xff; - m->b8 = 0xff; - m->b9 = 0x0e; /* Parameter subtype reference (fixed) */ - m->b10 = 0x00; - m->b11 = 0x1c; /* Envelope subtype reference (fixed) */ - m->b12 = 0x00; - m->b13 = 0x00; - m->b14 = 0x00; - } - ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r01_main)); - if (ret) { - hid_err(t500rs->hdev, "Failed to send Report 0x01 (second): %d\n", ret); - return ret; - } - - T500RS_DBG("Constant effect %d uploaded (dual 0x02 + dual 0x01 sequence)\n", + T500RS_DBG(t500rs, + "Constant effect %d uploaded (0x02 + 0x01 + 0x03 sequence)\n", effect->id); return 0; } @@ -451,7 +381,8 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, right_strength = (right_strength * 127) / 65535; left_strength = (left_strength * 127) / 65535; - T500RS_DBG("Upload condition: id=%d, type=0x%02x, gain=%u%%, R=%d, L=%d\n", + T500RS_DBG(t500rs, + "Upload condition: id=%d, type=0x%02x, gain=%u%%, R=%d, L=%d\n", effect->id, effect_type, effect_gain, right_strength, left_strength); @@ -463,13 +394,13 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, } /* Report 0x05 - Condition parameters (coefficients) */ - memset(buf, 0, 15); + memset(buf, 0, 11); buf[0] = 0x05; buf[1] = param_sub; buf[2] = 0x00; buf[3] = (u8)right_strength; buf[4] = (u8)left_strength; - buf[5] = 0x00; + buf[5] = 0x00;I buf[6] = 0x00; buf[7] = 0x00; buf[8] = 0x00; @@ -480,7 +411,7 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, return ret; /* Report 0x05 - Condition parameters (deadband/center) */ - memset(buf, 0, 15); + memset(buf, 0, 11); buf[0] = 0x05; buf[1] = env_sub_first; buf[2] = 0x00; @@ -572,7 +503,7 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, u8 env_sub_first = (u8)(0x1c + (0x1c * idx)); u8 env_sub_second = (u8)(env_sub_first + 0x1c); - /* Period (ms) -> device frequency (Hz×100). Default to 100ms = 10 Hz if unset */ + /* Period (ms) -> device frequency (Hz*100). Default to 100ms = 10 Hz if set to 0 */ if (period == 0) { period = 100; } @@ -582,8 +513,9 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, freq_hz100 = 1U; if (freq_hz100 > 65535U) freq_hz100 = 65535U; - T500RS_DBG("Upload %s: id=%d, magnitude=%d (0x%02x), period=%dms -> " - "freq=%u (Hz×100)\n", + T500RS_DBG(t500rs, + "Upload %s: id=%d, magnitude=%d (0x%02x), period=%dms -> " + "freq=%u (Hz*100)\n", type_name, effect->id, magnitude, mag, period, (unsigned)freq_hz100); /* Reuse 'period' variable to carry converted frequency to the packet write below */ @@ -685,7 +617,7 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, return ret; } - T500RS_DBG("%s effect %d uploaded\n", type_name, effect->id); + T500RS_DBG(t500rs, "%s effect %d uploaded\n", type_name, effect->id); return 0; } @@ -709,7 +641,8 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, /* Scale to 0-255 */ start_scaled = (abs(start_level) * 0xff) / 32767; - T500RS_DBG("Upload ramp: id=%d, start=%d, end=%d, duration=%dms\n", + T500RS_DBG(t500rs, + "Upload ramp: id=%d, start=%d, end=%d, duration=%dms\n", effect->id, start_level, end_level, duration_ms); /* Pre-upload STOP to clear the slot (Windows parity) */ @@ -807,7 +740,9 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, return ret; } - T500RS_DBG("Ramp effect %d uploaded (dual 0x02 + dual 0x01)\n", effect->id); + T500RS_DBG(t500rs, + "Ramp effect %d uploaded (dual 0x02 + dual 0x01)\n", + effect->id); return 0; } @@ -848,17 +783,22 @@ static int t500rs_play_effect(void *data, if (!t500rs) return -ENODEV; - T500RS_DBG("Play effect: id=%d, type=0x%02x (FF_CONSTANT=0x%02x)\n", + T500RS_DBG(t500rs, + "Play effect: id=%d, type=0x%02x (FF_CONSTANT=0x%02x)\n", effect->id, effect->type, FF_CONSTANT); - /* For constant force: send one level update (0x03) then START (0x41) */ + /* For constant force: send one level update (0x03) then START (0x41). + * Apply direction before scaling to s8. + */ if (effect->type == FF_CONSTANT) { int level = effect->u.constant.level; + u16 direction = effect->direction; s8 signed_level; - signed_level = t500rs_scale_const_level_s8(level); + signed_level = t500rs_scale_const_with_direction(level, direction); - T500RS_DBG("Constant force: level=%d -> %d (0x%02x)\n", level, signed_level, - (u8)signed_level); + T500RS_DBG(t500rs, + "Constant force: level=%d dir=%u -> %d (0x%02x)\n", level, + direction, signed_level, (u8)signed_level); /* Send Report 0x03 (force level) */ { @@ -873,19 +813,9 @@ static int t500rs_play_effect(void *data, hid_err(t500rs->hdev, "Failed to send Report 0x03: %d\n", ret); return ret; } - - /* Send Report 0x41 START */ - { - struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)buf; - r41->id = 0x41; - r41->effect_id = 0x00; - r41->command = 0x41; - r41->arg = 0x01; - } - return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); } - /* For other effect types, send start command - Report 0x41 + /* Send start command - Report 0x41 * T500RS expects EffectID=0 for 0x41 commands as well. */ { @@ -896,7 +826,8 @@ static int t500rs_play_effect(void *data, r41->arg = 0x01; } - T500RS_DBG("Sending START command (EffectID=0) for effect %d\n", effect->id); + T500RS_DBG(t500rs, + "Sending START command (EffectID=0) for effect %d\n", effect->id); return t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); } @@ -918,7 +849,7 @@ static int t500rs_stop_effect(void *data, return -ENOMEM; } - T500RS_DBG("Stop effect: id=%d, type=%d\n", state->effect.id, + T500RS_DBG(t500rs, "Stop effect: id=%d, type=%d\n", state->effect.id, state->effect.type); /* For constant force: Windows-style STOP (0x41 00 00 01) */ @@ -945,7 +876,7 @@ static int t500rs_stop_effect(void *data, } ret = t500rs_send_usb(t500rs, buf, sizeof(struct t500rs_r41_cmd)); - T500RS_DBG("Stop effect (non-constant) returned: %d\n", ret); + T500RS_DBG(t500rs, "Stop effect (non-constant) returned: %d\n", ret); return ret; } @@ -954,6 +885,7 @@ static int t500rs_update_effect(void *data, const struct tmff2_effect_state *state) { struct t500rs_device_entry *t500rs = data; const struct ff_effect *effect = &state->effect; + const struct ff_effect *old = &state->old; u8 *buf; if (!t500rs) @@ -967,7 +899,8 @@ static int t500rs_update_effect(void *data, switch (effect->type) { case FF_CONSTANT: { int level = effect->u.constant.level; - s8 signed_level = t500rs_scale_const_level_s8(level); + u16 direction = effect->direction; + s8 signed_level = t500rs_scale_const_with_direction(level, direction); struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; r3->id = 0x03; r3->code = 0x0e; @@ -1025,9 +958,13 @@ static int t500rs_update_effect(void *data, case FF_INERTIA: { unsigned int idx = (unsigned int)effect->id; u8 param_sub = (u8)(0x0e + (0x1c * idx)); - u8 effect_gain; - int right_strength = effect->u.condition[0].right_saturation; - int left_strength = effect->u.condition[0].left_saturation; + const struct ff_condition_effect *cond = &effect->u.condition[0]; + const struct ff_condition_effect *cond_old = &old->u.condition[0]; + u8 effect_gain, effect_gain_old; + int right_strength, left_strength; + int right_strength_old, left_strength_old; + u8 rcoef, lcoef, rcoef_old, lcoef_old; + u8 b9, b10, b9_old, b10_old; switch (effect->type) { case FF_SPRING: effect_gain = spring_level; break; @@ -1037,56 +974,60 @@ static int t500rs_update_effect(void *data, default: effect_gain = 100; break; } - right_strength = (right_strength * effect_gain) / 100; - left_strength = (left_strength * effect_gain) / 100; + switch (old->type) { + case FF_SPRING: effect_gain_old = spring_level; break; + case FF_DAMPER: effect_gain_old = damper_level; break; + case FF_FRICTION: effect_gain_old = friction_level; break; + case FF_INERTIA: effect_gain_old = 100; break; + default: effect_gain_old = 100; break; + } + + right_strength = (cond->right_saturation * effect_gain) / 100; + left_strength = (cond->left_saturation * effect_gain) / 100; + right_strength_old = (cond_old->right_saturation * effect_gain_old) / 100; + left_strength_old = (cond_old->left_saturation * effect_gain_old) / 100; - right_strength = (right_strength * 127) / 65535; - left_strength = (left_strength * 127) / 65535; + right_strength = (right_strength * 127) / 65535; + left_strength = (left_strength * 127) / 65535; + right_strength_old = (right_strength_old * 127) / 65535; + left_strength_old = (left_strength_old * 127) / 65535; + + rcoef = (u8)right_strength; + lcoef = (u8)left_strength; + rcoef_old = (u8)right_strength_old; + lcoef_old = (u8)left_strength_old; + + b9 = (effect->type == FF_SPRING) ? 0x54 : 0x64; + b10 = (effect->type == FF_SPRING) ? 0x54 : 0x64; + b9_old = (old->type == FF_SPRING) ? 0x54 : 0x64; + b10_old = (old->type == FF_SPRING) ? 0x54 : 0x64; /* * Rationale: ACC (and similar) may spam condition updates at low speed with * the exact same parameters. Re-sending 0x05 (sub=0x0e) at high cadence * makes T500RS micro-pulse/rumble. Also, sending 0x05 (sub=0x1c) on update * is unnecessary when deadband/center haven't changed and can exacerbate - * the issue. Therefore: - * - Only send 0x05 (sub=0x0e) when coefficients actually change - * - Skip 0x05 (sub=0x1c) entirely on updates + * the issue. Therefore we derive device coefficients from state->effect and + * state->old and only send 0x05 (sub=0x0e) when they differ, skipping + * 0x05 (sub=0x1c) entirely on updates. */ - { - u8 rcoef = (u8)right_strength; - u8 lcoef = (u8)left_strength; - u8 b9 = (effect->type == FF_SPRING) ? 0x54 : 0x64; - u8 b10 = (effect->type == FF_SPRING) ? 0x54 : 0x64; - - if (idx < T500RS_MAX_EFFECTS && t500rs->last_cond_valid[idx] && - t500rs->last_cond_rcoef[idx] == rcoef && - t500rs->last_cond_lcoef[idx] == lcoef && - t500rs->last_cond_b9[idx] == b9 && - t500rs->last_cond_b10[idx] == b10) { - /* Coefficients unchanged: no-op (avoid rumble) */ - return 0; - } - - memset(buf, 0, 15); - buf[0] = 0x05; - buf[1] = param_sub; - buf[2] = 0x00; - buf[3] = rcoef; - buf[4] = lcoef; - buf[5] = buf[6] = buf[7] = buf[8] = 0x00; - buf[9] = b9; - buf[10] = b10; - if (t500rs_send_usb(t500rs, buf, 11)) - return -EIO; - - if (idx < T500RS_MAX_EFFECTS) { - t500rs->last_cond_rcoef[idx] = rcoef; - t500rs->last_cond_lcoef[idx] = lcoef; - t500rs->last_cond_b9[idx] = b9; - t500rs->last_cond_b10[idx] = b10; - t500rs->last_cond_valid[idx] = 1; - } - } + if (rcoef == rcoef_old && + lcoef == lcoef_old && + b9 == b9_old && + b10 == b10_old) + return 0; + + memset(buf, 0, 11); + buf[0] = 0x05; + buf[1] = param_sub; + buf[2] = 0x00; + buf[3] = rcoef; + buf[4] = lcoef; + buf[5] = buf[6] = buf[7] = buf[8] = 0x00; + buf[9] = b9; + buf[10] = b10; + if (t500rs_send_usb(t500rs, buf, 11)) + return -EIO; /* Skip 0x05 (sub=0x1c) on updates by design; see rationale above. */ return 0; @@ -1184,7 +1125,7 @@ static int t500rs_set_range(void *data, u16 range) { if (!buf) return -ENOMEM; - T500RS_DBG("Setting wheel range to %u degrees\n", range); + T500RS_DBG(t500rs, "Setting wheel range to %u degrees\n", range); /* Device expects LITTLE-ENDIAN and value = range * 60. */ range_value = range * 60; @@ -1213,7 +1154,8 @@ static int t500rs_set_range(void *data, u16 range) { return ret; } - T500RS_DBG("Range set to %u degrees (final value=0x%04x)\n", range, + T500RS_DBG(t500rs, + "Range set to %u degrees (final value=0x%04x)\n", range, range_value); return 0; @@ -1276,7 +1218,7 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { ep = &t500rs->usbif->cur_altsetting->endpoint[1]; t500rs->ep_out = ep->desc.bEndpointAddress; - T500RS_DBG("Found INTERRUPT OUT endpoint: 0x%02x\n", t500rs->ep_out); + T500RS_DBG(t500rs, "Found INTERRUPT OUT endpoint: 0x%02x\n", t500rs->ep_out); /* Allocate send buffer */ t500rs->buffer_length = T500RS_BUFFER_LENGTH; @@ -1295,51 +1237,24 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { /* Use send_buffer for all USB transfers (DMA-safe) */ init_buf = t500rs->send_buffer; - T500RS_DBG("Sending initialization sequence...\n"); + T500RS_DBG(t500rs, "Sending initialization sequence...\n"); - /* Report 0x42 - Init (15 bytes) */ - memset(init_buf, 0, 15); + /* Report 0x42 - Apply/init (2 bytes) + * Minimal "initialize/apply" command observed as 0x42 0x05 in Windows + * captures. Send once at startup to bring the base into a known state + * before FFB uploads. + */ + memset(init_buf, 0, 2); init_buf[0] = 0x42; - init_buf[1] = 0x01; - ret = t500rs_send_usb(t500rs, init_buf, 15); - if (ret) { - hid_warn(t500rs->hdev, "Init command 1 (0x42) failed: %d\n", ret); - } - - /* Report 0x0a - Config 1 (15 bytes) */ - memset(init_buf, 0, 15); - init_buf[0] = 0x0a; - init_buf[1] = 0x04; - init_buf[2] = 0x90; - init_buf[3] = 0x03; - ret = t500rs_send_usb(t500rs, init_buf, 15); - if (ret) { - hid_warn(t500rs->hdev, "Init command 2 (0x0a config1) failed: %d\n", ret); - } - - /* Report 0x0a - Config 2 (15 bytes) */ - memset(init_buf, 0, 15); - init_buf[0] = 0x0a; - init_buf[1] = 0x04; - init_buf[2] = 0x12; - init_buf[3] = 0x10; - ret = t500rs_send_usb(t500rs, init_buf, 15); - if (ret) { - hid_warn(t500rs->hdev, "Init command 3 (0x0a config2) failed: %d\n", ret); - } - - /* Report 0x0a - Config 3 (15 bytes) */ - memset(init_buf, 0, 15); - init_buf[0] = 0x0a; - init_buf[1] = 0x04; - init_buf[2] = 0x00; - init_buf[3] = 0x06; - ret = t500rs_send_usb(t500rs, init_buf, 15); + init_buf[1] = 0x05; + ret = t500rs_send_usb(t500rs, init_buf, 2); if (ret) { - hid_warn(t500rs->hdev, "Init command 4 (0x0a config3) failed: %d\n", ret); + hid_warn(t500rs->hdev, "Init command 1 (0x42 0x05) failed: %d\n", ret); } - /* Report 0x40 - Enable FFB (4 bytes) */ + /* Report 0x40 - Enable FFB (4 bytes) + * Magic value seen in captures that enables FFB on the base. + */ memset(init_buf, 0, 4); init_buf[0] = 0x40; init_buf[1] = 0x11; @@ -1347,79 +1262,41 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { init_buf[3] = 0x7b; ret = t500rs_send_usb(t500rs, init_buf, 4); if (ret) { - hid_warn(t500rs->hdev, "Init command 5 (0x40 enable) failed: %d\n", ret); - } - - /* Report 0x42 - Additional init (2 bytes) */ - memset(init_buf, 0, 2); - init_buf[0] = 0x42; - init_buf[1] = 0x04; - ret = t500rs_send_usb(t500rs, init_buf, 2); - if (ret) { - hid_warn(t500rs->hdev, "Init command 6 (0x42) failed: %d\n", ret); + hid_warn(t500rs->hdev, "Init command 2 (0x40 enable) failed: %d\n", ret); } - /* Report 0x40 - Config (4 bytes) */ + /* Report 0x40 - Disable built-in autocenter (4 bytes) */ memset(init_buf, 0, 4); init_buf[0] = 0x40; init_buf[1] = 0x04; - /* b2..b3 = 0x0000 -> disable autocenter (Windows parity). - * Keep explicit zeros even though memset() clears them, to document the wire image. */ + /* b2..b3 = 0x0000 -> disable autocenter. + * Keep explicit zeros even though memset() clears them, to document the + * wire image. + */ init_buf[2] = 0x00; init_buf[3] = 0x00; ret = t500rs_send_usb(t500rs, init_buf, 4); if (ret) { - hid_warn(t500rs->hdev, "Init command 7 (0x40 config) failed: %d\n", ret); + hid_warn(t500rs->hdev, "Init command 3 (0x40 config) failed: %d\n", ret); } - /* Report 0x43 - Set global gain (2 bytes) */ + /* Report 0x43 - Set global gain (2 bytes) + * Start at maximum device gain; the FFB gain callback will adjust later. + */ memset(init_buf, 0, 2); init_buf[0] = 0x43; init_buf[1] = 0xFF; ret = t500rs_send_usb(t500rs, init_buf, 2); if (ret) { - hid_warn(t500rs->hdev, "Init command 8 (0x43) failed: %d\n", ret); - } - - /* Report 0x41 - Clear effects (4 bytes) */ - { - struct t500rs_r41_cmd *r41 = (struct t500rs_r41_cmd *)init_buf; - r41->id = 0x41; - r41->effect_id = 0x00; - r41->command = 0x00; - r41->arg = 0x00; - } - ret = t500rs_send_usb(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); - if (ret) { - hid_warn(t500rs->hdev, "Init command 9 (0x41 clear) failed: %d\n", ret); + hid_warn(t500rs->hdev, "Init command 4 (0x43) failed: %d\n", ret); } - /* Report 0x40 - Finalization (4 bytes) */ - memset(init_buf, 0, 4); - init_buf[0] = 0x40; - init_buf[1] = 0x08; - /* b2..b3 must be 0x0000 (reserved); explicit for clarity / Windows parity */ - init_buf[2] = 0x00; - init_buf[3] = 0x00; - ret = t500rs_send_usb(t500rs, init_buf, 4); - if (ret) { - hid_warn(t500rs->hdev, "Init command 10 (0x40 final) failed: %d\n", ret); - } + /* The remaining initialization (0x05 spring zeroing and 0x41 STOP for + * autocenter ID 15) is handled below. + */ - /* Report 0x40 - Set mode (4 bytes) */ - memset(init_buf, 0, 4); - init_buf[0] = 0x40; - init_buf[1] = 0x03; - init_buf[2] = 0x0d; - /* b3 reserved = 0x00 (Windows parity) */ - init_buf[3] = 0x00; - ret = t500rs_send_usb(t500rs, init_buf, 4); - if (ret) { - hid_warn(t500rs->hdev, "Init command 11 (0x40 mode) failed: %d\n", ret); - } - /* Report 0x05 - Set deadband and center */ - memset(init_buf, 0, 15); + memset(init_buf, 0, 11); init_buf[0] = 0x05; init_buf[1] = 0x1c; init_buf[2] = 0x00; @@ -1444,12 +1321,12 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { if (ret) { hid_warn(t500rs->hdev, "Stop autocenter effect failed: %d\n", ret); } else { - T500RS_DBG("Autocenter fully disabled\n"); + T500RS_DBG(t500rs, "Autocenter fully disabled\n"); } hid_info(t500rs->hdev, "T500RS initialized successfully (USB INTERRUPT mode)\n"); - T500RS_DBG("Endpoint: 0x%02x, Buffer: %zu bytes\n", t500rs->ep_out, + T500RS_DBG(t500rs, "Endpoint: 0x%02x, Buffer: %zu bytes\n", t500rs->ep_out, t500rs->buffer_length); /* Advertise capabilities now that init succeeded */ @@ -1474,7 +1351,7 @@ static int t500rs_wheel_destroy(void *data) { if (!t500rs) return 0; - T500RS_DBG("T500RS: Cleaning up\n"); + T500RS_DBG(t500rs, "T500RS: Cleaning up\n"); /* Free resources */ kfree(t500rs->send_buffer);