From fbd63f49f50908586fe6d57fbffe8335813b8b0d Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Tue, 4 Nov 2025 01:13:42 +0100 Subject: [PATCH 01/15] hid: tmff2: add Thrustmaster T500RS wheel base driver Add support for the Thrustmaster T500RS wheel base in the hid-tmff2 driver. This driver has been built from scratch based the previous dirty driver (see https://github.com/Kimplul/hid-tmff2/pull/175) on captures from ffbsdl tool ran in windows for all possible effects (through SDL2 library). --- Kbuild | 6 +- Makefile | 24 + docs/T500RS_USB_Protocol_Analysis.md | 663 ++++++++ src/hid-tmff2.c | 99 +- src/hid-tmff2.h | 10 + src/tmt500rs/hid-tmt500rs.c | 2073 ++++++++++++++++++++++++++ src/tmt500rs/t500rs_protocol.h | 222 +++ udev/71-thrustmaster-steamdeck.rules | 3 + udev/99-thrustmaster.rules | 4 + 9 files changed, 3096 insertions(+), 8 deletions(-) create mode 100644 docs/T500RS_USB_Protocol_Analysis.md create mode 100644 src/tmt500rs/hid-tmt500rs.c create mode 100644 src/tmt500rs/t500rs_protocol.h diff --git a/Kbuild b/Kbuild index 8cabb2b4..d1b57c14 100644 --- a/Kbuild +++ b/Kbuild @@ -5,4 +5,8 @@ hid-tmff-new-y := \ src/tmt248/hid-tmt248.o \ src/tmtx/hid-tmtx.o \ src/tmtsxw/hid-tmtsxw.o \ - src/tmtspc/hid-tmtspc.o + src/tmtspc/hid-tmtspc.o \ + src/tmt500rs/hid-tmt500rs.o + +# Pass through the global TMFF2 version define from Makefile +ccflags-y += $(TMFF2_VERSION_DEF) diff --git a/Makefile b/Makefile index ad526211..11487feb 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,30 @@ KDIR ?= /lib/modules/$(shell uname -r)/build +# Auto-generated global build-time version for TMFF2 +TMFF2_BASE_VERSION ?= 0.1 + +# 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) +export TMFF2_VERSION_DEF := -DTMFF2_DRIVER_VERSION=\"$(TMFF2_VERSION)\" + + all: deps/hid-tminit + @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/T500RS_USB_Protocol_Analysis.md b/docs/T500RS_USB_Protocol_Analysis.md new file mode 100644 index 00000000..84edfd3b --- /dev/null +++ b/docs/T500RS_USB_Protocol_Analysis.md @@ -0,0 +1,663 @@ +# T500RS USB Force Feedback Protocol Analysis +## Comprehensive Effect Implementation Reference +This is the result of the deep analysis of captures made using ffbsdl tool on windows and the implementation iterations to get a working driver supporting (hopefully) all effects on-par with windows official driver. + +--- + +## USB Packet Types Overview + +| Packet ID | Name | Size | Purpose | +|-----------|------|------|---------| +| 0x01 | Main Upload | 15 bytes | Effect type, direction, duration, packet codes | +| 0x02 | Envelope | 9 bytes | Attack/fade parameters (limited support) | +| 0x03 | Constant Force | 4 bytes | Force level for constant effects | +| 0x04 | Periodic/Ramp | 8 bytes | Magnitude, offset, phase, period | +| 0x05 | Conditional | 11 bytes | Spring/damper/inertia/friction parameters | +| 0x41 | Command | 4 bytes | START (0x41) / STOP (0x00) | +| 0x42 | Status/Control | 2-16 bytes | Device status queries (0x00, 0x04, 0x05) | +| 0x43 | Init/Config | 64 bytes | Device initialization | +| 0x49 | Polling | 7-16 bytes | Status polling (high frequency) | +| 0x07 | Telemetry | 15 bytes | Position feedback (high frequency) | + +**Note:** Envelope support (0x02) is limited on T500RS hardware. Non-zero envelope values cause EPROTO errors on periodic and constant effects. Always send zeros for envelope parameters on these effect types. + +--- + +## Subtype System and Effect Indexing + +On this wheel the last six bytes of the 0x01 main upload (bytes 9–14) do **not** contain envelope timings/levels directly. Instead they carry two 16‑bit "subtype" values that act as per‑effect indices: + +- Bytes 9–10 → `parameter_subtype` +- Bytes 11–12 → `envelope_subtype` +- Bytes 13–14 → padding (always 0x0000 in captures) + +These subtype values are then copied into the "code" / "subtype" field of other packets (0x02, 0x03, 0x04, 0x05) so the device can associate parameter/envelope packets with a particular logical effect. + +For effect index **n** (0‑based) the wheel uses a simple arithmetic progression: + +- `parameter_subtype = 0x000e + 0x001c * n` +- `envelope_subtype = 0x001c + 0x001c * n` + +Observed pairs from captures: + +| Effect index n | parameter_subtype | envelope_subtype | +|----------------|-------------------|------------------| +| 0 | 0x000e | 0x001c | +| 1 | 0x002a | 0x0038 | +| 2 | 0x0046 | 0x0054 | +| 3 | 0x0062 | 0x0070 | +| 4 | 0x007e | 0x008c | +| 5 | 0x009a | 0x00a8 | +| 6 | 0x00b6 | 0x00c4 | + +Implications for the driver: + +- `effect_id` (byte 1 of 0x01) stays **0x00** for normal uploads; logical effect slots are selected purely via these subtype pairs. +- The same subtype values appear in: + - 0x02 envelope packets (`subtype = envelope_subtype`) + - 0x03 constant packets (`code = parameter_subtype`) + - 0x04 periodic/ramp packets (`code = parameter_subtype`) + - 0x05 condition packets (`code = parameter_subtype` for the first, `code = envelope_subtype` for the second) + +Envelope attack/fade length and level values themselves live **only** in the 0x02 packets; bytes 9–14 of 0x01 are *references* to those blocks, not the envelope parameters. + +--- + +## Packet Structure Details + +### 0x01 - Main Upload Packet (15 bytes) +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- + 0 | 1 | packet_type | 0x01 + 1 | 1 | effect_id | Hardware effect slot ID (0-15, assigned by driver) + 2 | 1 | effect_type | Effect type (0x00=constant, 0x22=sine, 0x40=conditional) + 3 | 1 | control | Always 0x40 + 4 | 2 | duration_ms | Duration in milliseconds, little-endian + 6 | 2 | delay_ms | Delay before start, little-endian + 8 | 1 | reserved1 | 0x00 + 9 | 2 | packet_code_1 | Code for subsequent packet type (variable!) +11 | 2 | packet_code_2 | Code for second subsequent packet (variable!) +13 | 2 | reserved2 | 0x0000 +``` + +**Driver Implementation Note:** effect_id must be unique for concurrent effects to prevent slot collision. Use hardware ID allocation (0-15) instead of always 0x00. + +**IMPORTANT:** Bytes 9-12 specify the packet codes used in subsequent packets. These are NOT fixed values! + +**Common Code Combinations:** +- Constant effects: bytes 9-10 = 0x000e (for 0x03 packet), bytes 11-12 = 0x001c (envelope) +- Periodic effects: bytes 9-10 = 0x002a (for 0x04 packet), bytes 11-12 = 0x001c (envelope) +- Conditional effects: bytes 9-10 = 0x002a (for first 0x05 packet), bytes 11-12 = 0x0038 (for second 0x05 packet) +- Alternative codes observed: 0x00b6/0x00c4 (newer captures), 0x0046/0x0054, 0x0062/0x0070, 0x007e/0x008c, 0x009a/0x00a8 + +**Examples:** +- `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00` - Constant effect with envelope + - Effect ID: 0x00 + - Effect type: 0x00 (constant) + - Control: 0x40 + - Duration: 0x01f4 = 500ms + - Delay: 0x0000 = 0ms + - Reserved1: 0x00 + - Packet codes: 0x000e (constant), 0x001c (envelope) + - Reserved2: 0x0000 + +- `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` - Conditional effect + - Effect ID: 0x00 + - Effect type: 0x40 (conditional) + - Control: 0x40 + - Duration: 0x07d0 = 2000ms + - Delay: 0x0000 = 0ms + - Reserved1: 0x00 + - Packet codes: 0x002a (first conditional), 0x0038 (second conditional) + - Reserved2: 0x0000 + +### 0x02 - Envelope Packet (9 bytes) +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x02 +1 | 1 | subtype | 0x1c +2 | 2 | attack_len_ms | Attack duration in ms, little-endian +4 | 1 | attack_level | Attack level 0-255 +5 | 2 | fade_len_ms | Fade duration in ms, little-endian +7 | 1 | fade_level | Fade level 0-255 +8 | 1 | reserved | 0x00 +``` + +**Example:** `02 1c 00 00 12 00 00 12 00` +- Attack: 0ms, level 18 +- Fade: 0ms, level 18 + +**IMPORTANT FIRMWARE LIMITATION:** +Windows driver always sends zeros for envelope on periodic and constant effects: +`02 38 00 00 00 00 00 00 00` + +Non-zero envelope values cause EPROTO (-71) on subsequent packets. This appears +to be a firmware bug - the device does not properly support envelope parameters +for these effect types. The Linux driver must also send zeros to avoid crashes. + +### 0x03 - Constant Force Packet (4 bytes) +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x03 +1 | 1 | code | 0x0e +2 | 1 | reserved | 0x00 +3 | 1 | level | Signed -127 to +127 +``` + +**Examples:** +- `03 0e 00 00` - Level 0 (no force) +- `03 0e 00 09` - Level 9 (weak positive) +- `03 0e 00 f9` - Level -7 (0xf9 = -7 signed, weak negative) + +### 0x04 - Periodic/Ramp Packet (8 bytes) +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x04 +1 | 1 | code | Variable (from 0x01 packet bytes 9-10) +2 | 1 | magnitude | 0-127 (effect strength) +3 | 1 | offset | Signed -127 to +127 (DC offset) +4 | 1 | phase | 0-255 (0-360 degrees, 256 steps) +5 | 2 | period_ms | Period in milliseconds, little-endian +7 | 1 | reserved | 0x00 +``` + +**Code Values:** The code in byte 1 matches bytes 9-10 of the 0x01 packet (0x2a, 0xb6, 0x46, etc.) + +**Period Encoding:** Period is in MILLISECONDS (not Hz×100). No conversion needed. + +**Examples:** +- `04 2a 00 00 00 0a 00 00` - Code 0x2a, magnitude 0, period 10ms +- `04 2a 06 00 3f 0a 00 00` - Code 0x2a, magnitude 6, phase 63 (88.6°), period 10ms +- `04 2a 09 00 7f 64 00 00` - Code 0x2a, magnitude 9, phase 127 (178.6°), period 100ms +- `04 b6 00 00 7f 00 00 00` - Code 0xb6, magnitude 0, phase 127 (ramp effect) + +### 0x05 - Conditional Effect Packet (11 bytes) + +**IMPORTANT:** Conditional effects (spring, damper, inertia, friction) require TWO 0x05 packets! + +**First Packet (X-axis parameters):** +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x05 +1 | 1 | code | Variable (from 0x01 packet bytes 9-10, e.g. 0x2a or 0xb6) +2 | 2 | right_coeff | Right coefficient, little-endian +4 | 2 | left_coeff | Left coefficient, little-endian +6 | 2 | deadband | Deadband, little-endian +8 | 1 | center | Center offset +9 | 1 | right_sat | Right saturation +10 | 1 | left_sat | Left saturation +``` + +**Second Packet (Y-axis parameters):** +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x05 +1 | 1 | code | Variable (from 0x01 packet bytes 11-12, e.g. 0x38 or 0xc4) +2-10 | 9 | parameters | Same structure as first packet +``` + +**NOTE:** T500RS is single-axis, so the second packet typically contains zeros. + +**⚠️ CRITICAL FINDING :** Windows sends **zero coefficients, deadband, and center** in ALL 0x05 packets! +- The device firmware appears to reject 0x05 packets with non-zero coefficients +- Only saturation values (bytes 9-10) should be non-zero +- Sending non-zero coefficients causes EPROTO (-71) errors on subsequent packets +- The conditional effect behavior is determined by saturation values, not coefficients + +**Examples (correct - zeros for coefficients):** +- First packet: `05 2a 00 00 00 00 00 00 00 54 54` - Code 0x2a, all zeros, saturation 0x54 +- Second packet: `05 38 00 00 00 00 00 00 00 54 54` - Code 0x38, all zeros, saturation 0x54 + +**Example (INCORRECT - will cause device rejection):** +- `05 2a 7f 00 7f 00 00 00 7f 64 64` - Non-zero coefficients cause Y-axis packet to fail + +**Parameter Scaling:** (Based on limited data, needs verification) +- Coefficients: SDL2 value (0-32767) → device value (scaling TBD) +- Deadband: SDL2 value (0-65535) → device value (scaling TBD) +- Center: SDL2 value (-32767 to +32767) → device value (0-255?) +- Saturation: SDL2 value (0-32767) → device value (0-255, observed: 0x54, 0x64) + +### 0x41 - Command Packet (4 bytes) +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x41 +1 | 1 | effect_id | Always 0x00 for T500RS +2 | 1 | command | 0x41 = START, 0x00 = STOP +3 | 1 | argument | 0x01 for START, varies for STOP +``` + +**Examples:** +- `41 00 41 01` - START effect +- `41 00 00 01` - STOP effect + +--- + +## Effect Type Implementation Table + +### 1. CONSTANT FORCE EFFECTS + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Constant Zero** | level=0, dir=0°, len=500ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`03 0e 00 00`
`41 00 41 01` | +| **Constant Low** | level=8000, dir=0°, len=2000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 06 00 00 06 00`
`03 0e 00 03`
`41 00 41 01` | +| **Constant Medium** | level=24000, dir=0°, len=2000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 12 00 00 12 00`
`03 0e 00 09`
`41 00 41 01` | +| **Constant High** | level=48000, dir=0°, len=5000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 88 13 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 0d 00 00 0d 00`
`03 0e 00 f9`
`41 00 41 01` | +| **Constant Max** | level=65535, dir=180°, len=2000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`03 0e 00 00`
`41 00 41 01` | + +**Scaling Notes:** +- SDL2 level (0-65535) → Device level (-127 to +127) +- Envelope attack/fade level (0-32767) → Device level (0-255) +- Direction (0-35999) in 0.01 degree units + +### 2. PERIODIC EFFECTS - SINE WAVE + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Sine Zero** | mag=0, period=10ms, phase=0°, dir=0° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 00 00 00 0a 00 00`
`41 00 41 01` | +| **Sine Low** | mag=8000, period=10ms, phase=90°, dir=90° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 06 00 00 06 00`
`04 2a 06 00 3f 0a 00 00`
`41 00 41 01` | +| **Sine Medium** | mag=24000, period=100ms, phase=180°, dir=180° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 12 00 00 12 00`
`04 2a 09 00 7f 64 00 00`
`41 00 41 01` | +| **Sine with Envelope** | mag=24000, period=100ms, attack=500ms, fade=500ms | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c f4 01 12 f4 01 12 00`
`04 2a 09 00 00 64 00 00`
`41 00 41 01` | + +**Periodic Effect Notes:** +- Magnitude (0-32767) → Device magnitude (0-127) +- Phase (0-35999, 0.01° units) → Device phase (0-255, 256 steps for 360°) +- Period in milliseconds (no conversion needed) +- Code 0x2a is used (NOT 0x0e as in current driver!) + +### 3. PERIODIC EFFECTS - TRIANGLE WAVE + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Triangle Medium** | mag=24000, period=100ms, phase=180°, dir=180° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 12 00 00 12 00`
`04 2a 09 00 7f 64 00 00`
`41 00 41 01` | + +**Note:** Triangle uses same packet structure as sine; waveform type is determined by effect type in SDL2 upload, not in USB packets. + +### 4. PERIODIC EFFECTS - SAWTOOTH UP + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Sawtooth Up High** | mag=48000, period=100ms, phase=270°, dir=270° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 0d 00 00 0d 00`
`04 2a 06 00 bf 64 00 00`
`41 00 41 01` | + +### 5. PERIODIC EFFECTS - SAWTOOTH DOWN + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Sawtooth Down Max** | mag=65535, period=1000ms, phase=0°, dir=0°, offset=+16000 | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 00 05 7f e8 03 00`
`41 00 41 01` | + +**Note:** Offset field (byte 3 of 0x04 packet) allows DC bias on periodic effects. + +### 6. RAMP EFFECTS + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Ramp Up Low→High** | start=8000, end=48000, len=1000ms, dir=0° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 e8 03 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 03 00 00 e8 03 00`
`41 00 41 01` | +| **Ramp Down High→Low** | start=48000, end=8000, len=1000ms, dir=180° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 e8 03 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 03 00 00 e8 03 00`
`41 00 41 01` | +| **Ramp with Envelope** | start=8000, end=65535, len=5000ms, attack=500ms, fade=500ms | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 88 13 00 00 0e 00 1c 00 00 00`
`02 1c f4 01 12 f4 01 12 00`
`04 2a 03 00 00 27 10 00`
`41 00 41 01` | + +**Ramp Effect Notes:** +- Ramp effects use 0x04 packet type (same as periodic) +- Start/end levels encoded in magnitude and offset fields +- Period field may encode ramp duration or rate + +### 7. CONDITIONAL EFFECTS - SPRING + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Spring Low/High** | right_coeff=low, left_coeff=low, right_sat=high, left_sat=high | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 00 00 54 54`
`05 38 00 00 00 00 00 00 00 54 54`
`41 00 41 01` | +| **Spring Deadband** | right_coeff=medium, left_coeff=medium, deadband=500, center=0 | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 07 00 54 54`
`05 38 00 00 00 00 00 00 00 54 54`
`41 00 41 01` | +| **Spring Asymmetric** | right_coeff=0, left_coeff=high, deadband=5000, center=0 | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 99 00 4c 00 54 54`
`05 38 00 00 00 00 00 00 00 54 54`
`41 00 41 01` | + +**Conditional Packet Structure (0x05):** +- Two packets sent per conditional effect (one per axis) +- First packet: code 0x2a (X-axis parameters) +- Second packet: code 0x38 (Y-axis parameters) +- Bytes 2-3: Right coefficient (little-endian, 0-65535 scaled) +- Bytes 4-5: Left coefficient (little-endian, 0-65535 scaled) +- Bytes 6-7: Deadband (little-endian, scaled) +- Byte 8: Center offset (signed) +- Bytes 9-10: Right/Left saturation (0x5454 = 84,84) + +### 8. CONDITIONAL EFFECTS - DAMPER + +| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | +|-----------|----------------|-----------------|-----------------| +| **Damper Low** | right_coeff=low, left_coeff=low, right_sat=max, left_sat=max | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 00 00 64 64`
`05 38 00 00 00 00 00 00 00 64 64`
`41 00 41 01` | +| **Damper High** | right_coeff=high, left_coeff=high | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 00 00 64 64`
`05 38 00 00 00 00 00 00 00 64 64`
`41 00 41 01` | + +**Note:** Damper uses same 0x05 packet structure as spring. Saturation values differ (0x6464 for damper vs 0x5454 for spring). + +### 10. CONDITIONAL EFFECTS - INERTIA + +**Status:** Limited capture data available. Assumed to use same 0x05 packet structure as spring/damper. + +**Expected Structure:** +- Two 0x05 packets with codes from 0x01 bytes 9-12 +- Same parameter layout: right_coeff, left_coeff, deadband, center, saturation +- Saturation value may differ from spring/damper + +### 11. CONDITIONAL EFFECTS - FRICTION + +**Status:** Limited capture data available. Assumed to use same 0x05 packet structure as spring/damper. + +**Expected Structure:** +- Two 0x05 packets with codes from 0x01 bytes 9-12 +- Same parameter layout: right_coeff, left_coeff, deadband, center, saturation +- Saturation value may differ from spring/damper + +### 12. MULTI-EFFECT SCENARIOS + +#### Sequential Effects (No Overlap) + +| Scenario | Description | Effect IDs Used | Packet Sequence | +|----------|-------------|-----------------|-----------------| +| **Constant → Sine** | Constant 1500ms, then Sine 1500ms | 0x0000, then 0x0021 | Effect 1 (ID=0x00): Upload→Envelope→Constant→START→[wait]→STOP
Effect 2 (ID=0x21): Upload→Envelope→Periodic→START→[wait]→STOP | +| **Sine → Triangle** | Sine 3000ms, then Triangle 3000ms | 0x0000, then 0x0022 | Effect 1 (ID=0x00): Upload→Envelope→Periodic→START→[wait]→STOP
Effect 2 (ID=0x22): Upload→Envelope→Periodic→START→[wait]→STOP | + +**Sequential Effect Notes:** +- Each effect gets full upload sequence with unique effect ID +- Windows driver uses non-sequential IDs: 0x00, 0x21, 0x22, 0x40, 0x41 +- Previous effect must be stopped before starting next +- Gap between effects depends on timing in test + +#### Overlapping Effects + +| Scenario | Description | Effect IDs Used | Packet Sequence | +|----------|-------------|-----------------|-----------------| +| **Sine + Triangle Overlap** | Sine starts, Triangle joins after 1s, both run for 2s | 0x0040, 0x0041 | Effect 1 (ID=0x40): Upload→Envelope→Periodic→START
[wait 1s]
Effect 2 (ID=0x41): Upload→Envelope→Periodic→START
[wait 2s]
Effect 1 (ID=0x40): STOP
Effect 2 (ID=0x41): STOP | + +**Overlapping Effect Notes:** +- T500RS supports up to 16 simultaneous effects (hardware capability) +- Windows driver assigns unique effect IDs to each concurrent effect +- Effects are uploaded and started independently with different IDs +- Device mixes/sums the forces internally +- Effect IDs observed: 0x0000, 0x0021, 0x0022, 0x0040, 0x0041 (non-sequential pattern) +- Driver can use simple sequential assignment (0x00, 0x01, 0x02, etc.) instead + +#### Rapid Sequential Effects + +| Scenario | Description | Timing | +|----------|-------------|--------| +| **Short Rapid** | 3 effects × 200ms back-to-back | Constant→Sine→Spring, no gaps | +| **Short with Gaps** | 3 effects × 300ms with 100ms gaps | Sine→[100ms]→Constant→[100ms]→Damper | + +**Rapid Effect Notes:** +- Device handles rapid effect changes (200ms duration) +- No special packet sequence needed for rapid changes +- Standard upload→start→stop sequence for each effect + +--- + +## Parameter Encoding Reference + +### Direction Encoding +- **SDL2 Format:** 0-35999 (0.01 degree units) +- **Device Format:** 16-bit little-endian in 0x01 packet +- **Conversion:** Direct copy, no scaling +- **Examples:** + - 0° = 0x0000 + - 90° = 0x2328 (9000 decimal) + - 180° = 0x4650 (18000 decimal) + - 270° = 0x6978 (27000 decimal) + +### Duration Encoding +- **SDL2 Format:** Milliseconds +- **Device Format:** 16-bit little-endian in 0x01 packet +- **Conversion:** Direct copy +- **Examples:** + - 500ms = 0x01f4 + - 1000ms = 0x03e8 + - 2000ms = 0x07d0 + - 5000ms = 0x1388 + +### Force Level Encoding (Constant) +- **SDL2 Format:** 0-65535 (unsigned) +- **Device Format:** -127 to +127 (signed 8-bit) +- **Conversion:** Scale and sign +- **Formula:** `device_level = (sdl_level * 255 / 65535) - 127` +- **Examples:** + - SDL 0 → Device 0 + - SDL 8000 → Device 3 + - SDL 24000 → Device 9 + - SDL 48000 → Device -7 (0xf9) + - SDL 65535 → Device 127 (max positive) + +### Magnitude Encoding (Periodic) +- **SDL2 Format:** 0-32767 (unsigned) +- **Device Format:** 0-127 (unsigned 8-bit) +- **Conversion:** Scale down +- **Formula:** `device_mag = sdl_mag * 127 / 32767` +- **Examples:** + - SDL 0 → Device 0 + - SDL 8000 → Device 6 + - SDL 24000 → Device 9 + - SDL 32767 → Device 127 + +### Phase Encoding (Periodic) +- **SDL2 Format:** 0-35999 (0.01 degree units, 0-359.99°) +- **Device Format:** 0-255 (256 steps for 360°) +- **Conversion:** Scale to 256 steps +- **Formula:** `device_phase = (sdl_phase * 256 / 36000) & 0xFF` +- **Examples:** + - 0° (0) → 0x00 + - 90° (9000) → 0x40 (64) + - 180° (18000) → 0x80 (128) + - 270° (27000) → 0xC0 (192) + +### Period Encoding (Periodic) +- **SDL2 Format:** Milliseconds +- **Device Format:** 16-bit little-endian in 0x04 packet +- **Conversion:** Direct copy (keep in milliseconds, NOT Hz×100!) +- **Examples:** + - 10ms = 0x000a + - 50ms = 0x0032 + - 100ms = 0x0064 + - 1000ms = 0x03e8 + +### Envelope Level Encoding +- **SDL2 Format:** 0-32767 (unsigned) +- **Device Format:** 0-255 (unsigned 8-bit) +- **Conversion:** Scale down +- **Formula:** `device_env = sdl_env * 255 / 32767` +- **Examples:** + - SDL 0 → Device 0 + - SDL 8000 → Device 6 + - SDL 16000 → Device 12 + - SDL 24000 → Device 18 + - SDL 32767 → Device 255 + +--- + +## Important implementation facts + +### 1. Packet Code Discrepancy - +**FIXED:** Driver updated to use correct packet codes (0x2a instead of 0x0e) for periodic/conditional effects. + +**Original Issue:** Windows captures consistently show code **0x2a** in 0x04 periodic packets and 0x05 conditional packets, but the original driver used **0x0e**. + +**Resolution Applied:** +- ✅ Updated `struct t500rs_pkt_r04_periodic_ramp` to use variable code from 0x01 packet +- ✅ Implemented `struct t500rs_pkt_r05_condition` with two packets (codes 0x2a and 0x38) +- ✅ Updated 0x01 packet bytes 9-10 to use 0x002a for periodic/conditional effects +- ✅ Updated 0x01 packet bytes 11-12 to use 0x0038 for conditional effects +- ✅ All packet codes dynamically determined from 0x01 packet bytes 9-12 + +### 2. Period Encoding +**IMPORTANT:** Keep period in **milliseconds**, do NOT convert to Hz×100! +- Windows captures confirm: period values match milliseconds directly + +### 3. Effect ID and Slot Management - +**IMPLEMENTED:** Driver uses unique hardware IDs for concurrent effects to prevent slot collision. + +**Hardware Capability:** T500RS supports up to 16 simultaneous effects with internal mixing. +- **Single Effect:** Windows driver uses effect_id = 0x0000 for isolated testing +- **Multi-Effect:** Driver assigns unique effect IDs for concurrent effects (0-15 range) +- **Hardware Behavior:** Device maintains 16 effect slots internally with automatic force mixing + +**Driver Implementation Applied:** +- ✅ Hardware ID allocation system (0-15) prevents slot collision +- ✅ Unique effect_ids assigned to concurrent effects +- ✅ START/STOP commands use matching effect_id from 0x01 packet +- ✅ Spinlock protection for thread-safe hardware ID operations + +**Resolution Details:** +- Constant effects: effect_id = hw_id (0-15) with subtypes 0x0e/0x1c +- Conditional effects: effect_id = hw_id (0-15) with subtypes 0x2a/0x38 +- Periodic effects: effect_id = hw_id (0-15) with subtypes 0x2a/0x38 +- Hardware ID bitmap tracks occupied slots to prevent overwrites + +### 4. Envelope Flag +- 0x01 packet byte 11-12: 0x001c when envelope is present +- 0x0000 when no envelope +- Envelope packet (0x02) should always be sent, even if all zeros + +### 5. Telemetry Packets (0x07) +- High frequency position feedback from device +- 15 bytes: `07 [pos_lo] [pos_hi] 03 ff 03 ff 03 00 00 00 00 00 0f` +- Position appears to be 16-bit value in bytes 1-2 +- Not required for effect upload, but useful for force feedback tuning + +### 6. Polling Packets (0x49) +- Device sends these frequently during operation +- Two formats seen: + - Short: `49 00 00 00 00 10 00` (7 bytes) + - Long: `49 00 00 00 01 00 02 00 03 00 00 00 02 02 00 00` (16 bytes) +- Appear to be status/heartbeat packets +- Driver should handle/ignore these gracefully + +### 7. Wine Compatibility Considerations +- **Autocenter Hack:** Some games under Wine (e.g., Live for Speed) set autocenter to 100% permanently, which overpowers other force feedback effects and forces centered wheel all the time the game is started. The Linux driver includes a compatibility hack that ignores 100% autocenter requests while allowing 0-99% values to work normally. +- **Effect Behavior:** Wine applications may exhibit different force feedback behavior due to DirectInput vs SDL2 differences, but the underlying USB protocol remains the same. + +--- + +## Driver Implementation Sequence + +### For Constant Force Effect: +1. Build 0x01 packet (direction, duration, effect_code=0x0e, envelope_flag=0x1c) +2. Build 0x02 packet (attack/fade parameters, scaled to 0-255) +3. Build 0x03 packet (force level, scaled to -127..+127) +4. Send packets in order: 0x01 → 0x02 → 0x03 +5. Build 0x41 packet (START command) +6. Send 0x41 packet +7. [Effect runs] +8. Build 0x41 packet (STOP command, byte 2 = 0x00) +9. Send 0x41 packet + +### For Periodic Effect (Sine/Triangle/Sawtooth): +1. Build 0x01 packet (direction, duration, effect_code=0x0e, envelope_flag=0x1c) +2. Build 0x02 packet (attack/fade parameters, scaled to 0-255) +3. Build 0x04 packet (magnitude, offset, phase, period - **use code 0x2a!**) +4. Send packets in order: 0x01 → 0x02 → 0x04 +5. Build 0x41 packet (START command) +6. Send 0x41 packet +7. [Effect runs] +8. Build 0x41 packet (STOP command, byte 2 = 0x00) +9. Send 0x41 packet + +### For Ramp Effect: +1. Same as periodic effect (uses 0x04 packet) +2. Encode start/end levels in magnitude/offset fields +3. Period field may control ramp rate + +--- + +## Protocol Analysis Summary + +### Confirmed Protocol Features + +1. **Variable Packet Codes:** + - Bytes 9-12 of 0x01 packet specify codes for subsequent packets + - NOT fixed values - varies between captures (0x2a/0x38, 0xb6/0xc4, 0x46/0x54, etc.) + - Driver must use the codes specified in the 0x01 packet + +2. **Conditional Effect Structure:** + - TWO 0x05 packets required per conditional effect + - First packet uses code from bytes 9-10 of 0x01 (X-axis parameters) + - Second packet uses code from bytes 11-12 of 0x01 (Y-axis parameters) + - **⚠️ CRITICAL:** Both packets must have ZERO coefficients/deadband/center - only saturation is set + - Sending non-zero coefficients causes EPROTO errors and device rejection + +3. **Effect Slot Management:** + - Hardware supports 16 concurrent effect slots + - Windows driver uses non-sequential IDs: 0x0000, 0x0040, 0x0041, 0x0021, 0x0022 + - Effect IDs observed in range 0x00-0xff + - Device handles internal mixing of concurrent effects + +4. **Period Encoding:** + - Period is in MILLISECONDS (not Hz×100) + - No conversion needed - pass through directly + +5. **Gain Handling:** + - In-game gain (SDL2/DirectInput) is applied BEFORE sending to driver + - USB packets contain already-scaled values + - Device gain (sysfs) is separate and applied by hardware + - No gain parameter in USB protocol + +6. **Square Wave:** + - NOT SUPPORTED by T500RS hardware (as per captures) + - No 0x04 packets generated for square wave effect type + - SDL2 square wave requests produce no USB traffic + +### 📊 Parameter Scaling (Confirmed from captures) + +**Constant Force:** +- SDL2 level (0-65535) → Device level (-127 to +127) +- Formula: `device_level = (sdl_level * 255 / 65535) - 127` + +**Periodic Effects:** +- Magnitude (0-32767) → Device magnitude (0-127) +- Formula: `device_mag = sdl_mag * 127 / 32767` +- Phase (0-35999, 0.01° units) → Device phase (0-255) +- Formula: `device_phase = (sdl_phase * 256 / 36000) & 0xff` + +**Envelope:** +- Attack/Fade level (0-32767) → Device level (0-255) +- Formula: `device_level = sdl_level * 255 / 32767` + +**Conditional Effects:** +- Saturation values observed: 0x54 (spring), 0x64 (damper) +- **⚠️ CRITICAL:** Coefficient, deadband, and center must be sent as ZEROS +- The device firmware rejects 0x05 packets with non-zero coefficients +- Effect behavior is controlled solely through saturation values (0-100 range) + +### ⚠️ Areas Needing More Data + +1. **Conditional Parameter Scaling:** + - Exact formulas for coefficient, deadband, center encoding + - Most test captures show zero values for these parameters + - Need captures with varied conditional parameters + +2. **Ramp Effect Encoding:** + - How ramp_start and ramp_end map to 0x04 packet fields + - All ramp captures show identical packets + - Test application may not be passing ramp parameters correctly + +3. **Inertia and Friction Effects:** + - Limited packet examples for these effect types + - Assumed to use same 0x05 structure as spring/damper + - Need verification with actual captures + +### Optional Improvements + +1. **Conditional Parameter Scaling:** + - Implement best-guess scaling for coefficient, deadband, center + - Test with real hardware to verify behavior + - Adjust formulas based on testing results + +2. **Ramp Effect Support:** + - Implement ramp using 0x04 packet + - Map start/end to magnitude/offset fields + - Verify behavior on hardware + +3. **Multi-Effect Testing:** + - Test concurrent effect playback + - Verify effect mixing behavior + - Test effect ID reuse after stopping + diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index 162a30af..ac52cda0 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -242,8 +242,31 @@ 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); + + /* 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); + 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; @@ -315,15 +376,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) >= (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); @@ -694,6 +766,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; @@ -806,6 +883,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 */ @@ -828,4 +907,10 @@ static struct hid_driver tmff2_driver = { }; 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 6ad1d823..30cdcea7 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,7 @@ 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); + __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 +107,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); @@ -109,6 +117,8 @@ int tspc_populate_api(struct tmff2_device_entry *tmff2); #define TMT300RS_PS3_ADV_ID 0xb66f #define TMT300RS_PS4_NORM_ID 0xb66d +#define TMT500RS_PC_ID 0xb65e + #define TMT248_PC_ID 0xb696 #define TX_ACTIVE 0xb669 diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c new file mode 100644 index 00000000..7a239792 --- /dev/null +++ b/src/tmt500rs/hid-tmt500rs.c @@ -0,0 +1,2073 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Force feedback support for Thrustmaster T500RS + * + * HID implementation using HID output reports for all communication. + * + * Protocol documentation: docs/T500RS_USB_Protocol_Analysis.md + * + * Key protocol details (verified against Windows USB captures): + * - 0x01 packet: Main upload (15 bytes) - effect_id, direction, duration, + * delay, code1/2 + * - 0x02 packet: Envelope (9 bytes) - attack/fade levels and times + * - 0x03 packet: Constant force level (4 bytes) + * - 0x04 packet: Periodic/Ramp parameters (8 bytes) - code 0x2a, period in ms + * - 0x05 packet: Conditional parameters (11 bytes) - two packets per effect + * (X/Y) + * - 0x41 packet: START/STOP command (4 bytes) - per-effect hw_id + * + * Hardware supports 16 concurrent effects with internal mixing. + * Protocol analysis based on 70+ test captures from Windows driver. + */ + +#include "../hid-tmff2.h" +#include "t500rs_protocol.h" +#include +#include + +/* Packet sequence templates for each effect type */ +static const enum t500rs_seq_packet t500rs_seq_constant[] = { + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_CONSTANT, + T500RS_SEQ_MAIN, +}; + +static const enum t500rs_seq_packet t500rs_seq_periodic[] = { + T500RS_SEQ_STOP, T500RS_SEQ_SYNC_42_05, T500RS_SEQ_SYNC_42_04, + T500RS_SEQ_ENVELOPE, T500RS_SEQ_PERIODIC_RAMP, T500RS_SEQ_MAIN, +}; + +static const enum t500rs_seq_packet t500rs_seq_ramp[] = { + T500RS_SEQ_STOP, + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_PERIODIC_RAMP, + T500RS_SEQ_MAIN, +}; + +static const enum t500rs_seq_packet t500rs_seq_condition[] = { + T500RS_SEQ_CONDITION_X, + T500RS_SEQ_CONDITION_Y, + T500RS_SEQ_MAIN, +}; + +/* Scale constant level (-32767..32767) to signed 8-bit (-127..127) */ +static inline s8 t500rs_scale_const_level_s8(int level) { + /* Input validation and clamping */ + if (level > 32767) + level = 32767; + if (level < -32767) + level = -32767; + + /* Use 32-bit arithmetic to prevent overflow */ + return (s8)((level * 127LL) / 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) { + /* Input validation and clamping */ + if (magnitude < 0) + magnitude = -magnitude; + if (magnitude > 32767) + magnitude = 32767; + + /* Use 32-bit arithmetic to prevent overflow */ + return (u8)((magnitude * 127LL) / 32767); +} + +/* Map logical effect index to parameter/envelope subtypes as per protocol: + * Per protocol analysis, subtypes are calculated as: + * param_sub = 0x000e + 0x001c * idx (for ALL effects) + * env_sub = 0x001c + 0x001c * idx (envelope always uses 0x001c base) + * idx is wrapped to the hardware limit of 16 effect slots. + * + * CRITICAL: Index 0 (subtypes 0x0e/0x1c) is ONLY valid for constant effects + * Indices 1+ (subtypes 0x2a/0x38, etc.) are valid for all effect types + * Periodic/ramp effects sent with index 0 subtypes cause EPROTO + */ +static inline void t500rs_index_to_subtypes(unsigned int idx, u16 *param_sub, + u16 *env_sub, + bool is_periodic_or_conditional) { + /* Validate inputs */ + if (idx >= T500RS_MAX_HW_EFFECTS) { + idx = T500RS_MAX_HW_EFFECTS - 1; /* Clamp to valid range */ + } + + /* + * Critical protocol constraint from T500RS_USB_Protocol_Analysis.md: + * - Index 0 (subtypes 0x0e/0x1c) is ONLY valid for constant effects + * - Periodic/conditional MUST use index ≥ 1 (subtypes 0x2a+) + * - Formula is identical for all types: 0x000e + 0x001c * idx + * - Envelope uses: 0x001c + 0x001c * idx + */ + if (is_periodic_or_conditional && idx == 0) { + pr_warn_once("t500rs: Periodic/conditional effect using index 0 - device will reject!\n"); + } + + *param_sub = 0x000e + (0x001c * idx); + *env_sub = 0x001c + (0x001c * idx); +} + +/* Debug logging helper: pass struct t500rs_device_entry * explicitly */ +#define T500RS_DBG(dev, fmt, ...) hid_dbg((dev)->hdev, fmt, ##__VA_ARGS__) + +/* T500RS device data */ +struct t500rs_device_entry { + struct hid_device *hdev; + struct input_dev *input_dev; + + u8 *send_buffer; + size_t buffer_length; + + /* Current wheel range for smooth transitions */ + u16 current_range; /* Current rotation range in degrees */ + + /* + * Hardware effect ID management - Simplified Architecture (Phase 3). + * + * T500RS hardware supports up to 16 simultaneous effects with internal + * mixing. This simplified system replaces the complex three-array approach + * with: + * + * hw_id_map[logical_id] = hardware effect ID (0..15) assigned to logical slot + * hw_slots_in_use = bitmap tracking which hardware slots are occupied + * + * Benefits: Reduced complexity, better cache performance, easier debugging. + */ + u16 hw_id_map[T500RS_MAX_EFFECTS]; /* logical -> hardware mapping */ + DECLARE_BITMAP(hw_slots_in_use, + T500RS_MAX_HW_EFFECTS); /* occupied slots bitmap */ + + /* + * Thread safety - Phase 5 Security and Robustness. + * + * Hardware ID operations are not atomic and require protection against + * concurrent access from multiple threads/processes. + */ + spinlock_t hw_id_lock; /* Protects hw_id_map and hw_slots_in_use */ +}; + +/* + * Allocate a hardware effect ID for the given logical effect id. + * + * Per Windows USB captures, the T500RS device has specific expectations: + * - Index 0 (subtypes 0x0e/0x1c) is ONLY valid for constant effects (0x03 + * packets) + * - Indices 1+ (subtypes 0x2a/0x38, etc.) are valid for all effect types + * - Periodic/ramp effects (0x04 packets) sent with index 0 subtypes cause + * EPROTO + * + * The skip_index_zero parameter should be true for periodic, ramp, and + * conditional effects to avoid the firmware rejecting the upload. + * + * Returns the hardware ID (0..15) on success, or -ENOSPC if all slots are used. + */ +static int t500rs_alloc_hw_id(struct t500rs_device_entry *t500rs, + unsigned int logical_id, bool skip_index_zero) { + unsigned int start_idx = skip_index_zero ? 1 : 0; + int hw_slot; + unsigned long flags; + + /* Input validation */ + if (!t500rs) { + pr_err("t500rs_alloc_hw_id: NULL device entry\n"); + return -ENODEV; + } + if (logical_id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Invalid logical_id %u (max %d)\n", logical_id, + T500RS_MAX_EFFECTS); + return -EINVAL; + } + + spin_lock_irqsave(&t500rs->hw_id_lock, flags); + + /* Check if this logical_id already has a hardware slot assigned */ + if (t500rs->hw_id_map[logical_id] < T500RS_MAX_HW_EFFECTS) { + hw_slot = t500rs->hw_id_map[logical_id]; + /* Verify the slot is still marked as in use */ + if (test_bit(hw_slot, t500rs->hw_slots_in_use)) { + T500RS_DBG(t500rs, "hw_id %d already allocated for logical_id %d\n", + hw_slot, logical_id); + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + return hw_slot; + } + /* Slot was freed, clear the mapping */ + t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; + } + + /* Find the first available hardware slot */ + hw_slot = bitmap_find_next_zero_area(t500rs->hw_slots_in_use, + T500RS_MAX_HW_EFFECTS, start_idx, 1, 0); + if (hw_slot >= T500RS_MAX_HW_EFFECTS) { + hid_err(t500rs->hdev, "No available hardware slots for effect %d\n", + logical_id); + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + return -ENOSPC; + } + + /* + * Mark slot as in use atomically. This is technically not needed since + * we hold hw_id_lock, but using test_and_set_bit documents the atomicity + * requirement and protects against future refactoring errors. + */ + if (test_and_set_bit(hw_slot, t500rs->hw_slots_in_use)) { + /* Should never happen - we just found this slot was free */ + hid_err(t500rs->hdev, "BUG: Slot %d was free but is now occupied\n", hw_slot); + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + return -EBUSY; + } + + t500rs->hw_id_map[logical_id] = (u16)hw_slot; + + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + + hid_info(t500rs->hdev, + "T500RS: Allocated hw_id=%d for logical_id=%d (skip_zero=%d)\n", + hw_slot, logical_id, skip_index_zero); + return hw_slot; +} + +/* + * Get the hardware effect ID for the given logical effect id. + * Allocates a new slot if one is not yet assigned. + * Returns the hardware ID (0..15) on success, or negative error. + */ +static int t500rs_get_hw_id(struct t500rs_device_entry *t500rs, + unsigned int logical_id) { + int hw_slot; + unsigned long flags; + + /* Input validation */ + if (!t500rs) { + pr_err("t500rs_get_hw_id: NULL device entry\n"); + return -ENODEV; + } + if (logical_id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Invalid logical_id %u (max %d)\n", logical_id, + T500RS_MAX_EFFECTS); + return -EINVAL; + } + + spin_lock_irqsave(&t500rs->hw_id_lock, flags); + + /* Check if already allocated */ + if (t500rs->hw_id_map[logical_id] < T500RS_MAX_HW_EFFECTS) { + hw_slot = t500rs->hw_id_map[logical_id]; + /* Verify slot is still in use */ + if (test_bit(hw_slot, t500rs->hw_slots_in_use)) { + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + return hw_slot; + } + /* Slot was freed, clear mapping */ + t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; + } + + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + + /* Allocate new slot - default to skip_index_zero=true for safety */ + return t500rs_alloc_hw_id(t500rs, logical_id, true); +} + +/* + * Free the hardware effect ID for the given logical effect id. + * Called from stop_effect path to recycle hardware slots. + */ +static void t500rs_free_hw_id(struct t500rs_device_entry *t500rs, + unsigned int logical_id) { + unsigned long flags; + int hw_slot; + + /* Input validation */ + if (!t500rs) { + pr_err("t500rs_free_hw_id: NULL device entry\n"); + return; + } + if (logical_id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Invalid logical_id %u for free operation\n", + logical_id); + return; + } + + spin_lock_irqsave(&t500rs->hw_id_lock, flags); + + /* Check if this logical ID has a hardware slot assigned */ + if (t500rs->hw_id_map[logical_id] >= T500RS_MAX_HW_EFFECTS) { + /* No slot assigned, nothing to free */ + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + return; + } + + /* Get the hardware slot and verify it's still in use */ + hw_slot = t500rs->hw_id_map[logical_id]; + if (!test_bit(hw_slot, t500rs->hw_slots_in_use)) { + hid_warn(t500rs->hdev, + "Hardware slot %d not marked as in use for logical_id %d\n", + hw_slot, logical_id); + /* Clear the mapping anyway to be safe */ + t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + return; + } + + /* Free the slot */ + clear_bit(hw_slot, t500rs->hw_slots_in_use); + t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; + + spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); + + T500RS_DBG(t500rs, "Freed hw_id %d for logical_id %d\n", hw_slot, logical_id); +} + +/* + * Debug function to list currently active effects and their hardware slots. + * Useful for troubleshooting multi-effect scenarios. + */ +static void t500rs_debug_active_effects(struct t500rs_device_entry *t500rs) { + int logical_id; + bool has_active = false; + + if (!t500rs) { + pr_err("t500rs_debug_active_effects: NULL device entry\n"); + return; + } + + /* Iterate through logical IDs to find active mappings */ + for (logical_id = 0; logical_id < T500RS_MAX_EFFECTS; logical_id++) { + if (t500rs->hw_id_map[logical_id] < T500RS_MAX_HW_EFFECTS) { + int hw_slot = t500rs->hw_id_map[logical_id]; + if (test_bit(hw_slot, t500rs->hw_slots_in_use)) { + T500RS_DBG(t500rs, "Active effect: logical_id=%d, hw_slot=%d\n", + logical_id, hw_slot); + has_active = true; + } + } + } + + if (!has_active) { + T500RS_DBG(t500rs, "No active effects\n"); + } +} + +/* + * Scale direction from Linux ff_effect format to T500RS protocol format. + * + * Linux ff_effect.direction: 0-65535 (0 = forward, 16384 = right, 32768 = back, + * 49152 = left) T500RS protocol: 0-35999 in 0.01 degree units (0 = 0°, 9000 = + * 90°, 18000 = 180°, etc.) + * + * Conversion: device_dir = (linux_dir * 36000) / 65536 + * This maps 0-65535 → 0-35999 (approximately, since 65535 → 35999.45) + */ +static inline u16 t500rs_scale_direction(u16 linux_dir) { + /* Use 32-bit arithmetic to avoid overflow */ + return (u16)(((u32)linux_dir * 36000) / 65536); +} + +/* + * Build a protocol-accurate 0x01 main upload packet. + * + * Per the T500RS USB protocol documentation: + * - effect_id: 16-bit LE hardware effect slot (0..15 for now) + * - direction: 0..35999 in 0.01 degree units (already scaled, use + * t500rs_scale_direction) + * - duration_ms: duration in milliseconds + * - delay_ms: delay before effect starts + * - code1: parameter subtype (used by 0x03/0x04/0x05) + * - code2: envelope subtype (used by 0x02), or second conditional subtype + * + * Per Windows captures, effect_type values are: + * - 0x00 = Constant + * - 0x22 = Sine + * - 0x21 = Triangle (inferred) + * - 0x23 = Sawtooth Up (inferred) + * - 0x24 = Sawtooth Down (inferred) + * - 0x40 = Spring + * - 0x41 = Damper/Friction/Inertia + * + * NOTE: Direction is NOT in the 0x01 packet on T500RS! + */ +static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, + u8 effect_type, u16 duration_ms, u16 delay_ms, + u16 code1, u16 code2) { + /* Validate effect_id */ + if (effect_id >= T500RS_MAX_HW_EFFECTS) { + pr_err("t500rs: Invalid effect_id %u (max %d)\n", + effect_id, T500RS_MAX_HW_EFFECTS - 1); + return -EINVAL; + } + + /* Validate effect_type against known constants */ + switch (effect_type) { + case T500RS_EFFECT_CONSTANT: + case T500RS_EFFECT_SINE: + case T500RS_EFFECT_TRIANGLE: + case T500RS_EFFECT_SAW_UP: + case T500RS_EFFECT_SAW_DOWN: + case T500RS_EFFECT_SPRING: + case T500RS_EFFECT_DAMPER: /* Note: DAMPER, FRICTION, INERTIA all use 0x41 */ + break; + default: + pr_err("t500rs: Unknown effect_type 0x%02x\n", effect_type); + return -EINVAL; + } + + /* Validate packet codes are non-zero (0x0000 likely indicates bug) */ + if (code1 == 0 || code2 == 0) { + pr_warn("t500rs: Suspicious packet codes: code1=0x%04x code2=0x%04x\n", + code1, code2); + } + + memset(p, 0, sizeof(*p)); + p->id = T500RS_PKT_MAIN; + p->effect_id = effect_id; + p->effect_type = effect_type; + p->control = T500RS_CONTROL_DEFAULT; + p->duration_ms = cpu_to_le16(duration_ms); + p->delay_ms = cpu_to_le16(delay_ms); + p->reserved1 = 0; + p->packet_code_1 = cpu_to_le16(code1); + p->packet_code_2 = cpu_to_le16(code2); + p->reserved2 = 0; + + return 0; +} + +/* + * Build a protocol-accurate 0x04 periodic/ramp packet. + * + * Per the T500RS USB protocol documentation: + * - code: low byte of param_subtype from 0x01 (e.g., 0x2a for periodic, not + * 0x0e!) + * - magnitude: 0..127 (scaled from SDL's 0..32767) + * - offset: signed DC offset (scaled from SDL's -32768..32767 to device range) + * - phase: 0..255 (256 steps for 360°, scaled from SDL's 0..35999) + * - period_ms: period in MILLISECONDS (no Hz*100 conversion!) + * - reserved: always 0 + * + * Scaling formulas (from protocol doc): + * device_mag = sdl_mag * 127 / 32767 + * device_phase = (sdl_phase * 256 / 36000) & 0xFF + * device_offset = sdl_offset / 256 (approximate, TBD based on testing) + * period_ms = direct copy (no frequency conversion) + */ +static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, + u8 code, u8 magnitude, s8 offset, + u8 phase, u16 period_ms) { + /* Byte order per Windows USB captures (example: 04 2a 00 06 00 3f 0a 00): + * b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset, + * b5=phase, b6-b7=period + */ + memset(p, 0, sizeof(*p)); + p->id = T500RS_PKT_PERIODIC; /* b0 */ + p->code = code; /* b1 */ + p->reserved1 = 0; /* b2: always 0x00 */ + p->magnitude = magnitude; /* b3 */ + p->offset = (u8)offset; /* b4 */ + p->phase = phase; /* b5 */ + p->period_ms = cpu_to_le16(period_ms); /* b6-b7 */ +} + +/* + * Scale periodic magnitude from SDL format to device format. + * SDL: 0..32767 (unsigned) + * Device: 0..127 + */ +static inline u8 t500rs_scale_periodic_magnitude(int sdl_mag) { + /* Input validation and clamping */ + if (sdl_mag < 0) + sdl_mag = -sdl_mag; + if (sdl_mag > 32767) + sdl_mag = 32767; + + /* Use 32-bit arithmetic to prevent overflow */ + return (u8)((sdl_mag * 127LL) / 32767); +} + +/* + * Scale periodic phase from SDL format to device format. + * SDL: 0..35999 (0.01 degree units, 0-359.99°) + * Device: 0..255 (256 steps for 360°) + */ +static inline u8 t500rs_scale_periodic_phase(u16 sdl_phase) { + /* Clamp to valid range just in case */ + if (sdl_phase > 35999) + sdl_phase = 35999; + return (u8)((sdl_phase * 256) / 36000); +} + +/* + * Scale periodic offset from SDL format to device format. + * SDL: -32768..32767 + * Device: signed, stored as s8 (-128..127) + * Note: exact mapping TBD based on testing; using simple /256 for now. + */ +static inline s8 t500rs_scale_periodic_offset(s16 sdl_offset) { + return (s8)(sdl_offset / 256); +} + +/* + * Build a 0x04 packet for ramp effects. + * + * Per the T500RS USB protocol documentation, ramp effects use the same + * 0x04 packet structure as periodic effects. The encoding is: + * - magnitude: scaled from start/end levels (midpoint or average) + * - offset: difference between start and end (direction of ramp) + * - phase: typically 0 for ramp + * - period_ms: ramp duration in milliseconds + * + * Note: exact mapping of start/end to magnitude/offset is uncertain; + * Windows captures show identical packets for different ramp parameters. + * Current implementation uses a simple average for magnitude. + */ +static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, + u8 code, s16 start_level, s16 end_level, + u16 duration_ms) { + int avg_level; + u8 magnitude; + s8 offset; + + memset(p, 0, sizeof(*p)); + + /* Compute average magnitude from start/end levels */ + avg_level = (abs(start_level) + abs(end_level)) / 2; + magnitude = (u8)((avg_level * 127) / 32767); + + /* Offset encodes direction: positive = ramping up, negative = ramping down */ + /* Simple approximation: (end - start) / 512 to fit in s8 range */ + offset = (s8)((end_level - start_level) / 512); + + /* Byte order per Windows USB captures: b0=id, b1=code, b2=reserved1, b3=mag, + * b4=offset, b5=phase, b6-b7=period */ + p->id = 0x04; /* b0 */ + p->code = code; /* b1 */ + p->reserved1 = 0; /* b2: always 0x00 */ + p->magnitude = magnitude; /* b3 */ + p->offset = (u8)offset; /* b4 */ + p->phase = 0; /* b5: Ramp doesn't use phase */ + p->period_ms = cpu_to_le16(duration_ms); /* b6-b7 */ +} + +/* + * Build a 0x05 conditional effect packet. + * + * CRITICAL FIRMWARE BUG WORKAROUND: + * Per Windows captures (T500RS_USB_Protocol_Analysis.md §202-213), + * the T500RS firmware has a critical bug where conditional effects + * REQUIRE two 0x05 packets, but BOTH packets must have ALL coefficients, + * deadband, and center values set to ZERO. Only saturation values should + * be non-zero. + * + * The device firmware rejects 0x05 packets with non-zero coefficients + * and causes EPROTO (-71) errors on subsequent packets. Effect behavior + * is controlled SOLELY through saturation values in the 0-100 range. + * + * Parameters: + * - code: From 0x01 packet bytes 9-10 (first packet) or 11-12 (second packet) + * - saturation: Scaled saturation value (0-100) for both right/left channels + * + * Implementation note: + * We use memset(0) to ensure all fields are zero, then only set: + * - id = 0x05 + * - code (from param_sub or env_sub) + * - right_sat = saturation + * - left_sat = saturation + * + * This ensures compliance with the firmware's strict requirements. + */ +static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, + u8 code, u8 saturation) { + /* Zero entire structure to comply with firmware requirements */ + memset(p, 0, sizeof(*p)); + p->id = T500RS_PKT_CONDITIONAL; + p->code = code; + + /* CRITICAL: Per Windows captures, ALL coefficients/deadband/center MUST be + * zero */ + /* Only saturation values control conditional effect behavior */ + p->right_sat = saturation; + p->left_sat = saturation; +} + +/* + * Scale constant force level from SDL format to device format. + * + * Per the T500RS USB protocol documentation: + * - SDL2 level: 0-65535 (unsigned) + * - Device level: -127 to +127 (signed 8-bit) + * - Formula: device_level = (sdl_level * 255 / 65535) - 127 + * + * This maps: + * SDL 0 → Device -127 (max negative) + * SDL 32767 → Device 0 (neutral) + * SDL 65535 → Device +127 (max positive) + */ +static inline s8 t500rs_scale_constant_level(u16 sdl_level) { + s32 tmp = ((s32)sdl_level * 255) / 65535; + return (s8)(tmp - 127); +} + +/* + * Build a 0x03 constant force packet. + * + * Per the T500RS USB protocol documentation: + * - code: low byte of param_subtype from 0x01 (e.g., 0x0e) + * - reserved: always 0x00 + * - level: signed -127 to +127 + */ +static void t500rs_build_r03_constant(struct t500rs_r03_const *p, u8 code, + s8 level) { + p->id = T500RS_PKT_CONSTANT; + p->code = code; + p->zero = 0x00; + p->level = level; +} + +/* + * Scale envelope level from SDL format to device format. + * SDL: 0-32767 + * Device: 0-255 + * Formula: device_level = sdl_level * 255 / 32767 + */ +static inline u8 t500rs_scale_envelope_level(u16 sdl_level) { + /* Input validation and clamping */ + if (sdl_level > 32767) + sdl_level = 32767; + + /* Use 32-bit arithmetic to prevent overflow */ + return (u8)((sdl_level * 255LL) / 32767); +} + +/* + * Build a protocol-accurate 0x02 envelope packet. + * + * Per the T500RS USB protocol documentation: + * - subtype: low byte of env_sub from 0x01 (e.g., 0x1c) + * - attack_len: attack duration in milliseconds + * - attack_level: 0-255 (scaled from SDL 0-32767) + * - fade_len: fade duration in milliseconds + * - fade_level: 0-255 (scaled from SDL 0-32767) + * - reserved: always 0x00 + */ +static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, + u8 subtype, + const struct ff_envelope *env, + bool allow_nonzero) { + memset(p, 0, sizeof(*p)); + p->id = 0x02; + p->subtype = subtype; + + /* + * CRITICAL FIRMWARE BUG WORKAROUND: + * Per T500RS_USB_Protocol_Analysis.md, the device firmware rejects + * non-zero envelope values for periodic and constant effects with + * EPROTO (-71). Only ramp effects can safely use envelopes. + * + * Windows driver always sends zeros for periodic/constant: + * 02 38 00 00 00 00 00 00 00 + */ + if (env && allow_nonzero) { + p->attack_len = cpu_to_le16(env->attack_length); + p->attack_level = t500rs_scale_envelope_level(env->attack_level); + p->fade_len = cpu_to_le16(env->fade_length); + p->fade_level = t500rs_scale_envelope_level(env->fade_level); + } else if (env && !allow_nonzero) { + /* + * User requested envelope but device doesn't support it. + * Log once to inform user, then send zeros. + */ + pr_warn_once("t500rs: Envelope requested but not supported for this effect type\n"); + } + + p->reserved = 0x00; +} + +/* Supported parameters */ +const unsigned long t500rs_params = PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | + PARAM_FRICTION_LEVEL | PARAM_GAIN | + PARAM_RANGE; + +/* + * Supported effects. + * + * NOTE: FF_SQUARE is intentionally OMITTED. Per Windows USB captures, the + * T500RS protocol does not encode waveform type in USB packets. Windows/SDL2 + * may emulate square waves in software, but the device hardware appears to + * only support the base waveforms. Rather than silently map to sine (which + * would feel wrong to users), we reject FF_SQUARE and let applications fall + * back to alternative effects. + */ +const signed short t500rs_effects[] = { + FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, + FF_PERIODIC, FF_SINE, FF_TRIANGLE, FF_SAW_UP, FF_SAW_DOWN, + FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; + +/* Forward declarations to avoid implicit declarations before worker uses them + */ +static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, + size_t len); +static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, + u8 hw_effect_id); +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); + +/* + * Send a sequence of packets for effect upload. + * Abstracts the hardcoded packet orders in upload functions. + */ +static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state, + u8 hw_id, + const enum t500rs_seq_packet *sequence, + size_t seq_len) { + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; + int ret; + u16 param_sub, env_sub; + + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, + effect->type != FF_CONSTANT); + + for (size_t i = 0; i < seq_len; i++) { + /* Log sequence progress for debugging */ + T500RS_DBG(t500rs, "Sequence step %zu/%zu: packet type 0x%02x\n", + i + 1, seq_len, sequence[i]); + + switch (sequence[i]) { + case T500RS_SEQ_STOP: + ret = t500rs_send_stop(t500rs, hw_id); + break; + case T500RS_SEQ_SYNC_42_05: + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_hid(t500rs, buf, 2); + break; + case T500RS_SEQ_SYNC_42_04: + buf[0] = 0x42; + buf[1] = 0x04; + ret = t500rs_send_hid(t500rs, buf, 2); + break; + case T500RS_SEQ_ENVELOPE: { + struct t500rs_pkt_r02_envelope *env = + (struct t500rs_pkt_r02_envelope *)buf; + const struct ff_envelope *envelope = NULL; + bool allow_envelope = false; + + if (effect->type == FF_RAMP) { + envelope = &effect->u.ramp.envelope; + allow_envelope = true; /* Ramp supports envelope */ + } else if (effect->type == FF_CONSTANT || effect->type == FF_PERIODIC) { + envelope = &effect->u.periodic.envelope; + allow_envelope = false; /* Firmware bug: must send zeros */ + } + + t500rs_build_r02_envelope(env, (u8)(env_sub & 0xff), envelope, allow_envelope); + } + ret = + t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r02_envelope)); + break; + case T500RS_SEQ_CONSTANT: { + s8 level = t500rs_scale_const_with_direction(effect->u.constant.level, + effect->direction); + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; + t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), level); + } + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); + break; + case T500RS_SEQ_PERIODIC_RAMP: + if (effect->type == FF_RAMP) { + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), + effect->u.ramp.start_level, + effect->u.ramp.end_level, effect->replay.length); + } else { + u8 mag = t500rs_scale_periodic_magnitude(effect->u.periodic.magnitude); + u8 phase = t500rs_scale_periodic_phase(effect->u.periodic.phase); + s8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); + u16 period_ms = effect->u.periodic.period; + if (period_ms == 0) + period_ms = 100; + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, + period_ms); + } + ret = t500rs_send_hid(t500rs, buf, + sizeof(struct t500rs_pkt_r04_periodic_ramp)); + break; + case T500RS_SEQ_CONDITION_X: { + u8 saturation = 0; + switch (effect->type) { + case FF_SPRING: + saturation = T500RS_SAT_SPRING; + break; + case FF_DAMPER: + saturation = T500RS_SAT_DAMPER; + break; + case FF_FRICTION: + saturation = T500RS_SAT_FRICTION; + break; + case FF_INERTIA: + saturation = T500RS_SAT_INERTIA; + break; + default: + saturation = T500RS_SAT_DAMPER; + break; + } + struct t500rs_pkt_r05_condition *p = + (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), saturation); + } + ret = + t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); + break; + case T500RS_SEQ_CONDITION_Y: { + u8 saturation = 0; + switch (effect->type) { + case FF_SPRING: + saturation = T500RS_SAT_SPRING; + break; + case FF_DAMPER: + saturation = T500RS_SAT_DAMPER; + break; + case FF_FRICTION: + saturation = T500RS_SAT_FRICTION; + break; + case FF_INERTIA: + saturation = T500RS_SAT_INERTIA; + break; + default: + saturation = T500RS_SAT_DAMPER; + break; + } + struct t500rs_pkt_r05_condition *p = + (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), saturation); + } + ret = + t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); + break; + case T500RS_SEQ_MAIN: { + u8 effect_type = 0; + switch (effect->type) { + case FF_CONSTANT: + effect_type = T500RS_EFFECT_CONSTANT; + break; + case FF_SPRING: + effect_type = T500RS_EFFECT_SPRING; + break; + case FF_DAMPER: + effect_type = T500RS_EFFECT_DAMPER; + break; + case FF_FRICTION: + effect_type = T500RS_EFFECT_FRICTION; + break; + case FF_INERTIA: + effect_type = T500RS_EFFECT_INERTIA; + break; + case FF_PERIODIC: + switch (effect->u.periodic.waveform) { + case FF_SINE: + effect_type = T500RS_EFFECT_SINE; + break; + case FF_TRIANGLE: + effect_type = T500RS_EFFECT_TRIANGLE; + break; + case FF_SAW_UP: + effect_type = T500RS_EFFECT_SAW_UP; + break; + case FF_SAW_DOWN: + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + return -EINVAL; + } + break; + case FF_RAMP: + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + return -EINVAL; + } + u16 duration_ms = effect->replay.length ? effect->replay.length : 0xffff; + u16 delay_ms = effect->replay.delay; + struct t500rs_pkt_r01_main *m = (struct t500rs_pkt_r01_main *)buf; + ret = t500rs_build_r01_main(m, hw_id, effect_type, duration_ms, delay_ms, + param_sub, env_sub); + if (ret) + break; + } + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r01_main)); + break; + default: + ret = -EINVAL; + } + if (ret) { + hid_err(t500rs->hdev, + "Sequence failed at step %zu/%zu (packet type 0x%02x): %d\n", + i + 1, seq_len, sequence[i], ret); + return ret; + } + } + + T500RS_DBG(t500rs, "Sequence completed successfully (%zu packets)\n", seq_len); + return 0; +} + +static int t500rs_set_gain(void *data, u16 gain) { + struct t500rs_device_entry *t500rs = data; + u8 *buf; + u8 device_gain_byte; + int ret; + + /* Input validation */ + if (!data) { + pr_err("t500rs_set_gain: NULL data pointer\n"); + return -ENODEV; + } + + /* Validate gain range */ + if (gain > T500RS_GAIN_MAX) { + hid_err(t500rs->hdev, "Gain %u exceeds maximum %d\n", gain, + T500RS_GAIN_MAX); + return -EINVAL; + } + + t500rs = data; + + if (!t500rs->send_buffer) { + hid_err(t500rs->hdev, "t500rs_set_gain: NULL send buffer\n"); + return -ENOMEM; + } + + /* Bounds check buffer size */ + if (t500rs->buffer_length < 2) { + hid_err(t500rs->hdev, "t500rs_set_gain: Buffer too small (%zu < 2)\n", + t500rs->buffer_length); + return -ENOMEM; + } + + buf = t500rs->send_buffer; + + /* Scale 0..65535 to device 0..255 with bounds checking */ + if (gain > T500RS_GAIN_MAX) { + hid_warn(t500rs->hdev, + "t500rs_set_gain: Gain %u exceeds maximum %d, clamping\n", gain, + T500RS_GAIN_MAX); + gain = T500RS_GAIN_MAX; + } + device_gain_byte = (u8)((gain * 255ULL) / T500RS_GAIN_MAX); + + hid_info(t500rs->hdev, "FFB: set_gain %u -> device %u\n", gain, + device_gain_byte); + + /* Safe buffer access with bounds checking */ + buf[0] = T500RS_PKT_GAIN; + buf[1] = device_gain_byte; + + ret = t500rs_send_hid(t500rs, buf, 2); + if (ret == 0) + hid_info(t500rs->hdev, "FFB: Gain set successfully\n"); + else + hid_err(t500rs->hdev, "FFB: Failed to set gain: %d\n", ret); + return ret; +} + +/* Send data via HID output report (blocking) */ +static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, + size_t len) { + int ret; + + /* Input validation */ + if (!t500rs) { + pr_err("t500rs_send_hid: NULL device entry\n"); + return -ENODEV; + } + if (!data) { + hid_err(t500rs->hdev, "t500rs_send_hid: NULL data buffer\n"); + return -EINVAL; + } + if (len == 0 || len > T500RS_BUFFER_LENGTH) { + hid_err(t500rs->hdev, "t500rs_send_hid: Invalid length %zu (max %d)\n", len, + T500RS_BUFFER_LENGTH); + return -EINVAL; + } + + ret = hid_hw_output_report(t500rs->hdev, (u8 *)data, len); + if (ret < 0) { + hid_err(t500rs->hdev, "HID output report failed: %d\n", ret); + return ret; + } + if (ret != len) { + hid_err(t500rs->hdev, + "HID output report truncated: sent %d, expected %zu\n", ret, len); + return -EIO; + } + return 0; +} + +/* + * Send STOP command for a specific hardware effect ID. + * Used both for pre-upload clearing and explicit stop. + * Per protocol: 0x41 effect_id command arg + * command = 0x00 for STOP, 0x41 for START + */ +static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, + u8 hw_effect_id) { + 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 = hw_effect_id; + r41->command = 0x00; /* STOP */ + r41->arg = 0x01; + return t500rs_send_hid(t500rs, buf, sizeof(*r41)); +} + +/* + * Send START command for a specific hardware effect ID. + */ +static inline int t500rs_send_start(struct t500rs_device_entry *t500rs, + u8 hw_effect_id) { + u8 *buf; + struct t500rs_r41_cmd *r41; + if (!t500rs) + return -ENODEV; + /* Bounds check buffer size for 4-byte packets */ + if (t500rs->buffer_length < 4) { + hid_err(t500rs->hdev, "t500rs_set_autocenter: Buffer too small (%zu < 4)\n", + t500rs->buffer_length); + return -ENOMEM; + } + + buf = t500rs->send_buffer; + r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; + r41->effect_id = hw_effect_id; + r41->command = 0x41; /* START */ + r41->arg = 0x01; + return t500rs_send_hid(t500rs, buf, sizeof(*r41)); +} + +/* 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; + int ret; + int hw_id; + int level = effect->u.constant.level; + + /* Note: Gain is applied in play_effect, not here */ + T500RS_DBG(t500rs, "Upload constant: id=%d, level=%d, dir=%u\n", effect->id, + level, effect->direction); + + /* Allocate a hardware effect ID for this logical effect. + * Constant effects CAN use index 0 (subtypes 0x0e/0x1c) per Windows captures. + */ + hw_id = t500rs_alloc_hw_id(t500rs, effect->id, false); + if (hw_id < 0) + return hw_id; + + /* Send packet sequence for constant effect */ + ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_constant, + sizeof(t500rs_seq_constant) / + sizeof(t500rs_seq_constant[0])); + if (ret) { + hid_err(t500rs->hdev, "Failed to send constant effect sequence: %d\n", ret); + return ret; + } + + T500RS_DBG(t500rs, "Constant effect %d uploaded (hw_id=%d)\n", effect->id, + hw_id); + return 0; +} + +/* + * Upload spring/damper/friction/inertia effect. + * + * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * - 0x01 packet: direction=0x4000, code1=0x002a, code2=0x0038 + * - Two 0x05 packets: X-axis (code 0x2a) and Y-axis (code 0x38) + * - Saturation values 0x54 (84) for spring, 0x64 (100) for damper/friction + */ +static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + u8 effect_gain; + const char *type_name; + // cond variable no longer needed after protocol fix - saturation is + // calculated per effect type + + /* + * Determine effect type code and gain level. + * Per Windows captures: Spring=0x40, Damper/Friction/Inertia=0x41 + */ + u8 effect_type; + switch (effect->type) { + case FF_SPRING: + type_name = "spring"; + effect_gain = spring_level; + effect_type = T500RS_EFFECT_SPRING; + break; + case FF_DAMPER: + type_name = "damper"; + effect_gain = damper_level; + effect_type = T500RS_EFFECT_DAMPER; + break; + case FF_FRICTION: + type_name = "friction"; + effect_gain = friction_level; + effect_type = T500RS_EFFECT_FRICTION; + break; + case FF_INERTIA: + type_name = "inertia"; + effect_gain = 100; + effect_type = T500RS_EFFECT_INERTIA; + break; + default: + return -EINVAL; + } + + /* + * Allocate a hardware effect ID for this conditional effect. + * Conditional effects MUST skip index 0 - the device rejects 0x05 packets + * with index 0 subtypes (EPROTO). Per Windows captures, all conditional + * effects use index 1+ (subtypes 0x2a/0x38 or higher). + */ + hw_id = t500rs_alloc_hw_id(t500rs, effect->id, true); + if (hw_id < 0) { + hid_err(t500rs->hdev, "Failed to allocate hw_id for %s effect %d\n", + type_name, effect->id); + return hw_id; + } + + /* Send packet sequence for conditional effect */ + ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_condition, + sizeof(t500rs_seq_condition) / + sizeof(t500rs_seq_condition[0])); + if (ret) { + hid_err(t500rs->hdev, "Failed to send %s effect sequence: %d\n", type_name, + ret); + return ret; + } + + return 0; +} + +/* + * Upload periodic effect (sine, square, triangle, saw). + * + * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * - Waveform type is NOT encoded in USB packets; determined by SDL2/DirectInput + * - 0x01 packet: direction, duration, delay, code1=0x000e, code2=0x001c + * - 0x02 packet: envelope with subtype 0x1c + * - 0x04 packet: code=0x2a (NOT 0x0e!), magnitude, offset, phase, period_ms + * - Period is in MILLISECONDS (no Hz*100 conversion) + * + * NOTE: The current implementation only sends the simplified packet sequence + * observed in Windows captures. The dual-0x01/0x02 sequence in the old code + * may have been incorrect and is removed. + */ +static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + const char *type_name; + u8 effect_type; + u8 mag, phase, offset; + u16 direction_dev, duration_ms, delay_ms; + u16 period_ms; + + /* + * Determine waveform name and effect_type for 0x01 packet. + * + * Per Windows captures, waveform type IS encoded in the 0x01 packet's + * effect_type field (byte 2). We only support the waveforms observed in + * captures. + * + * FF_SQUARE is rejected because it's not in our supported effects list. + * + * Per Windows captures, effect_type values for periodic: + * - 0x21 = Triangle (inferred from protocol pattern) + * - 0x22 = Sine (confirmed from analysis.json) + * - 0x23 = Sawtooth Up (inferred) + * - 0x24 = Sawtooth Down (inferred) + */ + switch (effect->u.periodic.waveform) { + case FF_TRIANGLE: + type_name = "triangle"; + effect_type = T500RS_EFFECT_TRIANGLE; + break; + case FF_SINE: + type_name = "sine"; + effect_type = T500RS_EFFECT_SINE; + break; + case FF_SAW_UP: + type_name = "sawtooth_up"; + effect_type = T500RS_EFFECT_SAW_UP; + break; + case FF_SAW_DOWN: + type_name = "sawtooth_down"; + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + /* FF_SQUARE and other unsupported waveforms */ + hid_err(t500rs->hdev, "Unsupported periodic waveform: %d\n", + effect->u.periodic.waveform); + return -EINVAL; + } + + /* Allocate a hardware effect ID for this logical effect. + * Periodic effects MUST skip index 0 - the device rejects 0x04 packets + * with index 0 subtypes (EPROTO). Per Windows captures, all periodic + * effects use index 1+ (subtypes 0x2a/0x38 or higher). + */ + hw_id = t500rs_alloc_hw_id(t500rs, effect->id, true); + if (hw_id < 0) { + hid_err(t500rs->hdev, "Failed to allocate hw_id for %s effect %d\n", + type_name, effect->id); + return hw_id; + } + + /* Scale parameters using new protocol-accurate helpers */ + mag = t500rs_scale_periodic_magnitude(effect->u.periodic.magnitude); + phase = t500rs_scale_periodic_phase(effect->u.periodic.phase); + offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); + direction_dev = t500rs_scale_direction(effect->direction); + duration_ms = effect->replay.length ? effect->replay.length : 0xffff; + delay_ms = effect->replay.delay; + period_ms = effect->u.periodic.period; + if (period_ms == 0) + period_ms = 100; /* Default 100ms if not specified */ + + /* Send packet sequence for periodic effect */ + ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_periodic, + sizeof(t500rs_seq_periodic) / + sizeof(t500rs_seq_periodic[0])); + if (ret) { + hid_err(t500rs->hdev, "Failed to send %s effect sequence: %d\n", type_name, + ret); + return ret; + } + + T500RS_DBG(t500rs, "%s effect %d uploaded\n", type_name, effect->id); + return 0; +} + +/* + * Upload ramp effect. + * + * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * - Ramp uses same 0x04 packet structure as periodic (code 0x2a) + * - Packet sequence: 0x01 + 0x02 + 0x04 + 0x41 + * - Start/end levels encoded in magnitude/offset fields + * - Period field encodes ramp duration + */ +static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, + const struct tmff2_effect_state *state) { + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + + /* Allocate a hardware effect ID for this logical effect. + * Ramp effects MUST skip index 0 - the device rejects 0x04 packets + * with index 0 subtypes (EPROTO). Per Windows captures, all ramp + * effects use index 1+ (subtypes 0x2a/0x38 or higher). + */ + hw_id = t500rs_alloc_hw_id(t500rs, effect->id, true); + if (hw_id < 0) { + hid_err(t500rs->hdev, "Failed to allocate hw_id for ramp effect %d\n", + effect->id); + return hw_id; + } + + /* Send packet sequence for ramp effect */ + ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_ramp, + sizeof(t500rs_seq_ramp) / + sizeof(t500rs_seq_ramp[0])); + if (ret) { + hid_err(t500rs->hdev, "Failed to send ramp effect sequence: %d\n", ret); + return ret; + } + + T500RS_DBG(t500rs, "Ramp effect %d uploaded\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; + int ret; + + /* Input validation */ + if (!data) { + pr_err("t500rs_upload_effect: NULL data pointer\n"); + return -ENODEV; + } + if (!state) { + pr_err("t500rs_upload_effect: NULL state pointer\n"); + return -EINVAL; + } + + t500rs = data; + effect = &state->effect; + + /* Validate effect ID range */ + if (effect->id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", effect->id, + T500RS_MAX_EFFECTS); + return -EINVAL; + } + + /* Validate effect parameters based on type */ + switch (effect->type) { + case FF_CONSTANT: + /* Validate constant force level */ + if (effect->u.constant.level < -32767 || effect->u.constant.level > 32767) { + hid_err(t500rs->hdev, "Constant level %d out of range [-32767, 32767]\n", + effect->u.constant.level); + return -EINVAL; + } + break; + + case FF_PERIODIC: + /* Validate periodic effect parameters */ + if (effect->u.periodic.magnitude < 0 || + effect->u.periodic.magnitude > 32767) { + hid_err(t500rs->hdev, "Periodic magnitude %d out of range [0, 32767]\n", + effect->u.periodic.magnitude); + return -EINVAL; + } + if (effect->u.periodic.offset < -32768 || + effect->u.periodic.offset > 32767) { + hid_err(t500rs->hdev, "Periodic offset %d out of range [-32768, 32767]\n", + effect->u.periodic.offset); + return -EINVAL; + } + if (effect->u.periodic.phase > 35999) { + hid_err(t500rs->hdev, "Periodic phase %u exceeds maximum 35999\n", + effect->u.periodic.phase); + return -EINVAL; + } + break; + + case FF_RAMP: + /* Validate ramp effect parameters */ + if (effect->u.ramp.start_level < -32767 || + effect->u.ramp.start_level > 32767) { + hid_err(t500rs->hdev, + "Ramp start level %d out of range [-32767, 32767]\n", + effect->u.ramp.start_level); + return -EINVAL; + } + if (effect->u.ramp.end_level < -32767 || effect->u.ramp.end_level > 32767) { + hid_err(t500rs->hdev, "Ramp end level %d out of range [-32767, 32767]\n", + effect->u.ramp.end_level); + return -EINVAL; + } + break; + + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + break; + + default: + hid_err(t500rs->hdev, "Unsupported effect type: %d\n", effect->type); + return -EINVAL; + } + + /* Validate common parameters */ + if (effect->direction > 35999) { + hid_err(t500rs->hdev, "Direction %u exceeds maximum 35999\n", + effect->direction); + return -EINVAL; + } + if (effect->replay.delay > 65535) { + hid_err(t500rs->hdev, "Delay %u exceeds maximum 65535\n", + effect->replay.delay); + return -EINVAL; + } + + switch (effect->type) { + case FF_CONSTANT: + ret = t500rs_upload_constant(t500rs, state); + break; + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + ret = t500rs_upload_condition(t500rs, state); + break; + case FF_PERIODIC: + case FF_SINE: + case FF_TRIANGLE: + case FF_SAW_UP: + case FF_SAW_DOWN: + ret = t500rs_upload_periodic(t500rs, state); + break; + case FF_RAMP: + ret = t500rs_upload_ramp(t500rs, state); + break; + default: + hid_err(t500rs->hdev, "Unsupported effect type: %d\n", effect->type); + return -EINVAL; + } + + if (ret < 0) { + hid_err(t500rs->hdev, "Failed to upload effect type %d, id %d: %d\n", + effect->type, effect->id, ret); + } + return ret; +} + +/* + * Play effect - send START command (0x41) for the effect. + * For constant force, also sends a level update (0x03) before START. + */ +static int t500rs_play_effect(void *data, + const struct tmff2_effect_state *state) { + struct t500rs_device_entry *t500rs = data; + const struct ff_effect *effect; + u8 *buf; + int ret; + int hw_id; + + /* Input validation */ + if (!data) { + pr_err("t500rs_play_effect: NULL data pointer\n"); + return -ENODEV; + } + if (!state) { + pr_err("t500rs_play_effect: NULL state pointer\n"); + return -EINVAL; + } + + t500rs = data; + effect = &state->effect; + + /* Validate effect ID range */ + if (effect->id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", effect->id, + T500RS_MAX_EFFECTS); + return -EINVAL; + } + + /* Validate effect type is supported */ + switch (effect->type) { + case FF_CONSTANT: + case FF_PERIODIC: + case FF_RAMP: + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + break; + default: + hid_err(t500rs->hdev, "Unsupported effect type for play: %d\n", + effect->type); + return -EINVAL; + } + + if (!t500rs->send_buffer) { + hid_err(t500rs->hdev, "t500rs_play_effect: NULL send buffer\n"); + return -ENOMEM; + } + + buf = t500rs->send_buffer; + + hw_id = t500rs_get_hw_id(t500rs, effect->id); + if (hw_id < 0) { + hid_err(t500rs->hdev, "Failed to get hw_id for effect %d: %d\n", effect->id, + hw_id); + return hw_id; + } + + /* For constant force: send level update (0x03) before START */ + if (effect->type == FF_CONSTANT) { + int level = effect->u.constant.level; + u16 direction = effect->direction; + s8 signed_level; + u16 param_sub, env_sub; + + signed_level = t500rs_scale_const_with_direction(level, direction); + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, + false); /* Constant effects use 0x0e base */ + + { + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; + t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), signed_level); + } + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); + if (ret) + return ret; + } + + /* START command uses hw_id as effect_id to match 0x01 packet */ + ret = t500rs_send_start(t500rs, (u8)hw_id); + if (ret == 0) { + T500RS_DBG(t500rs, "Started effect %d (hw_id=%d)\n", effect->id, hw_id); + t500rs_debug_active_effects(t500rs); + } + return ret; +} + +/* + * Stop effect - send STOP command (0x41) and free hardware slot. + */ +static int t500rs_stop_effect(void *data, + const struct tmff2_effect_state *state) { + struct t500rs_device_entry *t500rs = data; + int ret; + int hw_id; + + /* Input validation */ + if (!data) { + pr_err("t500rs_stop_effect: NULL data pointer\n"); + return -ENODEV; + } + if (!state) { + pr_err("t500rs_stop_effect: NULL state pointer\n"); + return -EINVAL; + } + + t500rs = data; + + /* Validate effect ID range */ + if (state->effect.id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", state->effect.id, + T500RS_MAX_EFFECTS); + return -EINVAL; + } + + if (!t500rs->send_buffer) { + hid_err(t500rs->hdev, "t500rs_stop_effect: NULL send buffer\n"); + return -ENOMEM; + } + + hw_id = t500rs_get_hw_id(t500rs, state->effect.id); + if (hw_id < 0) + return 0; /* Effect was never uploaded */ + + /* STOP command uses hw_id as effect_id to match 0x01 packet */ + ret = t500rs_send_stop(t500rs, (u8)hw_id); + + t500rs_free_hw_id(t500rs, state->effect.id); + + return ret; +} + +/* Update effect - send parameter updates without re-uploading */ +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; + int hw_id; + + if (!t500rs) + return -ENODEV; + + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + + hw_id = t500rs_get_hw_id(t500rs, effect->id); + if (hw_id < 0) + return 0; /* Effect not uploaded yet */ + + switch (effect->type) { + case FF_CONSTANT: { + int level = effect->u.constant.level; + u16 direction = effect->direction; + + if (level == old->u.constant.level && direction == old->direction) + return 0; + + s8 signed_level = t500rs_scale_const_with_direction(level, direction); + u16 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, + false); /* Constant effects use 0x0e base */ + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; + t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), signed_level); + return t500rs_send_hid(t500rs, (u8 *)r3, sizeof(*r3)); + } + case FF_PERIODIC: { + u8 mag = t500rs_scale_periodic_magnitude(effect->u.periodic.magnitude); + u8 phase = t500rs_scale_periodic_phase(effect->u.periodic.phase); + u8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); + u16 period_ms = effect->u.periodic.period; + u16 param_sub, env_sub; + if (period_ms == 0) + period_ms = 100; + + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, + true); /* Periodic effects use 0x2a base */ + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, + period_ms); + return t500rs_send_hid(t500rs, buf, sizeof(*p)); + } + case FF_RAMP: { + u16 duration_ms = effect->replay.length ? effect->replay.length : 1000; + u16 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, + true); /* Ramp effects use 0x2a base */ + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), effect->u.ramp.start_level, + effect->u.ramp.end_level, duration_ms); + return t500rs_send_hid(t500rs, buf, sizeof(*p)); + } + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: { + /* + * Skip update if parameters unchanged - prevents micro-pulse/rumble + * when games spam identical condition updates. + */ + const struct ff_condition_effect *cond = &effect->u.condition[0]; + const struct ff_condition_effect *cond_old = &old->u.condition[0]; + u16 param_sub, env_sub; + + if (cond->right_coeff == cond_old->right_coeff && + cond->left_coeff == cond_old->left_coeff && + cond->right_saturation == cond_old->right_saturation && + cond->left_saturation == cond_old->left_saturation && + cond->deadband == cond_old->deadband && + cond->center == cond_old->center && effect->type == old->type) + return 0; + + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, + true); /* Conditional effects use 0x2a base */ + /* Calculate saturation value based on effect type */ + u8 saturation; + switch (effect->type) { + case FF_SPRING: + saturation = T500RS_SAT_SPRING; + break; + case FF_DAMPER: + saturation = T500RS_SAT_DAMPER; + break; + case FF_FRICTION: + saturation = T500RS_SAT_FRICTION; + break; + case FF_INERTIA: + saturation = T500RS_SAT_INERTIA; + break; + default: + saturation = T500RS_SAT_DAMPER; /* Default to damper level */ + break; + } + struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), saturation); + return t500rs_send_hid(t500rs, buf, sizeof(*p)); + } + default: + 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); + + /* 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; + + 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_hid(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_hid(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_hid(t500rs, buf, 4); + if (ret) + return ret; + } + + /* Apply settings: Report 0x42 0x05 */ + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_hid(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; + + /* Input validation */ + if (!data) { + pr_err("t500rs_set_range: NULL data pointer\n"); + return -ENODEV; + } + + /* Validate range - minimum 40°, maximum 1080° */ + if (range < T500RS_RANGE_MIN) { + hid_err(t500rs->hdev, "Range %u below minimum %d degrees\n", range, + T500RS_RANGE_MIN); + return -EINVAL; + } + if (range > T500RS_RANGE_MAX) { + hid_err(t500rs->hdev, "Range %u exceeds maximum %d degrees\n", range, + T500RS_RANGE_MAX); + return -EINVAL; + } + + t500rs = data; + + /* Bounds check buffer size for 4-byte packets */ + if (t500rs->buffer_length < 4) { + hid_err(t500rs->hdev, "t500rs_set_range: Buffer too small (%zu < 4)\n", + t500rs->buffer_length); + return -ENOMEM; + } + + /* Use DMA-safe preallocated buffer */ + buf = t500rs->send_buffer; + + T500RS_DBG(t500rs, "Setting wheel range to %u degrees\n", range); + + /* Device expects LITTLE-ENDIAN and value = range * 60. */ + range_value = range * 60; + + /* 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_hid(t500rs, buf, 4); + if (ret) { + hid_err(t500rs->hdev, "Failed to send range command: %d\n", ret); + return ret; + } + + /* Store current range */ + t500rs->current_range = range; + + /* Apply settings with Report 0x42 0x05 */ + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_hid(t500rs, buf, 2); + if (ret) { + hid_err(t500rs->hdev, "Failed to apply range settings: %d\n", ret); + return ret; + } + + T500RS_DBG(t500rs, "Range set to %u degrees (final value=0x%04x)\n", range, + 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 = NULL; + u8 *init_buf; /* Will use send_buffer for DMA-safe transfers */ + int ret; + int i; + + /* Sanity check protocol main-upload packet size against documentation */ + BUILD_BUG_ON(sizeof(struct t500rs_pkt_r01_main) != 15); + + /* Validate input parameters */ + if (!tmff2) { + pr_err("t500rs_wheel_init: NULL tmff2 structure\n"); + return -EINVAL; + } + if (!tmff2->hdev || !tmff2->input_dev) { + pr_err("t500rs_wheel_init: Invalid tmff2 structure (missing hdev or " + "input_dev)\n"); + return -EINVAL; + } + + hid_dbg(tmff2->hdev, "T500RS: Initializing HID mode\n"); + + /* Allocate device data */ + t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); + if (!t500rs) { + hid_err(tmff2->hdev, "Failed to allocate t500rs device structure\n"); + ret = -ENOMEM; + goto err_alloc; + } + + /* Initialize device structure */ + t500rs->hdev = tmff2->hdev; + t500rs->input_dev = tmff2->input_dev; + t500rs->current_range = 900; /* Default range: 900° */ + + /* Allocate send buffer with bounds checking */ + t500rs->buffer_length = T500RS_BUFFER_LENGTH; + if (t500rs->buffer_length == 0 || t500rs->buffer_length > 4096) { + hid_err(tmff2->hdev, "Invalid buffer length: %zu\n", t500rs->buffer_length); + ret = -EINVAL; + goto err_buffer_alloc; + } + + t500rs->send_buffer = kzalloc(t500rs->buffer_length, GFP_KERNEL); + if (!t500rs->send_buffer) { + hid_err(tmff2->hdev, "Failed to allocate send buffer (%zu bytes)\n", + t500rs->buffer_length); + ret = -ENOMEM; + goto err_buffer_alloc; + } + + /* Initialize hardware ID mapping and slot bitmap */ + bitmap_zero(t500rs->hw_slots_in_use, T500RS_MAX_HW_EFFECTS); + for (i = 0; i < T500RS_MAX_EFFECTS; i++) { + t500rs->hw_id_map[i] = T500RS_MAX_HW_EFFECTS; /* Invalid initial value */ + } + + /* Initialize spinlock for thread safety */ + spin_lock_init(&t500rs->hw_id_lock); + + /* Store device data in tmff2 BEFORE any operations that might fail */ + tmff2->data = t500rs; + + /* Use send_buffer for all HID transfers */ + init_buf = t500rs->send_buffer; + + T500RS_DBG(t500rs, "Sending initialization sequence...\n"); + + /* Report 0x42 - Init/status commands (2 bytes each) + * Windows sends these at startup: 0x42 0x04, 0x42 0x05, 0x42 0x00 + * These appear to initialize the FFB subsystem state. + */ + memset(init_buf, 0, 2); + init_buf[0] = 0x42; + init_buf[1] = 0x04; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) { + hid_warn(t500rs->hdev, "Init command 0x42 0x04 failed: %d\n", ret); + } + + memset(init_buf, 0, 2); + init_buf[0] = 0x42; + init_buf[1] = 0x05; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) { + hid_warn(t500rs->hdev, "Init command 0x42 0x05 failed: %d\n", ret); + } + + memset(init_buf, 0, 2); + init_buf[0] = 0x42; + init_buf[1] = 0x00; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) { + hid_warn(t500rs->hdev, "Init command 0x42 0x00 failed: %d\n", ret); + } + + /* 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; + init_buf[2] = 0x42; + init_buf[3] = 0x7b; + ret = t500rs_send_hid(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 2 (0x40 enable) failed: %d\n", ret); + } + + /* 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. + * Keep explicit zeros even though memset() clears them, to document the + * wire image. + */ + init_buf[2] = 0x00; + init_buf[3] = 0x00; + ret = t500rs_send_hid(t500rs, init_buf, 4); + if (ret) { + hid_warn(t500rs->hdev, "Init command 3 (0x40 config) failed: %d\n", ret); + } + + /* 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_hid(t500rs, init_buf, 2); + if (ret) { + hid_warn(t500rs->hdev, "Init command 4 (0x43) failed: %d\n", ret); + } + + /* The remaining initialization (0x05 spring zeroing and 0x41 STOP for + * autocenter ID 15) is handled below. + */ + + /* Report 0x05 - Set deadband and center */ + memset(init_buf, 0, 11); + 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_hid(t500rs, init_buf, 11); + if (ret) { + hid_warn(t500rs->hdev, "Disable autocenter (0x05 0x1c) failed: %d\n", ret); + } + + /* Stop autocenter effect (effect ID 15) */ + { + 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_hid(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); + if (ret) { + hid_warn(t500rs->hdev, "Stop autocenter effect failed: %d\n", ret); + } else { + T500RS_DBG(t500rs, "Autocenter fully disabled\n"); + } + + hid_info(t500rs->hdev, "T500RS initialized successfully (HID mode)\n"); + T500RS_DBG(t500rs, "Buffer: %zu bytes\n", 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_alloc: + /* t500rs structure is allocated but not yet stored in tmff2->data */ + kfree(t500rs); +err_alloc: + return ret; +} + +/* Cleanup T500RS device */ +static int t500rs_wheel_destroy(void *data) { + struct t500rs_device_entry *t500rs = data; + + if (!t500rs) { + pr_warn("t500rs_wheel_destroy: NULL data pointer\n"); + return 0; + } + + T500RS_DBG(t500rs, "T500RS: Cleaning up\n"); + + /* Free resources in reverse order of allocation */ + if (t500rs->send_buffer) { + kfree(t500rs->send_buffer); + t500rs->send_buffer = NULL; + } + + /* Clear the tmff2 data pointer to prevent use-after-free */ + if (t500rs->hdev && t500rs->hdev->driver_data) { + /* Note: We don't clear tmff2->data here as it's handled by the caller */ + } + + kfree(t500rs); + + return 0; +} + +/* Populate API callbacks */ +int t500rs_populate_api(struct tmff2_device_entry *tmff2) { + + 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->wheel_init = t500rs_wheel_init; + tmff2->wheel_destroy = t500rs_wheel_destroy; + + return 0; +} diff --git a/src/tmt500rs/t500rs_protocol.h b/src/tmt500rs/t500rs_protocol.h new file mode 100644 index 00000000..f3f46a93 --- /dev/null +++ b/src/tmt500rs/t500rs_protocol.h @@ -0,0 +1,222 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * T500RS Force Feedback Protocol Constants and Structures + * + * This header defines all protocol-specific constants and packet structures + * for the Thrustmaster T500RS racing wheel force feedback implementation. + */ + +#ifndef __T500RS_PROTOCOL_H +#define __T500RS_PROTOCOL_H + +#include + +/* Packet type constants */ +#define T500RS_PKT_MAIN 0x01 +#define T500RS_PKT_ENVELOPE 0x02 +#define T500RS_PKT_CONSTANT 0x03 +#define T500RS_PKT_PERIODIC 0x04 +#define T500RS_PKT_CONDITIONAL 0x05 +#define T500RS_PKT_COMMAND 0x41 +#define T500RS_PKT_STATUS 0x42 +#define T500RS_PKT_GAIN 0x43 + +/* Packet code constants */ +#define T500RS_CODE_CONSTANT 0x0e +#define T500RS_CODE_PERIODIC 0x2a +#define T500RS_CODE_ENVELOPE 0x1c +#define T500RS_CODE_CONDITIONAL_X 0x2a +#define T500RS_CODE_CONDITIONAL_Y 0x38 + +/* Control and command constants */ +#define T500RS_CONTROL_DEFAULT 0x40 +#define T500RS_CMD_START 0x41 +#define T500RS_CMD_STOP 0x00 +#define T500RS_CMD_ARG 0x01 + +/* Effect type constants */ +#define T500RS_EFFECT_CONSTANT 0x00 +#define T500RS_EFFECT_SINE 0x22 +#define T500RS_EFFECT_TRIANGLE 0x21 +#define T500RS_EFFECT_SAW_UP 0x23 +#define T500RS_EFFECT_SAW_DOWN 0x24 +#define T500RS_EFFECT_SPRING 0x40 +#define T500RS_EFFECT_DAMPER 0x41 +#define T500RS_EFFECT_FRICTION 0x41 +#define T500RS_EFFECT_INERTIA 0x41 + +/* Saturation values for conditional effects */ +#define T500RS_SAT_SPRING 84 +#define T500RS_SAT_DAMPER 100 +#define T500RS_SAT_FRICTION 100 +#define T500RS_SAT_INERTIA 100 + +/* Hardware limits */ +#define T500RS_MAX_EFFECTS 16 +#define T500RS_MAX_HW_EFFECTS T500RS_MAX_EFFECTS +#define T500RS_BUFFER_LENGTH 32 /* HID report max packet size */ +#define T500RS_HID_TIMEOUT 1000 /* 1 second */ + +/* Gain scaling */ +#define T500RS_GAIN_MAX 65535 + +/* Range limits */ +#define T500RS_RANGE_MIN 40 /* Minimum range: 40 degrees */ +#define T500RS_RANGE_MAX 1080 /* Maximum range: 1080 degrees */ + +/* + * Packet Sequence Abstraction Enums + * + * These enums define the packet sequencing abstraction for effect uploads. + * Used internally by the sequencing system to manage packet order. + */ +enum t500rs_seq_packet { + T500RS_SEQ_STOP, + T500RS_SEQ_SYNC_42_05, + T500RS_SEQ_SYNC_42_04, + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_CONSTANT, + T500RS_SEQ_PERIODIC_RAMP, + T500RS_SEQ_CONDITION_X, + T500RS_SEQ_CONDITION_Y, + T500RS_SEQ_MAIN, +}; + +/* + * Packet Sequence Templates + * + * These arrays define the packet sequences for different effect types. + * Used by the packet sequencing abstraction system. + */ +/* Sequence templates are now static in the implementation file */ + +/* Supported parameters and effects */ +extern const unsigned long t500rs_params; +extern const signed short t500rs_effects[]; + +/* + * T500RS USB Protocol Packet Structures + * + * These structures define the wire format for T500RS force feedback packets. + * All structures are packed to match the exact USB protocol format. + * + * Packet formats verified against Windows USB captures in: + * docs/T500RS_USB_Protocol_Analysis.md + */ + +/* + * 0x01 - Main upload packet (15 bytes) + * + * This packet initiates effect upload and specifies packet sequence. + * Verified against Windows USB captures - all fields match observed traffic. + * + * Packet format: + * - b0: packet type (0x01) + * - b1: hardware effect slot ID (0-15, assigned by driver) + * - b2: effect type (T500RS_EFFECT_* constants) + * - b3: control flags (always 0x40) + * - b4-b5: duration in milliseconds (LE) + * - b6-b7: delay before start in milliseconds (LE) + * - b8: reserved (0x00) + * - b9-b10: parameter packet subtype (LE) - determines 0x03/0x04/0x05 codes + * - b11-b12: envelope packet subtype (LE) - determines 0x02 code + * - b13-b14: reserved (0x0000) + */ +struct t500rs_pkt_r01_main { + u8 id; /* b0: T500RS_PKT_MAIN */ + u8 effect_id; /* b1: hardware effect slot ID (0-15) */ + u8 effect_type; /* b2: effect type (T500RS_EFFECT_*) */ + u8 control; /* b3: always T500RS_CONTROL_DEFAULT (0x40) */ + __le16 duration_ms; /* b4-b5: duration in ms (LE) */ + __le16 delay_ms; /* b6-b7: delay before start in ms (LE) */ + u8 reserved1; /* b8: 0x00 */ + __le16 packet_code_1; /* b9-b10: param subtype for 0x03/0x04/0x05 (LE) */ + __le16 packet_code_2; /* b11-b12: env subtype for 0x02 (LE) */ + __le16 reserved2; /* b13-b14: 0x0000 */ +} __packed; + +/* + * 0x04 - Periodic / Ramp parameters (8 bytes) + * + * Used for both periodic effects (sine, triangle, sawtooth) and ramp effects. + * Code field must match the subtype specified in 0x01 packet bytes 9-10. + * + * Packet format (verified against Windows captures): + * - b0: packet type (0x04) + * - b1: subtype code (from 0x01 packet_code_1, typically 0x2a) + * - b2: reserved (0x00) + * - b3: magnitude (0-127, scaled from SDL 0-32767) + * - b4: offset (signed -127 to +127, scaled from SDL -32768 to +32767) + * - b5: phase (0-255 for 360°, scaled from SDL 0-35999) + * - b6-b7: period in milliseconds (LE, no Hz conversion!) + * + * For ramp effects: phase=0, period=ramp duration, magnitude/offset encode start/end levels. + */ +struct t500rs_pkt_r04_periodic_ramp { + u8 id; /* b0: T500RS_PKT_PERIODIC */ + u8 code; /* b1: subtype code (from 0x01 packet_code_1) */ + u8 reserved1; /* b2: always 0x00 */ + u8 magnitude; /* b3: 0..127 magnitude (scaled) */ + u8 offset; /* b4: signed -127..+127 offset (scaled) */ + u8 phase; /* b5: 0..255 phase (0-360 degrees) */ + __le16 period_ms; /* b6-b7: period in milliseconds (LE) */ +} __packed; + +/* + * 0x05 - Conditional parameters (11 bytes) + * + * CRITICAL: Windows captures show that conditional effects require TWO 0x05 packets, + * but coefficients/deadband/center MUST be zero. Only saturation values control behavior. + * + * Packet format (verified against Windows captures): + * - b0: packet type (0x05) + * - b1: subtype code (from 0x01 packet_code_1 or packet_code_2) + * - b2-b3: right coefficient (LE) - MUST BE ZERO + * - b4-b5: left coefficient (LE) - MUST BE ZERO + * - b6-b7: deadband (LE) - MUST BE ZERO + * - b8: center - MUST BE ZERO + * - b9: right saturation (0-100, controls spring/damper strength) + * - b10: left saturation (0-100, controls spring/damper strength) + * + * Firmware rejects packets with non-zero coefficients, causing EPROTO errors. + * Effect behavior is controlled solely through saturation values. + */ +struct t500rs_pkt_r05_condition { + u8 id; /* T500RS_PKT_CONDITIONAL */ + u8 code; /* from 0x01 code1/code2 (T500RS_CODE_*) */ + __le16 right_coeff; /* MUST BE ZERO */ + __le16 left_coeff; /* MUST BE ZERO */ + __le16 deadband; /* MUST BE ZERO */ + u8 center; /* MUST BE ZERO */ + u8 right_sat; /* 0-100: controls effect strength */ + u8 left_sat; /* 0-100: controls effect strength */ +} __packed; + +/* 0x03 - Constant force level (4 bytes) */ +struct t500rs_r03_const { + u8 id; /* T500RS_PKT_CONSTANT */ + u8 code; /* T500RS_CODE_CONSTANT */ + u8 zero; /* 0x00 */ + s8 level; /* -127..127 */ +} __packed; + +/* 0x41 - START/STOP command (4 bytes) */ +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; + +/* 0x02 - Envelope packet (9 bytes) */ +struct t500rs_pkt_r02_envelope { + u8 id; /* 0x02 */ + u8 subtype; /* from 0x01 code2 (env_sub low byte) */ + __le16 attack_len; /* attack duration in ms */ + u8 attack_level; /* 0-255 */ + __le16 fade_len; /* fade duration in ms */ + u8 fade_level; /* 0-255 */ + u8 reserved; /* 0x00 */ +} __packed; + +#endif /* __T500RS_PROTOCOL_H */ \ No newline at end of file diff --git a/udev/71-thrustmaster-steamdeck.rules b/udev/71-thrustmaster-steamdeck.rules index 3f3ea1e6..1d1e42b0 100644 --- a/udev/71-thrustmaster-steamdeck.rules +++ b/udev/71-thrustmaster-steamdeck.rules @@ -20,3 +20,6 @@ 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 e96cb025..25b8b596 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 648bbcdc8198491364f6adf0a959acbfe1a9446b Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Wed, 10 Dec 2025 11:27:18 +0100 Subject: [PATCH 02/15] Major pass of improvements based on PR feedback - Simplified effect slot system - Added support of direction - Tidy-up code here and there - Reduced complexity overhead - Code standardization (in regards of the base repo) - Documentation updates --- docs/T500RS_USB_Protocol_Analysis.md | 205 +--- src/hid-tmff2.c | 49 +- src/hid-tmff2.h | 5 +- src/tmt500rs/hid-tmt500rs.c | 910 +++++------------- .../{t500rs_protocol.h => hid-tmt500rs.h} | 17 +- 5 files changed, 266 insertions(+), 920 deletions(-) rename src/tmt500rs/{t500rs_protocol.h => hid-tmt500rs.h} (94%) diff --git a/docs/T500RS_USB_Protocol_Analysis.md b/docs/T500RS_USB_Protocol_Analysis.md index 84edfd3b..7ea5fa9f 100644 --- a/docs/T500RS_USB_Protocol_Analysis.md +++ b/docs/T500RS_USB_Protocol_Analysis.md @@ -25,7 +25,7 @@ This is the result of the deep analysis of captures made using ffbsdl tool on wi ## Subtype System and Effect Indexing -On this wheel the last six bytes of the 0x01 main upload (bytes 9–14) do **not** contain envelope timings/levels directly. Instead they carry two 16‑bit "subtype" values that act as per‑effect indices: +On this wheel the last six bytes of the 0x01 main upload (bytes 9–14) do **not** contain envelope timings/levels directly compared to the T300RS. Instead they carry two 16‑bit "subtype" values that act as per‑effect indices: - Bytes 9–10 → `parameter_subtype` - Bytes 11–12 → `envelope_subtype` @@ -357,12 +357,12 @@ Offset | Size | Field | Description | Scenario | Description | Effect IDs Used | Packet Sequence | |----------|-------------|-----------------|-----------------| -| **Constant → Sine** | Constant 1500ms, then Sine 1500ms | 0x0000, then 0x0021 | Effect 1 (ID=0x00): Upload→Envelope→Constant→START→[wait]→STOP
Effect 2 (ID=0x21): Upload→Envelope→Periodic→START→[wait]→STOP | -| **Sine → Triangle** | Sine 3000ms, then Triangle 3000ms | 0x0000, then 0x0022 | Effect 1 (ID=0x00): Upload→Envelope→Periodic→START→[wait]→STOP
Effect 2 (ID=0x22): Upload→Envelope→Periodic→START→[wait]→STOP | +| **Constant → Sine** | Constant 1500ms, then Sine 1500ms | 0x00, then 0x01 | Effect 1 (ID=0x00): Upload→Envelope→Constant→START→[wait]→STOP
Effect 2 (ID=0x01): Upload→Envelope→Periodic→START→[wait]→STOP | +| **Sine → Triangle** | Sine 3000ms, then Triangle 3000ms | 0x00, then 0x01 | Effect 1 (ID=0x00): Upload→Envelope→Periodic→START→[wait]→STOP
Effect 2 (ID=0x01): Upload→Envelope→Periodic→START→[wait]→STOP | **Sequential Effect Notes:** - Each effect gets full upload sequence with unique effect ID -- Windows driver uses non-sequential IDs: 0x00, 0x21, 0x22, 0x40, 0x41 +- Effect IDs are hardware slot numbers in the range 0-15 (0x00-0x0F) - Previous effect must be stopped before starting next - Gap between effects depends on timing in test @@ -370,15 +370,14 @@ Offset | Size | Field | Description | Scenario | Description | Effect IDs Used | Packet Sequence | |----------|-------------|-----------------|-----------------| -| **Sine + Triangle Overlap** | Sine starts, Triangle joins after 1s, both run for 2s | 0x0040, 0x0041 | Effect 1 (ID=0x40): Upload→Envelope→Periodic→START
[wait 1s]
Effect 2 (ID=0x41): Upload→Envelope→Periodic→START
[wait 2s]
Effect 1 (ID=0x40): STOP
Effect 2 (ID=0x41): STOP | +| **Sine + Triangle Overlap** | Sine starts, Triangle joins after 1s, both run for 2s | 0x00, 0x01 | Effect 1 (ID=0x00): Upload→Envelope→Periodic→START
[wait 1s]
Effect 2 (ID=0x01): Upload→Envelope→Periodic→START
[wait 2s]
Effect 1 (ID=0x00): STOP
Effect 2 (ID=0x01): STOP | **Overlapping Effect Notes:** - T500RS supports up to 16 simultaneous effects (hardware capability) -- Windows driver assigns unique effect IDs to each concurrent effect +- Effect IDs are hardware slot numbers (0x00-0x0F, i.e., 0-15) - Effects are uploaded and started independently with different IDs - Device mixes/sums the forces internally -- Effect IDs observed: 0x0000, 0x0021, 0x0022, 0x0040, 0x0041 (non-sequential pattern) -- Driver can use simple sequential assignment (0x00, 0x01, 0x02, etc.) instead +- Driver uses sequential assignment (0x00, 0x01, 0x02, etc.) for simplicity #### Rapid Sequential Effects @@ -471,193 +470,3 @@ Offset | Size | Field | Description - SDL 16000 → Device 12 - SDL 24000 → Device 18 - SDL 32767 → Device 255 - ---- - -## Important implementation facts - -### 1. Packet Code Discrepancy - -**FIXED:** Driver updated to use correct packet codes (0x2a instead of 0x0e) for periodic/conditional effects. - -**Original Issue:** Windows captures consistently show code **0x2a** in 0x04 periodic packets and 0x05 conditional packets, but the original driver used **0x0e**. - -**Resolution Applied:** -- ✅ Updated `struct t500rs_pkt_r04_periodic_ramp` to use variable code from 0x01 packet -- ✅ Implemented `struct t500rs_pkt_r05_condition` with two packets (codes 0x2a and 0x38) -- ✅ Updated 0x01 packet bytes 9-10 to use 0x002a for periodic/conditional effects -- ✅ Updated 0x01 packet bytes 11-12 to use 0x0038 for conditional effects -- ✅ All packet codes dynamically determined from 0x01 packet bytes 9-12 - -### 2. Period Encoding -**IMPORTANT:** Keep period in **milliseconds**, do NOT convert to Hz×100! -- Windows captures confirm: period values match milliseconds directly - -### 3. Effect ID and Slot Management - -**IMPLEMENTED:** Driver uses unique hardware IDs for concurrent effects to prevent slot collision. - -**Hardware Capability:** T500RS supports up to 16 simultaneous effects with internal mixing. -- **Single Effect:** Windows driver uses effect_id = 0x0000 for isolated testing -- **Multi-Effect:** Driver assigns unique effect IDs for concurrent effects (0-15 range) -- **Hardware Behavior:** Device maintains 16 effect slots internally with automatic force mixing - -**Driver Implementation Applied:** -- ✅ Hardware ID allocation system (0-15) prevents slot collision -- ✅ Unique effect_ids assigned to concurrent effects -- ✅ START/STOP commands use matching effect_id from 0x01 packet -- ✅ Spinlock protection for thread-safe hardware ID operations - -**Resolution Details:** -- Constant effects: effect_id = hw_id (0-15) with subtypes 0x0e/0x1c -- Conditional effects: effect_id = hw_id (0-15) with subtypes 0x2a/0x38 -- Periodic effects: effect_id = hw_id (0-15) with subtypes 0x2a/0x38 -- Hardware ID bitmap tracks occupied slots to prevent overwrites - -### 4. Envelope Flag -- 0x01 packet byte 11-12: 0x001c when envelope is present -- 0x0000 when no envelope -- Envelope packet (0x02) should always be sent, even if all zeros - -### 5. Telemetry Packets (0x07) -- High frequency position feedback from device -- 15 bytes: `07 [pos_lo] [pos_hi] 03 ff 03 ff 03 00 00 00 00 00 0f` -- Position appears to be 16-bit value in bytes 1-2 -- Not required for effect upload, but useful for force feedback tuning - -### 6. Polling Packets (0x49) -- Device sends these frequently during operation -- Two formats seen: - - Short: `49 00 00 00 00 10 00` (7 bytes) - - Long: `49 00 00 00 01 00 02 00 03 00 00 00 02 02 00 00` (16 bytes) -- Appear to be status/heartbeat packets -- Driver should handle/ignore these gracefully - -### 7. Wine Compatibility Considerations -- **Autocenter Hack:** Some games under Wine (e.g., Live for Speed) set autocenter to 100% permanently, which overpowers other force feedback effects and forces centered wheel all the time the game is started. The Linux driver includes a compatibility hack that ignores 100% autocenter requests while allowing 0-99% values to work normally. -- **Effect Behavior:** Wine applications may exhibit different force feedback behavior due to DirectInput vs SDL2 differences, but the underlying USB protocol remains the same. - ---- - -## Driver Implementation Sequence - -### For Constant Force Effect: -1. Build 0x01 packet (direction, duration, effect_code=0x0e, envelope_flag=0x1c) -2. Build 0x02 packet (attack/fade parameters, scaled to 0-255) -3. Build 0x03 packet (force level, scaled to -127..+127) -4. Send packets in order: 0x01 → 0x02 → 0x03 -5. Build 0x41 packet (START command) -6. Send 0x41 packet -7. [Effect runs] -8. Build 0x41 packet (STOP command, byte 2 = 0x00) -9. Send 0x41 packet - -### For Periodic Effect (Sine/Triangle/Sawtooth): -1. Build 0x01 packet (direction, duration, effect_code=0x0e, envelope_flag=0x1c) -2. Build 0x02 packet (attack/fade parameters, scaled to 0-255) -3. Build 0x04 packet (magnitude, offset, phase, period - **use code 0x2a!**) -4. Send packets in order: 0x01 → 0x02 → 0x04 -5. Build 0x41 packet (START command) -6. Send 0x41 packet -7. [Effect runs] -8. Build 0x41 packet (STOP command, byte 2 = 0x00) -9. Send 0x41 packet - -### For Ramp Effect: -1. Same as periodic effect (uses 0x04 packet) -2. Encode start/end levels in magnitude/offset fields -3. Period field may control ramp rate - ---- - -## Protocol Analysis Summary - -### Confirmed Protocol Features - -1. **Variable Packet Codes:** - - Bytes 9-12 of 0x01 packet specify codes for subsequent packets - - NOT fixed values - varies between captures (0x2a/0x38, 0xb6/0xc4, 0x46/0x54, etc.) - - Driver must use the codes specified in the 0x01 packet - -2. **Conditional Effect Structure:** - - TWO 0x05 packets required per conditional effect - - First packet uses code from bytes 9-10 of 0x01 (X-axis parameters) - - Second packet uses code from bytes 11-12 of 0x01 (Y-axis parameters) - - **⚠️ CRITICAL:** Both packets must have ZERO coefficients/deadband/center - only saturation is set - - Sending non-zero coefficients causes EPROTO errors and device rejection - -3. **Effect Slot Management:** - - Hardware supports 16 concurrent effect slots - - Windows driver uses non-sequential IDs: 0x0000, 0x0040, 0x0041, 0x0021, 0x0022 - - Effect IDs observed in range 0x00-0xff - - Device handles internal mixing of concurrent effects - -4. **Period Encoding:** - - Period is in MILLISECONDS (not Hz×100) - - No conversion needed - pass through directly - -5. **Gain Handling:** - - In-game gain (SDL2/DirectInput) is applied BEFORE sending to driver - - USB packets contain already-scaled values - - Device gain (sysfs) is separate and applied by hardware - - No gain parameter in USB protocol - -6. **Square Wave:** - - NOT SUPPORTED by T500RS hardware (as per captures) - - No 0x04 packets generated for square wave effect type - - SDL2 square wave requests produce no USB traffic - -### 📊 Parameter Scaling (Confirmed from captures) - -**Constant Force:** -- SDL2 level (0-65535) → Device level (-127 to +127) -- Formula: `device_level = (sdl_level * 255 / 65535) - 127` - -**Periodic Effects:** -- Magnitude (0-32767) → Device magnitude (0-127) -- Formula: `device_mag = sdl_mag * 127 / 32767` -- Phase (0-35999, 0.01° units) → Device phase (0-255) -- Formula: `device_phase = (sdl_phase * 256 / 36000) & 0xff` - -**Envelope:** -- Attack/Fade level (0-32767) → Device level (0-255) -- Formula: `device_level = sdl_level * 255 / 32767` - -**Conditional Effects:** -- Saturation values observed: 0x54 (spring), 0x64 (damper) -- **⚠️ CRITICAL:** Coefficient, deadband, and center must be sent as ZEROS -- The device firmware rejects 0x05 packets with non-zero coefficients -- Effect behavior is controlled solely through saturation values (0-100 range) - -### ⚠️ Areas Needing More Data - -1. **Conditional Parameter Scaling:** - - Exact formulas for coefficient, deadband, center encoding - - Most test captures show zero values for these parameters - - Need captures with varied conditional parameters - -2. **Ramp Effect Encoding:** - - How ramp_start and ramp_end map to 0x04 packet fields - - All ramp captures show identical packets - - Test application may not be passing ramp parameters correctly - -3. **Inertia and Friction Effects:** - - Limited packet examples for these effect types - - Assumed to use same 0x05 structure as spring/damper - - Need verification with actual captures - -### Optional Improvements - -1. **Conditional Parameter Scaling:** - - Implement best-guess scaling for coefficient, deadband, center - - Test with real hardware to verify behavior - - Adjust formulas based on testing results - -2. **Ramp Effect Support:** - - Implement ramp using 0x04 packet - - Map start/end to magnitude/offset fields - - Verify behavior on hardware - -3. **Multi-Effect Testing:** - - Test concurrent effect playback - - Verify effect mixing behavior - - Test effect ID reuse after stopping - diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index ac52cda0..bc494a63 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -243,6 +243,9 @@ static ssize_t gain_store(struct device *dev, gain = value; + if (!tmff2->set_gain) + return count; + /* 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`. @@ -255,19 +258,17 @@ static ssize_t gain_store(struct device *dev, * * 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. + * - docs/T500RS_USB_Protocol_Analysis.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); - 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); - } - + unsigned long flags; + spin_lock_irqsave(&tmff2->lock, flags); + tmff2->pending_gain_value = GAIN_MAX; + __set_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); + spin_unlock_irqrestore(&tmff2->lock, flags); + if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) + schedule_delayed_work(&tmff2->work, 0); return count; } @@ -294,7 +295,7 @@ static void tmff2_set_gain(struct input_dev *dev, uint16_t value) /* Defer to workqueue: store pending gain and schedule */ spin_lock_irqsave(&tmff2->lock, flags); tmff2->pending_gain_value = value; - tmff2->gain_pending = 1; + __set_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); spin_unlock_irqrestore(&tmff2->lock, flags); if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) @@ -317,7 +318,7 @@ static void tmff2_set_autocenter(struct input_dev *dev, uint16_t value) /* Defer to workqueue: store pending autocenter and schedule */ spin_lock_irqsave(&tmff2->lock, flags); tmff2->pending_autocenter_value = value; - tmff2->autocenter_pending = 1; + __set_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags); spin_unlock_irqrestore(&tmff2->lock, flags); if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) @@ -344,14 +345,14 @@ static void tmff2_work_handler(struct work_struct *w) uint16_t pg = 0, pac = 0; int do_gain = 0, do_ac = 0; spin_lock_irqsave(&tmff2->lock, f2); - if (tmff2->gain_pending) { + if (test_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags)) { pg = tmff2->pending_gain_value; - tmff2->gain_pending = 0; + __clear_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); do_gain = 1; } - if (tmff2->autocenter_pending) { + if (test_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags)) { pac = tmff2->pending_autocenter_value; - tmff2->autocenter_pending = 0; + __clear_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags); do_ac = 1; } spin_unlock_irqrestore(&tmff2->lock, f2); @@ -376,26 +377,14 @@ 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) >= (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); diff --git a/src/hid-tmff2.h b/src/hid-tmff2.h index 30cdcea7..c605a32d 100644 --- a/src/hid-tmff2.h +++ b/src/hid-tmff2.h @@ -29,6 +29,8 @@ extern int alt_mode; #define FF_EFFECT_QUEUE_STOP 2 #define FF_EFFECT_QUEUE_UPDATE 3 #define FF_EFFECT_PLAYING 4 +#define FF_EFFECT_QUEUE_GAIN 5 +#define FF_EFFECT_QUEUE_AUTOCENTER 6 #define PARAM_SPRING_LEVEL (1 << 0) #define PARAM_DAMPER_LEVEL (1 << 1) @@ -67,8 +69,7 @@ struct tmff2_device_entry { /* 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; + unsigned long pending_flags; int allow_scheduling; diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index 7a239792..a154fa21 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -22,7 +22,7 @@ */ #include "../hid-tmff2.h" -#include "t500rs_protocol.h" +#include "hid-tmt500rs.h" #include #include @@ -88,35 +88,37 @@ static inline u8 t500rs_scale_mag_u7(int magnitude) { return (u8)((magnitude * 127LL) / 32767); } -/* Map logical effect index to parameter/envelope subtypes as per protocol: - * Per protocol analysis, subtypes are calculated as: - * param_sub = 0x000e + 0x001c * idx (for ALL effects) - * env_sub = 0x001c + 0x001c * idx (envelope always uses 0x001c base) - * idx is wrapped to the hardware limit of 16 effect slots. +/* + * Map logical effect ID to hardware effect ID. + * hw_id = logical_id + 1 * - * CRITICAL: Index 0 (subtypes 0x0e/0x1c) is ONLY valid for constant effects - * Indices 1+ (subtypes 0x2a/0x38, etc.) are valid for all effect types - * Periodic/ramp effects sent with index 0 subtypes cause EPROTO + * This avoids hardware index 0 entirely, which has quirky behavior + * (only valid for constant effects). By always using indices 1-15, + * all effect types work uniformly with no special-casing needed. + * + * Trade-off: 15 effect slots instead of 16, but simpler code and + * no risk of index 0 misuse. Most games don't need 16 simultaneous effects. + */ +static inline unsigned int t500rs_logical_to_hw_id(unsigned int logical_id) { + /* Clamp to valid range: logical 0-14 -> hw 1-15 */ + if (logical_id >= T500RS_MAX_EFFECTS) + logical_id = T500RS_MAX_EFFECTS - 1; + return logical_id + 1; +} + +/* Map hardware effect index to parameter/envelope subtypes as per protocol: + * Per protocol analysis, subtypes are calculated as: + * param_sub = 0x000e + 0x001c * idx + * env_sub = 0x001c + 0x001c * idx + * idx is the hardware effect ID (1..15 with simplified architecture). */ static inline void t500rs_index_to_subtypes(unsigned int idx, u16 *param_sub, - u16 *env_sub, - bool is_periodic_or_conditional) { + u16 *env_sub) { /* Validate inputs */ if (idx >= T500RS_MAX_HW_EFFECTS) { idx = T500RS_MAX_HW_EFFECTS - 1; /* Clamp to valid range */ } - /* - * Critical protocol constraint from T500RS_USB_Protocol_Analysis.md: - * - Index 0 (subtypes 0x0e/0x1c) is ONLY valid for constant effects - * - Periodic/conditional MUST use index ≥ 1 (subtypes 0x2a+) - * - Formula is identical for all types: 0x000e + 0x001c * idx - * - Envelope uses: 0x001c + 0x001c * idx - */ - if (is_periodic_or_conditional && idx == 0) { - pr_warn_once("t500rs: Periodic/conditional effect using index 0 - device will reject!\n"); - } - *param_sub = 0x000e + (0x001c * idx); *env_sub = 0x001c + (0x001c * idx); } @@ -126,241 +128,13 @@ static inline void t500rs_index_to_subtypes(unsigned int idx, u16 *param_sub, /* T500RS device data */ struct t500rs_device_entry { - struct hid_device *hdev; - struct input_dev *input_dev; - - u8 *send_buffer; - size_t buffer_length; - - /* Current wheel range for smooth transitions */ - u16 current_range; /* Current rotation range in degrees */ - - /* - * Hardware effect ID management - Simplified Architecture (Phase 3). - * - * T500RS hardware supports up to 16 simultaneous effects with internal - * mixing. This simplified system replaces the complex three-array approach - * with: - * - * hw_id_map[logical_id] = hardware effect ID (0..15) assigned to logical slot - * hw_slots_in_use = bitmap tracking which hardware slots are occupied - * - * Benefits: Reduced complexity, better cache performance, easier debugging. - */ - u16 hw_id_map[T500RS_MAX_EFFECTS]; /* logical -> hardware mapping */ - DECLARE_BITMAP(hw_slots_in_use, - T500RS_MAX_HW_EFFECTS); /* occupied slots bitmap */ + struct hid_device *hdev; + struct input_dev *input_dev; - /* - * Thread safety - Phase 5 Security and Robustness. - * - * Hardware ID operations are not atomic and require protection against - * concurrent access from multiple threads/processes. - */ - spinlock_t hw_id_lock; /* Protects hw_id_map and hw_slots_in_use */ + u8 *send_buffer; + size_t buffer_length; }; -/* - * Allocate a hardware effect ID for the given logical effect id. - * - * Per Windows USB captures, the T500RS device has specific expectations: - * - Index 0 (subtypes 0x0e/0x1c) is ONLY valid for constant effects (0x03 - * packets) - * - Indices 1+ (subtypes 0x2a/0x38, etc.) are valid for all effect types - * - Periodic/ramp effects (0x04 packets) sent with index 0 subtypes cause - * EPROTO - * - * The skip_index_zero parameter should be true for periodic, ramp, and - * conditional effects to avoid the firmware rejecting the upload. - * - * Returns the hardware ID (0..15) on success, or -ENOSPC if all slots are used. - */ -static int t500rs_alloc_hw_id(struct t500rs_device_entry *t500rs, - unsigned int logical_id, bool skip_index_zero) { - unsigned int start_idx = skip_index_zero ? 1 : 0; - int hw_slot; - unsigned long flags; - - /* Input validation */ - if (!t500rs) { - pr_err("t500rs_alloc_hw_id: NULL device entry\n"); - return -ENODEV; - } - if (logical_id >= T500RS_MAX_EFFECTS) { - hid_err(t500rs->hdev, "Invalid logical_id %u (max %d)\n", logical_id, - T500RS_MAX_EFFECTS); - return -EINVAL; - } - - spin_lock_irqsave(&t500rs->hw_id_lock, flags); - - /* Check if this logical_id already has a hardware slot assigned */ - if (t500rs->hw_id_map[logical_id] < T500RS_MAX_HW_EFFECTS) { - hw_slot = t500rs->hw_id_map[logical_id]; - /* Verify the slot is still marked as in use */ - if (test_bit(hw_slot, t500rs->hw_slots_in_use)) { - T500RS_DBG(t500rs, "hw_id %d already allocated for logical_id %d\n", - hw_slot, logical_id); - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - return hw_slot; - } - /* Slot was freed, clear the mapping */ - t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; - } - - /* Find the first available hardware slot */ - hw_slot = bitmap_find_next_zero_area(t500rs->hw_slots_in_use, - T500RS_MAX_HW_EFFECTS, start_idx, 1, 0); - if (hw_slot >= T500RS_MAX_HW_EFFECTS) { - hid_err(t500rs->hdev, "No available hardware slots for effect %d\n", - logical_id); - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - return -ENOSPC; - } - - /* - * Mark slot as in use atomically. This is technically not needed since - * we hold hw_id_lock, but using test_and_set_bit documents the atomicity - * requirement and protects against future refactoring errors. - */ - if (test_and_set_bit(hw_slot, t500rs->hw_slots_in_use)) { - /* Should never happen - we just found this slot was free */ - hid_err(t500rs->hdev, "BUG: Slot %d was free but is now occupied\n", hw_slot); - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - return -EBUSY; - } - - t500rs->hw_id_map[logical_id] = (u16)hw_slot; - - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - - hid_info(t500rs->hdev, - "T500RS: Allocated hw_id=%d for logical_id=%d (skip_zero=%d)\n", - hw_slot, logical_id, skip_index_zero); - return hw_slot; -} - -/* - * Get the hardware effect ID for the given logical effect id. - * Allocates a new slot if one is not yet assigned. - * Returns the hardware ID (0..15) on success, or negative error. - */ -static int t500rs_get_hw_id(struct t500rs_device_entry *t500rs, - unsigned int logical_id) { - int hw_slot; - unsigned long flags; - - /* Input validation */ - if (!t500rs) { - pr_err("t500rs_get_hw_id: NULL device entry\n"); - return -ENODEV; - } - if (logical_id >= T500RS_MAX_EFFECTS) { - hid_err(t500rs->hdev, "Invalid logical_id %u (max %d)\n", logical_id, - T500RS_MAX_EFFECTS); - return -EINVAL; - } - - spin_lock_irqsave(&t500rs->hw_id_lock, flags); - - /* Check if already allocated */ - if (t500rs->hw_id_map[logical_id] < T500RS_MAX_HW_EFFECTS) { - hw_slot = t500rs->hw_id_map[logical_id]; - /* Verify slot is still in use */ - if (test_bit(hw_slot, t500rs->hw_slots_in_use)) { - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - return hw_slot; - } - /* Slot was freed, clear mapping */ - t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; - } - - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - - /* Allocate new slot - default to skip_index_zero=true for safety */ - return t500rs_alloc_hw_id(t500rs, logical_id, true); -} - -/* - * Free the hardware effect ID for the given logical effect id. - * Called from stop_effect path to recycle hardware slots. - */ -static void t500rs_free_hw_id(struct t500rs_device_entry *t500rs, - unsigned int logical_id) { - unsigned long flags; - int hw_slot; - - /* Input validation */ - if (!t500rs) { - pr_err("t500rs_free_hw_id: NULL device entry\n"); - return; - } - if (logical_id >= T500RS_MAX_EFFECTS) { - hid_err(t500rs->hdev, "Invalid logical_id %u for free operation\n", - logical_id); - return; - } - - spin_lock_irqsave(&t500rs->hw_id_lock, flags); - - /* Check if this logical ID has a hardware slot assigned */ - if (t500rs->hw_id_map[logical_id] >= T500RS_MAX_HW_EFFECTS) { - /* No slot assigned, nothing to free */ - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - return; - } - - /* Get the hardware slot and verify it's still in use */ - hw_slot = t500rs->hw_id_map[logical_id]; - if (!test_bit(hw_slot, t500rs->hw_slots_in_use)) { - hid_warn(t500rs->hdev, - "Hardware slot %d not marked as in use for logical_id %d\n", - hw_slot, logical_id); - /* Clear the mapping anyway to be safe */ - t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - return; - } - - /* Free the slot */ - clear_bit(hw_slot, t500rs->hw_slots_in_use); - t500rs->hw_id_map[logical_id] = T500RS_MAX_HW_EFFECTS; - - spin_unlock_irqrestore(&t500rs->hw_id_lock, flags); - - T500RS_DBG(t500rs, "Freed hw_id %d for logical_id %d\n", hw_slot, logical_id); -} - -/* - * Debug function to list currently active effects and their hardware slots. - * Useful for troubleshooting multi-effect scenarios. - */ -static void t500rs_debug_active_effects(struct t500rs_device_entry *t500rs) { - int logical_id; - bool has_active = false; - - if (!t500rs) { - pr_err("t500rs_debug_active_effects: NULL device entry\n"); - return; - } - - /* Iterate through logical IDs to find active mappings */ - for (logical_id = 0; logical_id < T500RS_MAX_EFFECTS; logical_id++) { - if (t500rs->hw_id_map[logical_id] < T500RS_MAX_HW_EFFECTS) { - int hw_slot = t500rs->hw_id_map[logical_id]; - if (test_bit(hw_slot, t500rs->hw_slots_in_use)) { - T500RS_DBG(t500rs, "Active effect: logical_id=%d, hw_slot=%d\n", - logical_id, hw_slot); - has_active = true; - } - } - } - - if (!has_active) { - T500RS_DBG(t500rs, "No active effects\n"); - } -} - /* * Scale direction from Linux ff_effect format to T500RS protocol format. * @@ -368,12 +142,12 @@ static void t500rs_debug_active_effects(struct t500rs_device_entry *t500rs) { * 49152 = left) T500RS protocol: 0-35999 in 0.01 degree units (0 = 0°, 9000 = * 90°, 18000 = 180°, etc.) * - * Conversion: device_dir = (linux_dir * 36000) / 65536 + * Conversion: device_dir = (os_ffb_dir * 36000) / 65536 * This maps 0-65535 → 0-35999 (approximately, since 65535 → 35999.45) */ -static inline u16 t500rs_scale_direction(u16 linux_dir) { +static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { /* Use 32-bit arithmetic to avoid overflow */ - return (u16)(((u32)linux_dir * 36000) / 65536); + return (u16)(((u32)os_ffb_dir * 36000) / 65536); } /* @@ -381,8 +155,6 @@ static inline u16 t500rs_scale_direction(u16 linux_dir) { * * Per the T500RS USB protocol documentation: * - effect_id: 16-bit LE hardware effect slot (0..15 for now) - * - direction: 0..35999 in 0.01 degree units (already scaled, use - * t500rs_scale_direction) * - duration_ms: duration in milliseconds * - delay_ms: delay before effect starts * - code1: parameter subtype (used by 0x03/0x04/0x05) @@ -397,7 +169,8 @@ static inline u16 t500rs_scale_direction(u16 linux_dir) { * - 0x40 = Spring * - 0x41 = Damper/Friction/Inertia * - * NOTE: Direction is NOT in the 0x01 packet on T500RS! + * NOTE: Direction is sent separately in a 0x03 packet for constant force, + * not in this 0x01 packet. */ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, u8 effect_type, u16 duration_ms, u16 delay_ms, @@ -451,16 +224,16 @@ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, * Per the T500RS USB protocol documentation: * - code: low byte of param_subtype from 0x01 (e.g., 0x2a for periodic, not * 0x0e!) - * - magnitude: 0..127 (scaled from SDL's 0..32767) - * - offset: signed DC offset (scaled from SDL's -32768..32767 to device range) - * - phase: 0..255 (256 steps for 360°, scaled from SDL's 0..35999) + * - magnitude: 0..127 (scaled from 0..32767) + * - offset: signed DC offset (scaled from -32768..32767 to device range) + * - phase: 0..255 (256 steps for 360°, scaled from 0..35999) * - period_ms: period in MILLISECONDS (no Hz*100 conversion!) * - reserved: always 0 * * Scaling formulas (from protocol doc): - * device_mag = sdl_mag * 127 / 32767 - * device_phase = (sdl_phase * 256 / 36000) & 0xFF - * device_offset = sdl_offset / 256 (approximate, TBD based on testing) + * device_mag = os_ffb_mag * 127 / 32767 + * device_phase = (os_ffb_phase * 256 / 36000) & 0xFF + * device_offset = os_ffb_offset / 256 (approximate, TBD based on testing) * period_ms = direct copy (no frequency conversion) */ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, @@ -481,41 +254,71 @@ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, } /* - * Scale periodic magnitude from SDL format to device format. - * SDL: 0..32767 (unsigned) - * Device: 0..127 + * Scale periodic magnitude with direction projection. + * + * For periodic effects, the direction determines the axis of oscillation. + * We project the magnitude onto the wheel axis using sin(direction). + * + * When the projected magnitude is negative, we: + * 1. Take the absolute value (wheel only supports positive magnitudes) + * 2. Add 180° to the phase to maintain correct force direction + * + * Linux FFB magnitude: 0..32767 (unsigned) + * Linux FFB direction: 0..65535 (0=forward, 16384=right, 32768=back, 49152=left) + * Linux FFB phase: 0..65535 (0..360° in 1/65536 units) + * Device magnitude: 0..127 + * + * @param os_ffb_mag: Original magnitude from Linux FFB (0..32767) + * @param direction: Effect direction from Linux FFB (0..65535) + * @param phase_ptr: Pointer to phase value; will be adjusted if projection is negative + * @return: Scaled magnitude (0..127) */ -static inline u8 t500rs_scale_periodic_magnitude(int sdl_mag) { - /* Input validation and clamping */ - if (sdl_mag < 0) - sdl_mag = -sdl_mag; - if (sdl_mag > 32767) - sdl_mag = 32767; +static inline u8 t500rs_scale_periodic_with_direction(int os_ffb_mag, + u16 direction, + u16 *phase_ptr) { + int projected; - /* Use 32-bit arithmetic to prevent overflow */ - return (u8)((sdl_mag * 127LL) / 32767); + /* Project magnitude based on direction (same formula as T300RS) */ + projected = (os_ffb_mag * fixp_sin16(direction * 360 / 0x10000)) / 0x7fff; + + if (projected < 0) { + /* Wheel handles positive magnitudes only */ + projected = -projected; + + /* Add 180° to phase to maintain correct force direction. + * Phase is in 0..65535 range (Linux FFB), 180° = 0x8000 */ + if (phase_ptr) + *phase_ptr = (*phase_ptr + 0x8000) % 0x10000; + } + + /* Clamp to valid range */ + if (projected > 32767) + projected = 32767; + + /* Scale to device range: 0..32767 -> 0..127 */ + return (u8)((projected * 127LL) / 32767); } /* - * Scale periodic phase from SDL format to device format. - * SDL: 0..35999 (0.01 degree units, 0-359.99°) + * Scale periodic phase from Linux FFB subsystem format to device format. + * Linux FFB: 0..35999 (0.01 degree units, 0-359.99°) * Device: 0..255 (256 steps for 360°) */ -static inline u8 t500rs_scale_periodic_phase(u16 sdl_phase) { +static inline u8 t500rs_scale_periodic_phase(u16 os_ffb_phase) { /* Clamp to valid range just in case */ - if (sdl_phase > 35999) - sdl_phase = 35999; - return (u8)((sdl_phase * 256) / 36000); + if (os_ffb_phase > 35999) + os_ffb_phase = 35999; + return (u8)((os_ffb_phase * 256) / 36000); } /* - * Scale periodic offset from SDL format to device format. - * SDL: -32768..32767 + * Scale periodic offset from Linux FFB subsystem format to device format. + * Linux FFB: -32768..32767 * Device: signed, stored as s8 (-128..127) * Note: exact mapping TBD based on testing; using simple /256 for now. */ -static inline s8 t500rs_scale_periodic_offset(s16 sdl_offset) { - return (s8)(sdl_offset / 256); +static inline s8 t500rs_scale_periodic_offset(s16 os_ffb_offset) { + return (s8)(os_ffb_offset / 256); } /* @@ -563,59 +366,56 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, /* * Build a 0x05 conditional effect packet. * - * CRITICAL FIRMWARE BUG WORKAROUND: - * Per Windows captures (T500RS_USB_Protocol_Analysis.md §202-213), - * the T500RS firmware has a critical bug where conditional effects - * REQUIRE two 0x05 packets, but BOTH packets must have ALL coefficients, - * deadband, and center values set to ZERO. Only saturation values should - * be non-zero. - * - * The device firmware rejects 0x05 packets with non-zero coefficients - * and causes EPROTO (-71) errors on subsequent packets. Effect behavior - * is controlled SOLELY through saturation values in the 0-100 range. + * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * - Two 0x05 packets required per conditional effect (X-axis and Y-axis) + * - Coefficients are currently sent as zero (needs more capture verification) + * - Deadband and center are now experimental - some captures show non-zero values: + * - Spring Deadband test: deadband=500 -> 0x0007, deadband=5000 -> 0x0099 + * - This suggests scaling: device_deadband = (os_ffb_deadband * 255) / 65535 * * Parameters: * - code: From 0x01 packet bytes 9-10 (first packet) or 11-12 (second packet) * - saturation: Scaled saturation value (0-100) for both right/left channels - * - * Implementation note: - * We use memset(0) to ensure all fields are zero, then only set: - * - id = 0x05 - * - code (from param_sub or env_sub) - * - right_sat = saturation - * - left_sat = saturation - * - * This ensures compliance with the firmware's strict requirements. + * - deadband: Raw deadband from ff_condition_effect (0-65535) + * - center: Raw center from ff_condition_effect (-32767 to +32767) */ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, - u8 code, u8 saturation) { - /* Zero entire structure to comply with firmware requirements */ + u8 code, u8 saturation, + u16 deadband, s16 center) { memset(p, 0, sizeof(*p)); p->id = T500RS_PKT_CONDITIONAL; p->code = code; - /* CRITICAL: Per Windows captures, ALL coefficients/deadband/center MUST be - * zero */ - /* Only saturation values control conditional effect behavior */ + /* Coefficients: keep zero for now (needs capture verification) */ + p->right_coeff = 0; + p->left_coeff = 0; + + /* + * Experimental deadband/center support + * Center: scale from -32767..+32767 to 0..255 (128 = center) + */ + p->deadband = cpu_to_le16((deadband * 255) / 65535); + p->center = (u8)(((center + 32767) * 255) / 65535); + p->right_sat = saturation; p->left_sat = saturation; } /* - * Scale constant force level from SDL format to device format. + * Scale constant force level from Linux FFB subsystem format to device format. * * Per the T500RS USB protocol documentation: - * - SDL2 level: 0-65535 (unsigned) + * - Linux FFB level: 0-65535 (unsigned) * - Device level: -127 to +127 (signed 8-bit) - * - Formula: device_level = (sdl_level * 255 / 65535) - 127 + * - Formula: device_level = (os_ffb_level * 255 / 65535) - 127 * * This maps: - * SDL 0 → Device -127 (max negative) - * SDL 32767 → Device 0 (neutral) - * SDL 65535 → Device +127 (max positive) + * Linux FFB 0 → Device -127 (max negative) + * Linux FFB 32767 → Device 0 (neutral) + * Linux FFB 65535 → Device +127 (max positive) */ -static inline s8 t500rs_scale_constant_level(u16 sdl_level) { - s32 tmp = ((s32)sdl_level * 255) / 65535; +static inline s8 t500rs_scale_constant_level(u16 os_ffb_level) { + s32 tmp = ((s32)os_ffb_level * 255) / 65535; return (s8)(tmp - 127); } @@ -636,18 +436,18 @@ static void t500rs_build_r03_constant(struct t500rs_r03_const *p, u8 code, } /* - * Scale envelope level from SDL format to device format. - * SDL: 0-32767 + * Scale envelope level from Linux FFB subsystem format to device format. + * Linux FFB : 0-32767 * Device: 0-255 - * Formula: device_level = sdl_level * 255 / 32767 + * Formula: device_level = os_ffb_level * 255 / 32767 */ -static inline u8 t500rs_scale_envelope_level(u16 sdl_level) { +static inline u8 t500rs_scale_envelope_level(u16 os_ffb_level) { /* Input validation and clamping */ - if (sdl_level > 32767) - sdl_level = 32767; + if (os_ffb_level > 32767) + os_ffb_level = 32767; /* Use 32-bit arithmetic to prevent overflow */ - return (u8)((sdl_level * 255LL) / 32767); + return (u8)((os_ffb_level * 255LL) / 32767); } /* @@ -656,9 +456,9 @@ static inline u8 t500rs_scale_envelope_level(u16 sdl_level) { * Per the T500RS USB protocol documentation: * - subtype: low byte of env_sub from 0x01 (e.g., 0x1c) * - attack_len: attack duration in milliseconds - * - attack_level: 0-255 (scaled from SDL 0-32767) + * - attack_level: 0-255 (scaled from Linux FFB 0-32767) * - fade_len: fade duration in milliseconds - * - fade_level: 0-255 (scaled from SDL 0-32767) + * - fade_level: 0-255 (scaled from Linux FFB 0-32767) * - reserved: always 0x00 */ static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, @@ -683,31 +483,27 @@ static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, p->attack_level = t500rs_scale_envelope_level(env->attack_level); p->fade_len = cpu_to_le16(env->fade_length); p->fade_level = t500rs_scale_envelope_level(env->fade_level); - } else if (env && !allow_nonzero) { + } else { /* * User requested envelope but device doesn't support it. * Log once to inform user, then send zeros. */ pr_warn_once("t500rs: Envelope requested but not supported for this effect type\n"); } - - p->reserved = 0x00; } /* Supported parameters */ -const unsigned long t500rs_params = PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | +static unsigned long t500rs_params = PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | PARAM_FRICTION_LEVEL | PARAM_GAIN | PARAM_RANGE; /* * Supported effects. * - * NOTE: FF_SQUARE is intentionally OMITTED. Per Windows USB captures, the - * T500RS protocol does not encode waveform type in USB packets. Windows/SDL2 - * may emulate square waves in software, but the device hardware appears to - * only support the base waveforms. Rather than silently map to sine (which - * would feel wrong to users), we reject FF_SQUARE and let applications fall - * back to alternative effects. + * NOTE: FF_SQUARE is intentionally OMITTED. The tool used to generate + * the Square effect was not supporting it at the time of the captures. + * Another pass of implementation will be done after that tool will support it + * and new captures are done for this effect. */ const signed short t500rs_effects[] = { FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, @@ -745,8 +541,7 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, int ret; u16 param_sub, env_sub; - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, - effect->type != FF_CONSTANT); + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); for (size_t i = 0; i < seq_len; i++) { /* Log sequence progress for debugging */ @@ -782,42 +577,50 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, } t500rs_build_r02_envelope(env, (u8)(env_sub & 0xff), envelope, allow_envelope); + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r02_envelope)); } - ret = - t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r02_envelope)); break; case T500RS_SEQ_CONSTANT: { s8 level = t500rs_scale_const_with_direction(effect->u.constant.level, effect->direction); struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), level); - } ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); + } break; - case T500RS_SEQ_PERIODIC_RAMP: - if (effect->type == FF_RAMP) { - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), - effect->u.ramp.start_level, - effect->u.ramp.end_level, effect->replay.length); - } else { - u8 mag = t500rs_scale_periodic_magnitude(effect->u.periodic.magnitude); - u8 phase = t500rs_scale_periodic_phase(effect->u.periodic.phase); - s8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); - u16 period_ms = effect->u.periodic.period; - if (period_ms == 0) - period_ms = 100; - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, - period_ms); + case T500RS_SEQ_PERIODIC_RAMP: { + if (effect->type == FF_RAMP) { + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), + effect->u.ramp.start_level, + effect->u.ramp.end_level, effect->replay.length); + } else { + /* Apply direction projection to magnitude and adjust phase if needed */ + u16 phase_raw = effect->u.periodic.phase; + u8 mag = t500rs_scale_periodic_with_direction( + effect->u.periodic.magnitude, effect->direction, &phase_raw); + u8 phase = t500rs_scale_periodic_phase(phase_raw); + s8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); + u16 period_ms = effect->u.periodic.period; + if (period_ms == 0) { + hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); + return -EINVAL; + } + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, + period_ms); + } + ret = t500rs_send_hid(t500rs, buf, + sizeof(struct t500rs_pkt_r04_periodic_ramp)); + if (ret) + break; } - ret = t500rs_send_hid(t500rs, buf, - sizeof(struct t500rs_pkt_r04_periodic_ramp)); - break; + break; case T500RS_SEQ_CONDITION_X: { u8 saturation = 0; + const struct ff_condition_effect *cond = &effect->u.condition[0]; switch (effect->type) { case FF_SPRING: saturation = T500RS_SAT_SPRING; @@ -837,13 +640,15 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, } struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), saturation); + t500rs_build_r05_condition(p, (u8)(param_sub), saturation, + cond->deadband, cond->center); + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); } - ret = - t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); break; case T500RS_SEQ_CONDITION_Y: { u8 saturation = 0; + /* Y-axis: use condition[1] if available, else zeros */ + const struct ff_condition_effect *cond = &effect->u.condition[1]; switch (effect->type) { case FF_SPRING: saturation = T500RS_SAT_SPRING; @@ -863,10 +668,10 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, } struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), saturation); + t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), saturation, + cond->deadband, cond->center); + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); } - ret = - t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); break; case T500RS_SEQ_MAIN: { u8 effect_type = 0; @@ -917,8 +722,8 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, param_sub, env_sub); if (ret) break; - } ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r01_main)); + } break; default: ret = -EINVAL; @@ -941,42 +746,14 @@ static int t500rs_set_gain(void *data, u16 gain) { u8 device_gain_byte; int ret; - /* Input validation */ - if (!data) { - pr_err("t500rs_set_gain: NULL data pointer\n"); - return -ENODEV; - } - - /* Validate gain range */ - if (gain > T500RS_GAIN_MAX) { - hid_err(t500rs->hdev, "Gain %u exceeds maximum %d\n", gain, - T500RS_GAIN_MAX); - return -EINVAL; - } - - t500rs = data; - if (!t500rs->send_buffer) { hid_err(t500rs->hdev, "t500rs_set_gain: NULL send buffer\n"); return -ENOMEM; } - /* Bounds check buffer size */ - if (t500rs->buffer_length < 2) { - hid_err(t500rs->hdev, "t500rs_set_gain: Buffer too small (%zu < 2)\n", - t500rs->buffer_length); - return -ENOMEM; - } - buf = t500rs->send_buffer; - /* Scale 0..65535 to device 0..255 with bounds checking */ - if (gain > T500RS_GAIN_MAX) { - hid_warn(t500rs->hdev, - "t500rs_set_gain: Gain %u exceeds maximum %d, clamping\n", gain, - T500RS_GAIN_MAX); - gain = T500RS_GAIN_MAX; - } + /* Scale 0..65535 to device 0..255 */ device_gain_byte = (u8)((gain * 255ULL) / T500RS_GAIN_MAX); hid_info(t500rs->hdev, "FFB: set_gain %u -> device %u\n", gain, @@ -1000,14 +777,6 @@ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, int ret; /* Input validation */ - if (!t500rs) { - pr_err("t500rs_send_hid: NULL device entry\n"); - return -ENODEV; - } - if (!data) { - hid_err(t500rs->hdev, "t500rs_send_hid: NULL data buffer\n"); - return -EINVAL; - } if (len == 0 || len > T500RS_BUFFER_LENGTH) { hid_err(t500rs->hdev, "t500rs_send_hid: Invalid length %zu (max %d)\n", len, T500RS_BUFFER_LENGTH); @@ -1059,12 +828,6 @@ static inline int t500rs_send_start(struct t500rs_device_entry *t500rs, struct t500rs_r41_cmd *r41; if (!t500rs) return -ENODEV; - /* Bounds check buffer size for 4-byte packets */ - if (t500rs->buffer_length < 4) { - hid_err(t500rs->hdev, "t500rs_set_autocenter: Buffer too small (%zu < 4)\n", - t500rs->buffer_length); - return -ENOMEM; - } buf = t500rs->send_buffer; r41 = (struct t500rs_r41_cmd *)buf; @@ -1087,12 +850,7 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, T500RS_DBG(t500rs, "Upload constant: id=%d, level=%d, dir=%u\n", effect->id, level, effect->direction); - /* Allocate a hardware effect ID for this logical effect. - * Constant effects CAN use index 0 (subtypes 0x0e/0x1c) per Windows captures. - */ - hw_id = t500rs_alloc_hw_id(t500rs, effect->id, false); - if (hw_id < 0) - return hw_id; + hw_id = t500rs_logical_to_hw_id(effect->id); /* Send packet sequence for constant effect */ ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_constant, @@ -1123,9 +881,6 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, int hw_id; u8 effect_gain; const char *type_name; - // cond variable no longer needed after protocol fix - saturation is - // calculated per effect type - /* * Determine effect type code and gain level. * Per Windows captures: Spring=0x40, Damper/Friction/Inertia=0x41 @@ -1156,18 +911,7 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, return -EINVAL; } - /* - * Allocate a hardware effect ID for this conditional effect. - * Conditional effects MUST skip index 0 - the device rejects 0x05 packets - * with index 0 subtypes (EPROTO). Per Windows captures, all conditional - * effects use index 1+ (subtypes 0x2a/0x38 or higher). - */ - hw_id = t500rs_alloc_hw_id(t500rs, effect->id, true); - if (hw_id < 0) { - hid_err(t500rs->hdev, "Failed to allocate hw_id for %s effect %d\n", - type_name, effect->id); - return hw_id; - } + hw_id = t500rs_logical_to_hw_id(effect->id); /* Send packet sequence for conditional effect */ ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_condition, @@ -1186,7 +930,7 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, * Upload periodic effect (sine, square, triangle, saw). * * Per Windows captures (T500RS_USB_Protocol_Analysis.md): - * - Waveform type is NOT encoded in USB packets; determined by SDL2/DirectInput + * - Waveform type is NOT encoded in USB packets; determined by Linux FFB subsystem * - 0x01 packet: direction, duration, delay, code1=0x000e, code2=0x001c * - 0x02 packet: envelope with subtype 0x1c * - 0x04 packet: code=0x2a (NOT 0x0e!), magnitude, offset, phase, period_ms @@ -1203,9 +947,6 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, int hw_id; const char *type_name; u8 effect_type; - u8 mag, phase, offset; - u16 direction_dev, duration_ms, delay_ms; - u16 period_ms; /* * Determine waveform name and effect_type for 0x01 packet. @@ -1246,28 +987,7 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, return -EINVAL; } - /* Allocate a hardware effect ID for this logical effect. - * Periodic effects MUST skip index 0 - the device rejects 0x04 packets - * with index 0 subtypes (EPROTO). Per Windows captures, all periodic - * effects use index 1+ (subtypes 0x2a/0x38 or higher). - */ - hw_id = t500rs_alloc_hw_id(t500rs, effect->id, true); - if (hw_id < 0) { - hid_err(t500rs->hdev, "Failed to allocate hw_id for %s effect %d\n", - type_name, effect->id); - return hw_id; - } - - /* Scale parameters using new protocol-accurate helpers */ - mag = t500rs_scale_periodic_magnitude(effect->u.periodic.magnitude); - phase = t500rs_scale_periodic_phase(effect->u.periodic.phase); - offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); - direction_dev = t500rs_scale_direction(effect->direction); - duration_ms = effect->replay.length ? effect->replay.length : 0xffff; - delay_ms = effect->replay.delay; - period_ms = effect->u.periodic.period; - if (period_ms == 0) - period_ms = 100; /* Default 100ms if not specified */ + hw_id = t500rs_logical_to_hw_id(effect->id); /* Send packet sequence for periodic effect */ ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_periodic, @@ -1298,17 +1018,7 @@ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, int ret; int hw_id; - /* Allocate a hardware effect ID for this logical effect. - * Ramp effects MUST skip index 0 - the device rejects 0x04 packets - * with index 0 subtypes (EPROTO). Per Windows captures, all ramp - * effects use index 1+ (subtypes 0x2a/0x38 or higher). - */ - hw_id = t500rs_alloc_hw_id(t500rs, effect->id, true); - if (hw_id < 0) { - hid_err(t500rs->hdev, "Failed to allocate hw_id for ramp effect %d\n", - effect->id); - return hw_id; - } + hw_id = t500rs_logical_to_hw_id(effect->id); /* Send packet sequence for ramp effect */ ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_ramp, @@ -1330,17 +1040,6 @@ static int t500rs_upload_effect(void *data, const struct ff_effect *effect; int ret; - /* Input validation */ - if (!data) { - pr_err("t500rs_upload_effect: NULL data pointer\n"); - return -ENODEV; - } - if (!state) { - pr_err("t500rs_upload_effect: NULL state pointer\n"); - return -EINVAL; - } - - t500rs = data; effect = &state->effect; /* Validate effect ID range */ @@ -1455,29 +1154,14 @@ static int t500rs_upload_effect(void *data, /* * Play effect - send START command (0x41) for the effect. - * For constant force, also sends a level update (0x03) before START. */ static int t500rs_play_effect(void *data, const struct tmff2_effect_state *state) { struct t500rs_device_entry *t500rs = data; - const struct ff_effect *effect; - u8 *buf; + const struct ff_effect *effect = &state->effect; int ret; int hw_id; - /* Input validation */ - if (!data) { - pr_err("t500rs_play_effect: NULL data pointer\n"); - return -ENODEV; - } - if (!state) { - pr_err("t500rs_play_effect: NULL state pointer\n"); - return -EINVAL; - } - - t500rs = data; - effect = &state->effect; - /* Validate effect ID range */ if (effect->id >= T500RS_MAX_EFFECTS) { hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", effect->id, @@ -1501,70 +1185,24 @@ static int t500rs_play_effect(void *data, return -EINVAL; } - if (!t500rs->send_buffer) { - hid_err(t500rs->hdev, "t500rs_play_effect: NULL send buffer\n"); - return -ENOMEM; - } - - buf = t500rs->send_buffer; - - hw_id = t500rs_get_hw_id(t500rs, effect->id); - if (hw_id < 0) { - hid_err(t500rs->hdev, "Failed to get hw_id for effect %d: %d\n", effect->id, - hw_id); - return hw_id; - } - - /* For constant force: send level update (0x03) before START */ - if (effect->type == FF_CONSTANT) { - int level = effect->u.constant.level; - u16 direction = effect->direction; - s8 signed_level; - u16 param_sub, env_sub; - - signed_level = t500rs_scale_const_with_direction(level, direction); - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, - false); /* Constant effects use 0x0e base */ + hw_id = t500rs_logical_to_hw_id(effect->id); - { - struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; - t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), signed_level); - } - ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); - if (ret) - return ret; - } - - /* START command uses hw_id as effect_id to match 0x01 packet */ ret = t500rs_send_start(t500rs, (u8)hw_id); if (ret == 0) { T500RS_DBG(t500rs, "Started effect %d (hw_id=%d)\n", effect->id, hw_id); - t500rs_debug_active_effects(t500rs); } return ret; } /* - * Stop effect - send STOP command (0x41) and free hardware slot. + * Stop effect - send STOP command (0x41). + * No slot freeing needed with simplified hw_id = logical_id + 1 mapping. */ static int t500rs_stop_effect(void *data, const struct tmff2_effect_state *state) { struct t500rs_device_entry *t500rs = data; - int ret; int hw_id; - /* Input validation */ - if (!data) { - pr_err("t500rs_stop_effect: NULL data pointer\n"); - return -ENODEV; - } - if (!state) { - pr_err("t500rs_stop_effect: NULL state pointer\n"); - return -EINVAL; - } - - t500rs = data; - /* Validate effect ID range */ if (state->effect.id >= T500RS_MAX_EFFECTS) { hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", state->effect.id, @@ -1577,16 +1215,10 @@ static int t500rs_stop_effect(void *data, return -ENOMEM; } - hw_id = t500rs_get_hw_id(t500rs, state->effect.id); - if (hw_id < 0) - return 0; /* Effect was never uploaded */ + hw_id = t500rs_logical_to_hw_id(state->effect.id); /* STOP command uses hw_id as effect_id to match 0x01 packet */ - ret = t500rs_send_stop(t500rs, (u8)hw_id); - - t500rs_free_hw_id(t500rs, state->effect.id); - - return ret; + return t500rs_send_stop(t500rs, (u8)hw_id); } /* Update effect - send parameter updates without re-uploading */ @@ -1605,9 +1237,7 @@ static int t500rs_update_effect(void *data, if (!buf) return -ENOMEM; - hw_id = t500rs_get_hw_id(t500rs, effect->id); - if (hw_id < 0) - return 0; /* Effect not uploaded yet */ + hw_id = t500rs_logical_to_hw_id(effect->id); switch (effect->type) { case FF_CONSTANT: { @@ -1619,23 +1249,26 @@ static int t500rs_update_effect(void *data, s8 signed_level = t500rs_scale_const_with_direction(level, direction); u16 param_sub, env_sub; - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, - false); /* Constant effects use 0x0e base */ + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), signed_level); return t500rs_send_hid(t500rs, (u8 *)r3, sizeof(*r3)); } case FF_PERIODIC: { - u8 mag = t500rs_scale_periodic_magnitude(effect->u.periodic.magnitude); - u8 phase = t500rs_scale_periodic_phase(effect->u.periodic.phase); + /* Apply direction projection to magnitude and adjust phase if needed */ + u16 phase_raw = effect->u.periodic.phase; + u8 mag = t500rs_scale_periodic_with_direction(effect->u.periodic.magnitude, + effect->direction, &phase_raw); + u8 phase = t500rs_scale_periodic_phase(phase_raw); u8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); u16 period_ms = effect->u.periodic.period; u16 param_sub, env_sub; - if (period_ms == 0) - period_ms = 100; + if (period_ms == 0) { + hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); + return -EINVAL; + } - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, - true); /* Periodic effects use 0x2a base */ + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); struct t500rs_pkt_r04_periodic_ramp *p = (struct t500rs_pkt_r04_periodic_ramp *)buf; t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, @@ -1643,10 +1276,13 @@ static int t500rs_update_effect(void *data, return t500rs_send_hid(t500rs, buf, sizeof(*p)); } case FF_RAMP: { - u16 duration_ms = effect->replay.length ? effect->replay.length : 1000; + u16 duration_ms = effect->replay.length; + if (duration_ms == 0) { + hid_err(t500rs->hdev, "Ramp effect duration cannot be zero\n"); + return -EINVAL; + } u16 param_sub, env_sub; - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, - true); /* Ramp effects use 0x2a base */ + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); struct t500rs_pkt_r04_periodic_ramp *p = (struct t500rs_pkt_r04_periodic_ramp *)buf; t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), effect->u.ramp.start_level, @@ -1673,8 +1309,7 @@ static int t500rs_update_effect(void *data, cond->center == cond_old->center && effect->type == old->type) return 0; - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub, - true); /* Conditional effects use 0x2a base */ + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); /* Calculate saturation value based on effect type */ u8 saturation; switch (effect->type) { @@ -1695,7 +1330,8 @@ static int t500rs_update_effect(void *data, break; } struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), saturation); + t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), saturation, + cond->deadband, cond->center); return t500rs_send_hid(t500rs, buf, sizeof(*p)); } default: @@ -1730,34 +1366,23 @@ static int t500rs_set_autocenter(void *data, u16 autocenter) { 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_hid(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_hid(t500rs, buf, 4); - if (ret) - return ret; + /* Enable autocenter: Report 0x40 0x04 0x01 */ + buf[0] = 0x40; + buf[1] = 0x04; + buf[2] = 0x01; /* Enable */ + buf[3] = 0x00; + ret = t500rs_send_hid(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_hid(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_hid(t500rs, buf, 4); + if (ret) + return ret; /* Apply settings: Report 0x42 0x05 */ buf[0] = 0x42; @@ -1776,31 +1401,12 @@ static int t500rs_set_range(void *data, u16 range) { int ret; u16 range_value; - /* Input validation */ - if (!data) { - pr_err("t500rs_set_range: NULL data pointer\n"); - return -ENODEV; - } - - /* Validate range - minimum 40°, maximum 1080° */ + /* Validate range - minimum 40 degrees, maximum 1080 degrees */ if (range < T500RS_RANGE_MIN) { - hid_err(t500rs->hdev, "Range %u below minimum %d degrees\n", range, - T500RS_RANGE_MIN); - return -EINVAL; + range = T500RS_RANGE_MIN; } if (range > T500RS_RANGE_MAX) { - hid_err(t500rs->hdev, "Range %u exceeds maximum %d degrees\n", range, - T500RS_RANGE_MAX); - return -EINVAL; - } - - t500rs = data; - - /* Bounds check buffer size for 4-byte packets */ - if (t500rs->buffer_length < 4) { - hid_err(t500rs->hdev, "t500rs_set_range: Buffer too small (%zu < 4)\n", - t500rs->buffer_length); - return -ENOMEM; + range = T500RS_RANGE_MAX; } /* Use DMA-safe preallocated buffer */ @@ -1823,9 +1429,6 @@ static int t500rs_set_range(void *data, u16 range) { return ret; } - /* Store current range */ - t500rs->current_range = range; - /* Apply settings with Report 0x42 0x05 */ buf[0] = 0x42; buf[1] = 0x05; @@ -1846,8 +1449,6 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { struct t500rs_device_entry *t500rs = NULL; u8 *init_buf; /* Will use send_buffer for DMA-safe transfers */ int ret; - int i; - /* Sanity check protocol main-upload packet size against documentation */ BUILD_BUG_ON(sizeof(struct t500rs_pkt_r01_main) != 15); @@ -1875,15 +1476,9 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { /* Initialize device structure */ t500rs->hdev = tmff2->hdev; t500rs->input_dev = tmff2->input_dev; - t500rs->current_range = 900; /* Default range: 900° */ - /* Allocate send buffer with bounds checking */ + /* Allocate send buffer */ t500rs->buffer_length = T500RS_BUFFER_LENGTH; - if (t500rs->buffer_length == 0 || t500rs->buffer_length > 4096) { - hid_err(tmff2->hdev, "Invalid buffer length: %zu\n", t500rs->buffer_length); - ret = -EINVAL; - goto err_buffer_alloc; - } t500rs->send_buffer = kzalloc(t500rs->buffer_length, GFP_KERNEL); if (!t500rs->send_buffer) { @@ -1893,15 +1488,6 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { goto err_buffer_alloc; } - /* Initialize hardware ID mapping and slot bitmap */ - bitmap_zero(t500rs->hw_slots_in_use, T500RS_MAX_HW_EFFECTS); - for (i = 0; i < T500RS_MAX_EFFECTS; i++) { - t500rs->hw_id_map[i] = T500RS_MAX_HW_EFFECTS; /* Invalid initial value */ - } - - /* Initialize spinlock for thread safety */ - spin_lock_init(&t500rs->hw_id_lock); - /* Store device data in tmff2 BEFORE any operations that might fail */ tmff2->data = t500rs; @@ -1977,39 +1563,6 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { hid_warn(t500rs->hdev, "Init command 4 (0x43) failed: %d\n", ret); } - /* The remaining initialization (0x05 spring zeroing and 0x41 STOP for - * autocenter ID 15) is handled below. - */ - - /* Report 0x05 - Set deadband and center */ - memset(init_buf, 0, 11); - 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_hid(t500rs, init_buf, 11); - if (ret) { - hid_warn(t500rs->hdev, "Disable autocenter (0x05 0x1c) failed: %d\n", ret); - } - - /* Stop autocenter effect (effect ID 15) */ - { - 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_hid(t500rs, init_buf, sizeof(struct t500rs_r41_cmd)); - if (ret) { - hid_warn(t500rs->hdev, "Stop autocenter effect failed: %d\n", ret); - } else { - T500RS_DBG(t500rs, "Autocenter fully disabled\n"); - } - hid_info(t500rs->hdev, "T500RS initialized successfully (HID mode)\n"); T500RS_DBG(t500rs, "Buffer: %zu bytes\n", t500rs->buffer_length); @@ -2044,11 +1597,6 @@ static int t500rs_wheel_destroy(void *data) { t500rs->send_buffer = NULL; } - /* Clear the tmff2 data pointer to prevent use-after-free */ - if (t500rs->hdev && t500rs->hdev->driver_data) { - /* Note: We don't clear tmff2->data here as it's handled by the caller */ - } - kfree(t500rs); return 0; diff --git a/src/tmt500rs/t500rs_protocol.h b/src/tmt500rs/hid-tmt500rs.h similarity index 94% rename from src/tmt500rs/t500rs_protocol.h rename to src/tmt500rs/hid-tmt500rs.h index f3f46a93..a5952b35 100644 --- a/src/tmt500rs/t500rs_protocol.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -6,8 +6,8 @@ * for the Thrustmaster T500RS racing wheel force feedback implementation. */ -#ifndef __T500RS_PROTOCOL_H -#define __T500RS_PROTOCOL_H +#ifndef __T500RS_H +#define __T500RS_H #include @@ -90,8 +90,7 @@ enum t500rs_seq_packet { */ /* Sequence templates are now static in the implementation file */ -/* Supported parameters and effects */ -extern const unsigned long t500rs_params; +/* Supported effects */ extern const signed short t500rs_effects[]; /* @@ -184,10 +183,10 @@ struct t500rs_pkt_r04_periodic_ramp { struct t500rs_pkt_r05_condition { u8 id; /* T500RS_PKT_CONDITIONAL */ u8 code; /* from 0x01 code1/code2 (T500RS_CODE_*) */ - __le16 right_coeff; /* MUST BE ZERO */ - __le16 left_coeff; /* MUST BE ZERO */ - __le16 deadband; /* MUST BE ZERO */ - u8 center; /* MUST BE ZERO */ + __le16 right_coeff; /* Currently zero - needs capture verification */ + __le16 left_coeff; /* Currently zero - needs capture verification */ + __le16 deadband; /* Experimental: scaled from ff_condition_effect.deadband */ + u8 center; /* Experimental: scaled from ff_condition_effect.center */ u8 right_sat; /* 0-100: controls effect strength */ u8 left_sat; /* 0-100: controls effect strength */ } __packed; @@ -219,4 +218,4 @@ struct t500rs_pkt_r02_envelope { u8 reserved; /* 0x00 */ } __packed; -#endif /* __T500RS_PROTOCOL_H */ \ No newline at end of file +#endif /* __T500RS_PROTOCOL_H */ From 3a9a60f98708f74482163e9766a42540db2b8124 Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Thu, 11 Dec 2025 15:05:06 +0100 Subject: [PATCH 03/15] Add square wave (FF_SQUARE) periodic effect support Add support for FF_SQUARE periodic waveform based on FFEdit USB captures --- src/tmt500rs/hid-tmt500rs.c | 42 ++++++++++++++++++------------------- src/tmt500rs/hid-tmt500rs.h | 1 + 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index a154fa21..b4b251fb 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -185,6 +185,7 @@ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, /* Validate effect_type against known constants */ switch (effect_type) { case T500RS_EFFECT_CONSTANT: + case T500RS_EFFECT_SQUARE: case T500RS_EFFECT_SINE: case T500RS_EFFECT_TRIANGLE: case T500RS_EFFECT_SAW_UP: @@ -497,21 +498,13 @@ static unsigned long t500rs_params = PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | PARAM_FRICTION_LEVEL | PARAM_GAIN | PARAM_RANGE; -/* - * Supported effects. - * - * NOTE: FF_SQUARE is intentionally OMITTED. The tool used to generate - * the Square effect was not supporting it at the time of the captures. - * Another pass of implementation will be done after that tool will support it - * and new captures are done for this effect. - */ +/* Supported effects. */ const signed short t500rs_effects[] = { FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, - FF_PERIODIC, FF_SINE, FF_TRIANGLE, FF_SAW_UP, FF_SAW_DOWN, - FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; + FF_PERIODIC, FF_SQUARE, FF_SINE, FF_TRIANGLE, FF_SAW_UP, + FF_SAW_DOWN, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; -/* Forward declarations to avoid implicit declarations before worker uses them - */ +/* Forward declarations to avoid implicit declarations before worker uses them */ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, size_t len); static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, @@ -693,6 +686,9 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, break; case FF_PERIODIC: switch (effect->u.periodic.waveform) { + case FF_SQUARE: + effect_type = T500RS_EFFECT_SQUARE; + break; case FF_SINE: effect_type = T500RS_EFFECT_SINE; break; @@ -952,18 +948,20 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, * Determine waveform name and effect_type for 0x01 packet. * * Per Windows captures, waveform type IS encoded in the 0x01 packet's - * effect_type field (byte 2). We only support the waveforms observed in - * captures. - * - * FF_SQUARE is rejected because it's not in our supported effects list. + * effect_type field (byte 2). * - * Per Windows captures, effect_type values for periodic: - * - 0x21 = Triangle (inferred from protocol pattern) - * - 0x22 = Sine (confirmed from analysis.json) - * - 0x23 = Sawtooth Up (inferred) - * - 0x24 = Sawtooth Down (inferred) + * Effect type values for periodic waveforms: + * - 0x20 = Square + * - 0x21 = Triangle + * - 0x22 = Sine + * - 0x23 = Sawtooth Up + * - 0x24 = Sawtooth Down */ switch (effect->u.periodic.waveform) { + case FF_SQUARE: + type_name = "square"; + effect_type = T500RS_EFFECT_SQUARE; + break; case FF_TRIANGLE: type_name = "triangle"; effect_type = T500RS_EFFECT_TRIANGLE; @@ -981,7 +979,6 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, effect_type = T500RS_EFFECT_SAW_DOWN; break; default: - /* FF_SQUARE and other unsupported waveforms */ hid_err(t500rs->hdev, "Unsupported periodic waveform: %d\n", effect->u.periodic.waveform); return -EINVAL; @@ -1131,6 +1128,7 @@ static int t500rs_upload_effect(void *data, ret = t500rs_upload_condition(t500rs, state); break; case FF_PERIODIC: + case FF_SQUARE: case FF_SINE: case FF_TRIANGLE: case FF_SAW_UP: diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index a5952b35..9cd59c90 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -36,6 +36,7 @@ /* Effect type constants */ #define T500RS_EFFECT_CONSTANT 0x00 +#define T500RS_EFFECT_SQUARE 0x20 /* Discovered from FFEdit captures (Dec 2025) */ #define T500RS_EFFECT_SINE 0x22 #define T500RS_EFFECT_TRIANGLE 0x21 #define T500RS_EFFECT_SAW_UP 0x23 From ec7a62c2366ca32c8f7df74e7d98a3cf8596d25a Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Thu, 11 Dec 2025 18:18:25 +0100 Subject: [PATCH 04/15] Fix 0x05 conditional effect packet structure and scaling + ramp Corrected packet structure and scaling formulas Added ramp support --- Kbuild | 5 +- Makefile | 24 ---- docs/T500RS_USB_Protocol_Analysis.md | 178 +++++++++++++++++++++------ src/tmt500rs/hid-tmt500rs.c | 74 ++++++----- src/tmt500rs/hid-tmt500rs.h | 48 ++++---- 5 files changed, 211 insertions(+), 118 deletions(-) diff --git a/Kbuild b/Kbuild index d1b57c14..5562215e 100644 --- a/Kbuild +++ b/Kbuild @@ -6,7 +6,4 @@ hid-tmff-new-y := \ src/tmtx/hid-tmtx.o \ src/tmtsxw/hid-tmtsxw.o \ src/tmtspc/hid-tmtspc.o \ - src/tmt500rs/hid-tmt500rs.o - -# Pass through the global TMFF2 version define from Makefile -ccflags-y += $(TMFF2_VERSION_DEF) + src/tmt500rs/hid-tmt500rs.o \ No newline at end of file diff --git a/Makefile b/Makefile index 11487feb..ad526211 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,6 @@ KDIR ?= /lib/modules/$(shell uname -r)/build -# Auto-generated global build-time version for TMFF2 -TMFF2_BASE_VERSION ?= 0.1 - -# 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) -export TMFF2_VERSION_DEF := -DTMFF2_DRIVER_VERSION=\"$(TMFF2_VERSION)\" - - all: deps/hid-tminit - @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/T500RS_USB_Protocol_Analysis.md b/docs/T500RS_USB_Protocol_Analysis.md index 7ea5fa9f..77240f75 100644 --- a/docs/T500RS_USB_Protocol_Analysis.md +++ b/docs/T500RS_USB_Protocol_Analysis.md @@ -71,7 +71,7 @@ Offset | Size | Field | Description -------|------|----------------|---------------------------------- 0 | 1 | packet_type | 0x01 1 | 1 | effect_id | Hardware effect slot ID (0-15, assigned by driver) - 2 | 1 | effect_type | Effect type (0x00=constant, 0x22=sine, 0x40=conditional) + 2 | 1 | effect_type | Effect type (see table below) 3 | 1 | control | Always 0x40 4 | 2 | duration_ms | Duration in milliseconds, little-endian 6 | 2 | delay_ms | Delay before start, little-endian @@ -83,6 +83,20 @@ Offset | Size | Field | Description **Driver Implementation Note:** effect_id must be unique for concurrent effects to prevent slot collision. Use hardware ID allocation (0-15) instead of always 0x00. +**Effect Type Codes (byte 2):** +| Code | Effect Type | Source | +|------|-------------|--------| +| 0x00 | Constant | Windows driver captures | +| 0x20 | Square | FFEdit captures (December 2025) | +| 0x21 | Triangle | Windows driver captures | +| 0x22 | Sine | Windows driver captures | +| 0x23 | Sawtooth Up | Inferred from pattern | +| 0x24 | Sawtooth Down | Inferred from pattern | +| 0x40 | Spring | Windows driver captures | +| 0x41 | Damper/Friction/Inertia | Windows driver + FFEdit captures | + +**Note:** Square wave (0x20) was discovered in FFEdit captures. The Windows driver may not expose this effect type through the standard API. + **IMPORTANT:** Bytes 9-12 specify the packet codes used in subsequent packets. These are NOT fixed values! **Common Code Combinations:** @@ -179,49 +193,35 @@ Offset | Size | Field | Description **IMPORTANT:** Conditional effects (spring, damper, inertia, friction) require TWO 0x05 packets! -**First Packet (X-axis parameters):** -``` -Offset | Size | Field | Description --------|------|----------------|---------------------------------- -0 | 1 | packet_type | 0x05 -1 | 1 | code | Variable (from 0x01 packet bytes 9-10, e.g. 0x2a or 0xb6) -2 | 2 | right_coeff | Right coefficient, little-endian -4 | 2 | left_coeff | Left coefficient, little-endian -6 | 2 | deadband | Deadband, little-endian -8 | 1 | center | Center offset -9 | 1 | right_sat | Right saturation -10 | 1 | left_sat | Left saturation -``` - -**Second Packet (Y-axis parameters):** +**Packet Structure:** ``` Offset | Size | Field | Description -------|------|----------------|---------------------------------- 0 | 1 | packet_type | 0x05 -1 | 1 | code | Variable (from 0x01 packet bytes 11-12, e.g. 0x38 or 0xc4) -2-10 | 9 | parameters | Same structure as first packet +1 | 1 | code | Variable (from 0x01 packet, e.g. 0x0e, 0x1c, 0x2a, 0x38) +2 | 1 | reserved | Always 0x00 +3 | 1 | right_coeff | Right/positive coefficient (0-10 scale, u8) +4 | 1 | left_coeff | Left/negative coefficient (0-10 scale, u8) +5-6 | 2 | center | Center offset (s16 LE, scaled: device = input/20) +7-8 | 2 | deadband | Deadband width (u16 LE, scaled: device = input/10) +9 | 1 | right_sat | Right saturation (0-100) +10 | 1 | left_sat | Left saturation (0-100) ``` -**NOTE:** T500RS is single-axis, so the second packet typically contains zeros. +**Second Packet (Y-axis):** Same structure with second code from 0x01 packet. -**⚠️ CRITICAL FINDING :** Windows sends **zero coefficients, deadband, and center** in ALL 0x05 packets! -- The device firmware appears to reject 0x05 packets with non-zero coefficients -- Only saturation values (bytes 9-10) should be non-zero -- Sending non-zero coefficients causes EPROTO (-71) errors on subsequent packets -- The conditional effect behavior is determined by saturation values, not coefficients +**NOTE:** T500RS is single-axis, so Y-axis packet typically contains zeros. -**Examples (correct - zeros for coefficients):** -- First packet: `05 2a 00 00 00 00 00 00 00 54 54` - Code 0x2a, all zeros, saturation 0x54 -- Second packet: `05 38 00 00 00 00 00 00 00 54 54` - Code 0x38, all zeros, saturation 0x54 +**Parameter Scaling (Linux FFB → Device):** +- **Coefficients:** 0-10000 → Device 0-10 (divide by 1000) +- **Center/Offset:** -10000 to +10000 → Device s16 LE (divide by 20) +- **Deadband:** 0-10000 → Device u16 LE (divide by 10) +- **Saturation:** 0-100 percentage -**Example (INCORRECT - will cause device rejection):** -- `05 2a 7f 00 7f 00 00 00 7f 64 64` - Non-zero coefficients cause Y-axis packet to fail - -**Parameter Scaling:** (Based on limited data, needs verification) -- Coefficients: SDL2 value (0-32767) → device value (scaling TBD) -- Deadband: SDL2 value (0-65535) → device value (scaling TBD) -- Center: SDL2 value (-32767 to +32767) → device value (0-255?) -- Saturation: SDL2 value (0-32767) → device value (0-255, observed: 0x54, 0x64) +**Examples from Captures:** +- `05 0e 00 0a 0a 00 00 00 00 64 64` - Coeffs=10,10, center=0, deadband=0, sat=100 +- `05 0e 00 06 04 fa 00 00 00 64 64` - Coeffs=6,4, center=250 (5000/20), deadband=0 +- `05 0e 00 0a 0a 8c fe c2 01 64 64` - Coeffs=10,10, center=-372 (-7439/20), deadband=450 ### 0x41 - Command Packet (4 bytes) ``` @@ -335,12 +335,36 @@ Offset | Size | Field | Description ### 10. CONDITIONAL EFFECTS - INERTIA -**Status:** Limited capture data available. Assumed to use same 0x05 packet structure as spring/damper. +**Capture Example:** +- Input: Axis0 coefficient=10000, offset=-7439; Axis1 coefficient=10000, offset=0 +- Main packet: `01 04 41 40 7a 09 00 ff ff 42 01 50 01 00 00` + - effect_type = 0x41 (same as damper) + - duration = 0x097a (2426ms) + - codes: 0x0142, 0x0150 + +- First 0x05 packet (X-axis): `05 42 01 0a 0a 8c fe 00 00 64 64` + - code = 0x42 + - right_coeff = 0x0a01 (2561) - **NON-ZERO!** + - left_coeff = 0x8c0a (35850 or -29686 signed) - **NON-ZERO!** + - deadband = 0x00fe (254) + - center = 0x00 + - saturation = 0x64 (100) + +- Second 0x05 packet (Y-axis): `05 50 01 00 00 00 00 00 00 64 64` + - code = 0x50 + - right_coeff = 0x0001 (1) + - left_coeff = 0x0000 (0) + - deadband = 0x0000 (0) + - center = 0x00 + - saturation = 0x64 (100) + +**Implication:** The current driver sends zero coefficients for all conditional effects (matching Windows driver behavior for spring/damper). However, inertia may require non-zero coefficients for proper behavior. This needs further testing. **Expected Structure:** - Two 0x05 packets with codes from 0x01 bytes 9-12 - Same parameter layout: right_coeff, left_coeff, deadband, center, saturation -- Saturation value may differ from spring/damper +- Saturation value: 0x64 (100) - same as damper +- Coefficients: May need to be non-zero for proper inertia feel ### 11. CONDITIONAL EFFECTS - FRICTION @@ -349,7 +373,8 @@ Offset | Size | Field | Description **Expected Structure:** - Two 0x05 packets with codes from 0x01 bytes 9-12 - Same parameter layout: right_coeff, left_coeff, deadband, center, saturation -- Saturation value may differ from spring/damper +- Saturation value: 0x64 (100) - same as damper +- Coefficients: May need to be non-zero (similar to inertia) - needs verification ### 12. MULTI-EFFECT SCENARIOS @@ -470,3 +495,80 @@ Offset | Size | Field | Description - SDL 16000 → Device 12 - SDL 24000 → Device 18 - SDL 32767 → Device 255 + +--- + +## FFEdit Capture Analysis (December 2025) + +New captures from FFEdit (Force Feedback Editor) tool provide comprehensive protocol understanding. + +### Capture Files Analyzed + +**Initial Captures:** +- `inertia_Axis0_10000_offset_-7439_axis1_10000_offset_0.pcapng` +- `square_medium.pcapng`, `square_max.pcapng` +- `Ramp.pcapng` + +**Systematic Conditional Effect Captures (Dec 11, 2025):** +- **Damper:** 8 captures with varying coefficients, offsets, deadbands +- **Friction:** 5 captures with varying positive/negative coefficients +- **Inertia:** 8 captures with varying coefficients and offsets + +### Key Findings + +#### 1. Square Wave Effect Type (0x20) ✅ IMPLEMENTED +FFEdit confirms square wave uses effect type **0x20**: +- Example: `01 04 20 40 1d 10 00 ff ff 42 01 50 01 00 00` +- Uses same 0x04 packet structure as other periodic effects + +#### 2. Corrected 0x05 Packet Structure +Previous understanding was incorrect. FFEdit analysis reveals: +- **Coefficients are u8 (not u16!)** - scaled 0-10 for 0-10000 FFEdit range +- **Center/offset is s16 LE** at bytes 5-6 (not single byte at position 8) +- **Deadband is u16 LE** at bytes 7-8 + +**Verified Examples:** +| FFEdit Parameters | 0x05 Packet | Decoded | +|-------------------|-------------|---------| +| friction P=10000,N=10000 | `050e000a0a000000006464` | coeff=10,10 | +| friction P=7500,N=5000 | `050e000805000000006464` | coeff=8,5 (rounded) | +| damper PC=6000,NC=4000,O=5000 | `0546000604fa00000064 64` | coeff=6,4, center=250 | +| damper O=-7439,D=4500 | `05460a0a8cfec2016464` | center=-372, deadband=450 | +| inertia P=6000,N=4000,O=-4000 | `052a00060438ff00006464` | coeff=6,4, center=-200 | + +#### 3. Coefficient Scaling Formula +``` +device_coeff = ffb_coeff / 1000 (for 0-10000 range) → 0-10 u8 +device_coeff = ffb_coeff / 3277 (for 0-32767 Linux range) → 0-10 u8 +``` + +#### 4. Center/Offset Scaling Formula +``` +device_center = ffb_center / 20 (for ±10000 FFEdit range) → s16 LE +device_center = ffb_center / 65 (for ±32767 Linux range) → approx ±500 +``` + +#### 5. Deadband Scaling Formula +``` +device_deadband = ffb_deadband / 10 (for 0-10000 range) → u16 LE +device_deadband = ffb_deadband / 65 (for 0-65535 Linux range) → 0-1008 +``` + +#### 6. Windows vs FFEdit Behavior +- **Windows driver:** Sends zeros for coefficients/center/deadband, relies on saturation only +- **FFEdit:** Sends actual coefficient values; device accepts both approaches +- **Conclusion:** Coefficients are optional for basic functionality but enable finer control + +#### 7. Ramp Effect Phase Field (✅ IMPLEMENTED) +Ramp captures confirm phase encodes ramp direction: +- `049a0000007f0000` - phase 0x7f (127) = positive/up ramp (start < end) +- `049a000c00000000` - phase 0x00 = negative/down ramp (start > end) + +**Implementation:** +```c +/* Phase encodes ramp direction: + * - Positive ramp (start < end): phase = 0x7f + * - Negative ramp (start > end): phase = 0x00 + */ +phase = (start_level < end_level) ? 0x7f : 0x00; +``` diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index b4b251fb..20edf6dc 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -329,7 +329,7 @@ static inline s8 t500rs_scale_periodic_offset(s16 os_ffb_offset) { * 0x04 packet structure as periodic effects. The encoding is: * - magnitude: scaled from start/end levels (midpoint or average) * - offset: difference between start and end (direction of ramp) - * - phase: typically 0 for ramp + * - phase: encodes ramp direction (0x7f = up, 0x00 = down) * - period_ms: ramp duration in milliseconds * * Note: exact mapping of start/end to magnitude/offset is uncertain; @@ -342,6 +342,7 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, int avg_level; u8 magnitude; s8 offset; + u8 phase; memset(p, 0, sizeof(*p)); @@ -353,50 +354,62 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, /* Simple approximation: (end - start) / 512 to fit in s8 range */ offset = (s8)((end_level - start_level) / 512); - /* Byte order per Windows USB captures: b0=id, b1=code, b2=reserved1, b3=mag, + /* + * Phase encodes ramp direction per FFEdit captures: + * - Positive ramp (start < end): phase = 0x7f (127) + * - Negative ramp (start > end): phase = 0x00 + * - Equal levels: treat as positive (neutral case) + * + * Example captures: + * - 049a0000007f0000 - phase 0x7f = positive/up direction + * - 049a000c00000000 - phase 0x00 = negative/down direction + */ + phase = (start_level < end_level) ? 0x7f : 0x00; + + /* Byte order per USB captures: b0=id, b1=code, b2=reserved1, b3=mag, * b4=offset, b5=phase, b6-b7=period */ p->id = 0x04; /* b0 */ p->code = code; /* b1 */ p->reserved1 = 0; /* b2: always 0x00 */ p->magnitude = magnitude; /* b3 */ p->offset = (u8)offset; /* b4 */ - p->phase = 0; /* b5: Ramp doesn't use phase */ + p->phase = phase; /* b5: direction (0x7f=up, 0x00=down) */ p->period_ms = cpu_to_le16(duration_ms); /* b6-b7 */ } /* * Build a 0x05 conditional effect packet. * - * Per Windows captures (T500RS_USB_Protocol_Analysis.md): - * - Two 0x05 packets required per conditional effect (X-axis and Y-axis) - * - Coefficients are currently sent as zero (needs more capture verification) - * - Deadband and center are now experimental - some captures show non-zero values: - * - Spring Deadband test: deadband=500 -> 0x0007, deadband=5000 -> 0x0099 - * - This suggests scaling: device_deadband = (os_ffb_deadband * 255) / 65535 + * Per captures (T500RS_USB_Protocol_Analysis.md): + * - packet structure with u8 coefficients and proper field layout + * - Coefficients are sent as 0-10 scale (not zero) + * - Center and deadband are scaled from Linux FFB ranges * * Parameters: * - code: From 0x01 packet bytes 9-10 (first packet) or 11-12 (second packet) - * - saturation: Scaled saturation value (0-100) for both right/left channels - * - deadband: Raw deadband from ff_condition_effect (0-65535) - * - center: Raw center from ff_condition_effect (-32767 to +32767) + * - right_coeff: Right/positive coefficient from ff_condition_effect (0-32767) + * - left_coeff: Left/negative coefficient from ff_condition_effect (0-32767) + * - saturation: Saturation value (0-100) for both right/left channels + * - deadband: Deadband from ff_condition_effect (0-65535) + * - center: Center offset from ff_condition_effect (-32767 to +32767) */ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, - u8 code, u8 saturation, - u16 deadband, s16 center) { + u8 code, s16 right_coeff, s16 left_coeff, + u8 saturation, u16 deadband, s16 center) { memset(p, 0, sizeof(*p)); p->id = T500RS_PKT_CONDITIONAL; p->code = code; + p->reserved = 0x00; - /* Coefficients: keep zero for now (needs capture verification) */ - p->right_coeff = 0; - p->left_coeff = 0; + /* Scale coefficients from Linux 0-32767 range to device 0-10 u8 scale */ + p->right_coeff = (u8)((right_coeff * 10) / 32767); + p->left_coeff = (u8)((left_coeff * 10) / 32767); - /* - * Experimental deadband/center support - * Center: scale from -32767..+32767 to 0..255 (128 = center) - */ - p->deadband = cpu_to_le16((deadband * 255) / 65535); - p->center = (u8)(((center + 32767) * 255) / 65535); + /* Scale center from Linux ±32767 range to device s16 LE (approx ±500) */ + p->center = cpu_to_le16((s16)(center / 65)); + + /* Scale deadband from Linux 0-65535 range to device u16 LE (0-1008) */ + p->deadband = cpu_to_le16((u16)(deadband / 65)); p->right_sat = saturation; p->left_sat = saturation; @@ -633,8 +646,9 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, } struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub), saturation, - cond->deadband, cond->center); + t500rs_build_r05_condition(p, (u8)(param_sub), cond->right_coeff, + cond->left_coeff, saturation, cond->deadband, + cond->center); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); } break; @@ -661,8 +675,9 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, } struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), saturation, - cond->deadband, cond->center); + t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), cond->right_coeff, + cond->left_coeff, saturation, cond->deadband, + cond->center); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); } break; @@ -1328,8 +1343,9 @@ static int t500rs_update_effect(void *data, break; } struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), saturation, - cond->deadband, cond->center); + t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), cond->right_coeff, + cond->left_coeff, saturation, cond->deadband, + cond->center); return t500rs_send_hid(t500rs, buf, sizeof(*p)); } default: diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index 9cd59c90..c720fdb4 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -141,7 +141,7 @@ struct t500rs_pkt_r01_main { * Used for both periodic effects (sine, triangle, sawtooth) and ramp effects. * Code field must match the subtype specified in 0x01 packet bytes 9-10. * - * Packet format (verified against Windows captures): + * Packet format: * - b0: packet type (0x04) * - b1: subtype code (from 0x01 packet_code_1, typically 0x2a) * - b2: reserved (0x00) @@ -163,33 +163,35 @@ struct t500rs_pkt_r04_periodic_ramp { } __packed; /* - * 0x05 - Conditional parameters (11 bytes) - * - * CRITICAL: Windows captures show that conditional effects require TWO 0x05 packets, - * but coefficients/deadband/center MUST be zero. Only saturation values control behavior. + * 0x05 - Conditional Effect Packet (11 bytes) * - * Packet format (verified against Windows captures): + * Packet format: * - b0: packet type (0x05) - * - b1: subtype code (from 0x01 packet_code_1 or packet_code_2) - * - b2-b3: right coefficient (LE) - MUST BE ZERO - * - b4-b5: left coefficient (LE) - MUST BE ZERO - * - b6-b7: deadband (LE) - MUST BE ZERO - * - b8: center - MUST BE ZERO - * - b9: right saturation (0-100, controls spring/damper strength) - * - b10: left saturation (0-100, controls spring/damper strength) + * - b1: code (from 0x01 packet_code_1 or packet_code_2) + * - b2: reserved (always 0x00) + * - b3: right coefficient (u8, 0-10 scale) + * - b4: left coefficient (u8, 0-10 scale) + * - b5-b6: center/offset (s16 LE, scaled from Linux ±32767 range) + * - b7-b8: deadband (u16 LE, scaled from Linux 0-65535 range) + * - b9: right saturation (0-100, controls effect strength) + * - b10: left saturation (0-100, controls effect strength) * - * Firmware rejects packets with non-zero coefficients, causing EPROTO errors. - * Effect behavior is controlled solely through saturation values. + * Scaling (from Linux FFB to device): + * - Coefficients: (value * 10) / 32767 → 0-10 u8 + * - Center: value / 65 → s16 LE (approx ±500 range) + * - Deadband: value / 65 → u16 LE (0-1008 range) + * - Saturation: 0-100 (no scaling) */ struct t500rs_pkt_r05_condition { - u8 id; /* T500RS_PKT_CONDITIONAL */ - u8 code; /* from 0x01 code1/code2 (T500RS_CODE_*) */ - __le16 right_coeff; /* Currently zero - needs capture verification */ - __le16 left_coeff; /* Currently zero - needs capture verification */ - __le16 deadband; /* Experimental: scaled from ff_condition_effect.deadband */ - u8 center; /* Experimental: scaled from ff_condition_effect.center */ - u8 right_sat; /* 0-100: controls effect strength */ - u8 left_sat; /* 0-100: controls effect strength */ + u8 id; /* T500RS_PKT_CONDITIONAL */ + u8 code; /* from 0x01 code1/code2 */ + u8 reserved; /* Always 0x00 */ + u8 right_coeff; /* Right/positive coefficient (0-10 scale) */ + u8 left_coeff; /* Left/negative coefficient (0-10 scale) */ + __le16 center; /* Center offset (s16 LE, scaled by /65) */ + __le16 deadband; /* Deadband width (u16 LE, scaled by /65) */ + u8 right_sat; /* Right saturation (0-100) */ + u8 left_sat; /* Left saturation (0-100) */ } __packed; /* 0x03 - Constant force level (4 bytes) */ From bb9f6042217920dba86a375bd2bd19c663c86eca Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Wed, 17 Dec 2025 23:14:45 +0100 Subject: [PATCH 05/15] Added support for boot mode switch to normal mode --- src/hid-tmff2.c | 9 ++- src/hid-tmff2.h | 1 + src/tmt500rs/hid-tmt500rs.c | 112 ++++++++++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index bc494a63..48b41d44 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -756,6 +756,7 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) break; case TMT500RS_PC_ID: + case TMT500RS_PC_BOOT_ID: if ((ret = t500rs_populate_api(tmff2))) goto wheel_err; break; @@ -795,8 +796,11 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) tmff2->input_dev = list_entry(hdev->inputs.next, struct hid_input, list)->input; if ((ret = tmff2_wheel_init(tmff2))) { - hid_err(hdev, "init failed\n"); - goto init_err; + /* For all errors but -EAGAIN (boot mode switch for T500RS for instance), we throw init error */ + if (ret != -EAGAIN) { + hid_err(hdev, "init failed\n"); + goto init_err; + } } return 0; @@ -873,6 +877,7 @@ static const struct hid_device_id tmff2_devices[] = { {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_BOOT_ID)}, {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT500RS_PC_ID)}, /* t248 PC*/ {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT248_PC_ID)}, diff --git a/src/hid-tmff2.h b/src/hid-tmff2.h index c605a32d..cf8cbff3 100644 --- a/src/hid-tmff2.h +++ b/src/hid-tmff2.h @@ -118,6 +118,7 @@ int tspc_populate_api(struct tmff2_device_entry *tmff2); #define TMT300RS_PS3_ADV_ID 0xb66f #define TMT300RS_PS4_NORM_ID 0xb66d +#define TMT500RS_PC_BOOT_ID 0xb65d #define TMT500RS_PC_ID 0xb65e #define TMT248_PC_ID 0xb696 diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index 20edf6dc..ef1c0a22 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -25,6 +25,91 @@ #include "hid-tmt500rs.h" #include #include +#include +#include + +static int t500rs_request_normal_mode(struct hid_device *hdev) +{ + struct usb_device *udev; + u8 *buf; + int ret, i; + + /* HID over USB: hdev->dev.parent is usb_interface->dev */ + if (!hdev->dev.parent || !hdev->dev.parent->parent) + return -ENODEV; + + udev = to_usb_device(hdev->dev.parent->parent); + if (!udev) + return -ENODEV; + + buf = kmalloc(16, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + hid_info(hdev, "T500RS detected in boot mode (0x%04x); requesting switch to normal mode...\n", + hdev->product); + + /* + * Observed Windows boot->normal sequence (USBPcap): + * - Vendor IN requests (bmRequestType=0xc1, recipient=interface) + * 0x49 wLength=16 (polled) + * 0x56/0x42/0x4e wLength=8 (one-time probes) + * - Vendor OUT request (bmRequestType=0x41): + * bRequest=0x53, wValue=0x0002, wLength=0 + * which causes the device to reset and re-enumerate as 0xb65e. + * + * The device may STALL the 0x53 request while performing the reset. + */ + ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x49, 0xc1, 0, 0, + buf, sizeof(buf), USB_CTRL_GET_TIMEOUT); + if (ret < 0) + hid_dbg(hdev, "boot-mode probe 0x49 failed: %d\n", ret); + + ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x56, 0xc1, 0, 0, + buf, 8, USB_CTRL_GET_TIMEOUT); + if (ret < 0) + hid_dbg(hdev, "boot-mode probe 0x56 failed: %d\n", ret); + + ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x42, 0xc1, 0, 0, + buf, 8, USB_CTRL_GET_TIMEOUT); + if (ret < 0) + hid_dbg(hdev, "boot-mode probe 0x42 failed: %d\n", ret); + + ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x4e, 0xc1, 0, 0, + buf, 8, USB_CTRL_GET_TIMEOUT); + if (ret < 0) + hid_dbg(hdev, "boot-mode probe 0x4e failed: %d\n", ret); + + ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x56, 0xc1, 0, 0, + buf, 8, USB_CTRL_GET_TIMEOUT); + if (ret < 0) + hid_dbg(hdev, "boot-mode probe 0x56 (2) failed: %d\n", ret); + + /* Poll 0x49 briefly (Windows issues multiple times at ~15ms cadence) */ + for (i = 0; i < 16; i++) { + ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x49, 0xc1, 0, 0, + buf, sizeof(buf), USB_CTRL_GET_TIMEOUT); + if (ret >= 0) + break; + msleep(15); + } + + ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0), 0x53, 0x41, 0x0002, 0, + NULL, 0, USB_CTRL_SET_TIMEOUT); + if (ret < 0) { + /* A STALL here is expected on some firmware revisions during reset. */ + if (ret == -EPIPE || ret == -EPROTO || ret == -EIO) + ret = 0; + else + hid_warn(hdev, "boot->normal request failed: %d\n", ret); + } + + /* Give the device a moment to tear down and re-enumerate */ + msleep(50); + + kfree(buf); + return ret; +} /* Packet sequence templates for each effect type */ static const enum t500rs_seq_packet t500rs_seq_constant[] = { @@ -1478,7 +1563,22 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } hid_dbg(tmff2->hdev, "T500RS: Initializing HID mode\n"); - + + if (tmff2->hdev->product == TMT500RS_PC_BOOT_ID) { + int ret = t500rs_request_normal_mode(tmff2->hdev); + if (ret == 0) { + hid_info(tmff2->hdev, "Boot mode switch initiated, device should re-enumerate as normal mode\n"); + /* Special code for boot mode switch. We return "resource temporarily unavailable" + * c.f. https://en.wikipedia.org/wiki/Errno.h + */ + return -EAGAIN; + } else { + hid_err(tmff2->hdev, "Boot mode switch failed: %d, stopping\n", ret); + /* return " Protocol not supported " */ + return -EPROTONOSUPPORT; + } + } + /* Allocate device data */ t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); if (!t500rs) { @@ -1596,12 +1696,12 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { /* Cleanup T500RS device */ static int t500rs_wheel_destroy(void *data) { - struct t500rs_device_entry *t500rs = data; + struct t500rs_device_entry *t500rs = data; - if (!t500rs) { - pr_warn("t500rs_wheel_destroy: NULL data pointer\n"); - return 0; - } + if (!t500rs) { + /* Expected for boot mode devices that return -ENODEV before allocation */ + return 0; + } T500RS_DBG(t500rs, "T500RS: Cleaning up\n"); From 163c4466cc9cccc48ad3c3b07f4ac2c2b125a0fa Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Wed, 17 Dec 2025 23:50:07 +0100 Subject: [PATCH 06/15] Minor adjustments (code tidy-up, documentation) --- src/tmt500rs/hid-tmt500rs.c | 151 ++++++++++++++++-------------------- src/tmt500rs/hid-tmt500rs.h | 22 +++--- 2 files changed, 78 insertions(+), 95 deletions(-) diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index ef1c0a22..8ceb92cf 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -1,24 +1,10 @@ - -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later /* - * Force feedback support for Thrustmaster T500RS - * - * HID implementation using HID output reports for all communication. - * - * Protocol documentation: docs/T500RS_USB_Protocol_Analysis.md - * - * Key protocol details (verified against Windows USB captures): - * - 0x01 packet: Main upload (15 bytes) - effect_id, direction, duration, - * delay, code1/2 - * - 0x02 packet: Envelope (9 bytes) - attack/fade levels and times - * - 0x03 packet: Constant force level (4 bytes) - * - 0x04 packet: Periodic/Ramp parameters (8 bytes) - code 0x2a, period in ms - * - 0x05 packet: Conditional parameters (11 bytes) - two packets per effect - * (X/Y) - * - 0x41 packet: START/STOP command (4 bytes) - per-effect hw_id + * HID driver for Thrustmaster T500RS wheel base that provides Force feedback * - * Hardware supports 16 concurrent effects with internal mixing. - * Protocol analysis based on 70+ test captures from Windows driver. + * Protocol documentation: docs/T500RS_USB_Protocol_Analysis.md + * + * Copyright (c) 2025 Casimir Bonnet */ #include "../hid-tmff2.h" @@ -27,6 +13,7 @@ #include #include #include +#include static int t500rs_request_normal_mode(struct hid_device *hdev) { @@ -50,7 +37,7 @@ static int t500rs_request_normal_mode(struct hid_device *hdev) hdev->product); /* - * Observed Windows boot->normal sequence (USBPcap): + * Observed Windows boot->normal sequence: * - Vendor IN requests (bmRequestType=0xc1, recipient=interface) * 0x49 wLength=16 (polled) * 0x56/0x42/0x4e wLength=8 (one-time probes) @@ -224,11 +211,11 @@ struct t500rs_device_entry { * Scale direction from Linux ff_effect format to T500RS protocol format. * * Linux ff_effect.direction: 0-65535 (0 = forward, 16384 = right, 32768 = back, - * 49152 = left) T500RS protocol: 0-35999 in 0.01 degree units (0 = 0°, 9000 = - * 90°, 18000 = 180°, etc.) + * 49152 = left) T500RS protocol: 0-35999 in 0.01 degree units (0 = 0 degrees, 9000 = + * 90 degrees, 18000 = 180 degrees, etc.) * * Conversion: device_dir = (os_ffb_dir * 36000) / 65536 - * This maps 0-65535 → 0-35999 (approximately, since 65535 → 35999.45) + * This maps 0-65535 -> 0-35999 (approximately, since 65535 -> 35999.45) */ static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { /* Use 32-bit arithmetic to avoid overflow */ @@ -312,7 +299,7 @@ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, * 0x0e!) * - magnitude: 0..127 (scaled from 0..32767) * - offset: signed DC offset (scaled from -32768..32767 to device range) - * - phase: 0..255 (256 steps for 360°, scaled from 0..35999) + * - phase: 0..255 (256 steps for 360 degrees, scaled from 0..35999) * - period_ms: period in MILLISECONDS (no Hz*100 conversion!) * - reserved: always 0 * @@ -347,11 +334,11 @@ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, * * When the projected magnitude is negative, we: * 1. Take the absolute value (wheel only supports positive magnitudes) - * 2. Add 180° to the phase to maintain correct force direction + * 2. Add 180 degrees to the phase to maintain correct force direction * * Linux FFB magnitude: 0..32767 (unsigned) * Linux FFB direction: 0..65535 (0=forward, 16384=right, 32768=back, 49152=left) - * Linux FFB phase: 0..65535 (0..360° in 1/65536 units) + * Linux FFB phase: 0..65535 (0..360 degrees in 1/65536 units) * Device magnitude: 0..127 * * @param os_ffb_mag: Original magnitude from Linux FFB (0..32767) @@ -371,8 +358,8 @@ static inline u8 t500rs_scale_periodic_with_direction(int os_ffb_mag, /* Wheel handles positive magnitudes only */ projected = -projected; - /* Add 180° to phase to maintain correct force direction. - * Phase is in 0..65535 range (Linux FFB), 180° = 0x8000 */ + /* Add 180 degrees to phase to maintain correct force direction. + * Phase is in 0..65535 range (Linux FFB), 180 degrees = 0x8000 */ if (phase_ptr) *phase_ptr = (*phase_ptr + 0x8000) % 0x10000; } @@ -387,8 +374,8 @@ static inline u8 t500rs_scale_periodic_with_direction(int os_ffb_mag, /* * Scale periodic phase from Linux FFB subsystem format to device format. - * Linux FFB: 0..35999 (0.01 degree units, 0-359.99°) - * Device: 0..255 (256 steps for 360°) + * Linux FFB: 0..35999 (0.01 degree units, 0-359.99 degrees) + * Device: 0..255 (256 steps for 360 degrees) */ static inline u8 t500rs_scale_periodic_phase(u16 os_ffb_phase) { /* Clamp to valid range just in case */ @@ -509,9 +496,9 @@ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, * - Formula: device_level = (os_ffb_level * 255 / 65535) - 127 * * This maps: - * Linux FFB 0 → Device -127 (max negative) - * Linux FFB 32767 → Device 0 (neutral) - * Linux FFB 65535 → Device +127 (max positive) + * Linux FFB 0 -> Device -127 (max negative) + * Linux FFB 32767 -> Device 0 (neutral) + * Linux FFB 65535 -> Device +127 (max positive) */ static inline s8 t500rs_scale_constant_level(u16 os_ffb_level) { s32 tmp = ((s32)os_ffb_level * 255) / 65535; @@ -669,46 +656,46 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, t500rs_build_r02_envelope(env, (u8)(env_sub & 0xff), envelope, allow_envelope); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r02_envelope)); - } break; + } case T500RS_SEQ_CONSTANT: { s8 level = t500rs_scale_const_with_direction(effect->u.constant.level, effect->direction); struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), level); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); - } break; + } case T500RS_SEQ_PERIODIC_RAMP: { - if (effect->type == FF_RAMP) { - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), - effect->u.ramp.start_level, - effect->u.ramp.end_level, effect->replay.length); - } else { - /* Apply direction projection to magnitude and adjust phase if needed */ - u16 phase_raw = effect->u.periodic.phase; - u8 mag = t500rs_scale_periodic_with_direction( - effect->u.periodic.magnitude, effect->direction, &phase_raw); - u8 phase = t500rs_scale_periodic_phase(phase_raw); - s8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); - u16 period_ms = effect->u.periodic.period; - if (period_ms == 0) { - hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); - return -EINVAL; - } - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, - period_ms); + if (effect->type == FF_RAMP) { + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), + effect->u.ramp.start_level, + effect->u.ramp.end_level, effect->replay.length); + } else { + /* Apply direction projection to magnitude and adjust phase if needed */ + u16 phase_raw = effect->u.periodic.phase; + u8 mag = t500rs_scale_periodic_with_direction( + effect->u.periodic.magnitude, effect->direction, &phase_raw); + u8 phase = t500rs_scale_periodic_phase(phase_raw); + s8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); + u16 period_ms = effect->u.periodic.period; + if (period_ms == 0) { + hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); + return -EINVAL; } - ret = t500rs_send_hid(t500rs, buf, - sizeof(struct t500rs_pkt_r04_periodic_ramp)); - if (ret) - break; + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, + period_ms); } + ret = t500rs_send_hid(t500rs, buf, + sizeof(struct t500rs_pkt_r04_periodic_ramp)); + if (ret) break; + break; + } case T500RS_SEQ_CONDITION_X: { u8 saturation = 0; const struct ff_condition_effect *cond = &effect->u.condition[0]; @@ -735,8 +722,8 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, cond->left_coeff, saturation, cond->deadband, cond->center); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); - } break; + } case T500RS_SEQ_CONDITION_Y: { u8 saturation = 0; /* Y-axis: use condition[1] if available, else zeros */ @@ -764,8 +751,8 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, cond->left_coeff, saturation, cond->deadband, cond->center); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); - } break; + } case T500RS_SEQ_MAIN: { u8 effect_type = 0; switch (effect->type) { @@ -786,25 +773,25 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, break; case FF_PERIODIC: switch (effect->u.periodic.waveform) { - case FF_SQUARE: - effect_type = T500RS_EFFECT_SQUARE; - break; - case FF_SINE: - effect_type = T500RS_EFFECT_SINE; - break; - case FF_TRIANGLE: - effect_type = T500RS_EFFECT_TRIANGLE; - break; - case FF_SAW_UP: - effect_type = T500RS_EFFECT_SAW_UP; - break; - case FF_SAW_DOWN: - effect_type = T500RS_EFFECT_SAW_DOWN; + case FF_SQUARE: + effect_type = T500RS_EFFECT_SQUARE; + break; + case FF_SINE: + effect_type = T500RS_EFFECT_SINE; + break; + case FF_TRIANGLE: + effect_type = T500RS_EFFECT_TRIANGLE; + break; + case FF_SAW_UP: + effect_type = T500RS_EFFECT_SAW_UP; + break; + case FF_SAW_DOWN: + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + return -EINVAL; + } break; - default: - return -EINVAL; - } - break; case FF_RAMP: effect_type = T500RS_EFFECT_SAW_DOWN; break; @@ -819,8 +806,8 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, if (ret) break; ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r01_main)); - } break; + } default: ret = -EINVAL; } diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index c720fdb4..f6d0314d 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -1,9 +1,13 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ +// SPDX-License-Identifier: GPL-2.0-or-later /* - * T500RS Force Feedback Protocol Constants and Structures + * T500RS Force Feedback Protocol Constants and Structures for + * Thrustmaster T500RS wheel base. * - * This header defines all protocol-specific constants and packet structures - * for the Thrustmaster T500RS racing wheel force feedback implementation. + * Protocol documentation: docs/T500RS_USB_Protocol_Analysis.md + * This header defines all protocol-specific constants and packet structures + * for the Thrustmaster T500RS racing wheel force feedback implementation. + * + * Copyright (c) 2025 Casimir Bonnet */ #ifndef __T500RS_H @@ -83,14 +87,6 @@ enum t500rs_seq_packet { T500RS_SEQ_MAIN, }; -/* - * Packet Sequence Templates - * - * These arrays define the packet sequences for different effect types. - * Used by the packet sequencing abstraction system. - */ -/* Sequence templates are now static in the implementation file */ - /* Supported effects */ extern const signed short t500rs_effects[]; @@ -221,4 +217,4 @@ struct t500rs_pkt_r02_envelope { u8 reserved; /* 0x00 */ } __packed; -#endif /* __T500RS_PROTOCOL_H */ +#endif /* __HID_TMT500RS_H */ From 2aa9bf9c0b20a9ab2edf7b7521e47d9c52804cac Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Thu, 18 Dec 2025 13:40:15 +0100 Subject: [PATCH 07/15] Reverting init mode switch from driver, it should be handled by hid-tminit --- src/hid-tmff2.c | 9 +-- src/hid-tmff2.h | 1 - src/tmt500rs/hid-tmt500rs.c | 113 ++---------------------------------- 3 files changed, 8 insertions(+), 115 deletions(-) diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index 48b41d44..bc494a63 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -756,7 +756,6 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) break; case TMT500RS_PC_ID: - case TMT500RS_PC_BOOT_ID: if ((ret = t500rs_populate_api(tmff2))) goto wheel_err; break; @@ -796,11 +795,8 @@ static int tmff2_probe(struct hid_device *hdev, const struct hid_device_id *id) tmff2->input_dev = list_entry(hdev->inputs.next, struct hid_input, list)->input; if ((ret = tmff2_wheel_init(tmff2))) { - /* For all errors but -EAGAIN (boot mode switch for T500RS for instance), we throw init error */ - if (ret != -EAGAIN) { - hid_err(hdev, "init failed\n"); - goto init_err; - } + hid_err(hdev, "init failed\n"); + goto init_err; } return 0; @@ -877,7 +873,6 @@ static const struct hid_device_id tmff2_devices[] = { {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_BOOT_ID)}, {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT500RS_PC_ID)}, /* t248 PC*/ {HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, TMT248_PC_ID)}, diff --git a/src/hid-tmff2.h b/src/hid-tmff2.h index cf8cbff3..c605a32d 100644 --- a/src/hid-tmff2.h +++ b/src/hid-tmff2.h @@ -118,7 +118,6 @@ int tspc_populate_api(struct tmff2_device_entry *tmff2); #define TMT300RS_PS3_ADV_ID 0xb66f #define TMT300RS_PS4_NORM_ID 0xb66d -#define TMT500RS_PC_BOOT_ID 0xb65d #define TMT500RS_PC_ID 0xb65e #define TMT248_PC_ID 0xb696 diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index 8ceb92cf..822119fa 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -11,92 +11,6 @@ #include "hid-tmt500rs.h" #include #include -#include -#include -#include - -static int t500rs_request_normal_mode(struct hid_device *hdev) -{ - struct usb_device *udev; - u8 *buf; - int ret, i; - - /* HID over USB: hdev->dev.parent is usb_interface->dev */ - if (!hdev->dev.parent || !hdev->dev.parent->parent) - return -ENODEV; - - udev = to_usb_device(hdev->dev.parent->parent); - if (!udev) - return -ENODEV; - - buf = kmalloc(16, GFP_KERNEL); - if (!buf) - return -ENOMEM; - - hid_info(hdev, "T500RS detected in boot mode (0x%04x); requesting switch to normal mode...\n", - hdev->product); - - /* - * Observed Windows boot->normal sequence: - * - Vendor IN requests (bmRequestType=0xc1, recipient=interface) - * 0x49 wLength=16 (polled) - * 0x56/0x42/0x4e wLength=8 (one-time probes) - * - Vendor OUT request (bmRequestType=0x41): - * bRequest=0x53, wValue=0x0002, wLength=0 - * which causes the device to reset and re-enumerate as 0xb65e. - * - * The device may STALL the 0x53 request while performing the reset. - */ - ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x49, 0xc1, 0, 0, - buf, sizeof(buf), USB_CTRL_GET_TIMEOUT); - if (ret < 0) - hid_dbg(hdev, "boot-mode probe 0x49 failed: %d\n", ret); - - ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x56, 0xc1, 0, 0, - buf, 8, USB_CTRL_GET_TIMEOUT); - if (ret < 0) - hid_dbg(hdev, "boot-mode probe 0x56 failed: %d\n", ret); - - ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x42, 0xc1, 0, 0, - buf, 8, USB_CTRL_GET_TIMEOUT); - if (ret < 0) - hid_dbg(hdev, "boot-mode probe 0x42 failed: %d\n", ret); - - ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x4e, 0xc1, 0, 0, - buf, 8, USB_CTRL_GET_TIMEOUT); - if (ret < 0) - hid_dbg(hdev, "boot-mode probe 0x4e failed: %d\n", ret); - - ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x56, 0xc1, 0, 0, - buf, 8, USB_CTRL_GET_TIMEOUT); - if (ret < 0) - hid_dbg(hdev, "boot-mode probe 0x56 (2) failed: %d\n", ret); - - /* Poll 0x49 briefly (Windows issues multiple times at ~15ms cadence) */ - for (i = 0; i < 16; i++) { - ret = usb_control_msg(udev, usb_rcvctrlpipe(udev, 0), 0x49, 0xc1, 0, 0, - buf, sizeof(buf), USB_CTRL_GET_TIMEOUT); - if (ret >= 0) - break; - msleep(15); - } - - ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0), 0x53, 0x41, 0x0002, 0, - NULL, 0, USB_CTRL_SET_TIMEOUT); - if (ret < 0) { - /* A STALL here is expected on some firmware revisions during reset. */ - if (ret == -EPIPE || ret == -EPROTO || ret == -EIO) - ret = 0; - else - hid_warn(hdev, "boot->normal request failed: %d\n", ret); - } - - /* Give the device a moment to tear down and re-enumerate */ - msleep(50); - - kfree(buf); - return ret; -} /* Packet sequence templates for each effect type */ static const enum t500rs_seq_packet t500rs_seq_constant[] = { @@ -1550,22 +1464,7 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { } hid_dbg(tmff2->hdev, "T500RS: Initializing HID mode\n"); - - if (tmff2->hdev->product == TMT500RS_PC_BOOT_ID) { - int ret = t500rs_request_normal_mode(tmff2->hdev); - if (ret == 0) { - hid_info(tmff2->hdev, "Boot mode switch initiated, device should re-enumerate as normal mode\n"); - /* Special code for boot mode switch. We return "resource temporarily unavailable" - * c.f. https://en.wikipedia.org/wiki/Errno.h - */ - return -EAGAIN; - } else { - hid_err(tmff2->hdev, "Boot mode switch failed: %d, stopping\n", ret); - /* return " Protocol not supported " */ - return -EPROTONOSUPPORT; - } - } - + /* Allocate device data */ t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); if (!t500rs) { @@ -1683,12 +1582,12 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { /* Cleanup T500RS device */ static int t500rs_wheel_destroy(void *data) { - struct t500rs_device_entry *t500rs = data; + struct t500rs_device_entry *t500rs = data; - if (!t500rs) { - /* Expected for boot mode devices that return -ENODEV before allocation */ - return 0; - } + if (!t500rs) { + pr_warn("t500rs_wheel_destroy: NULL data pointer\n"); + return 0; + } T500RS_DBG(t500rs, "T500RS: Cleaning up\n"); From 781c7096f21c75938188968daeac219df96fdf5e Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Thu, 18 Dec 2025 14:49:04 +0100 Subject: [PATCH 08/15] Updated effects slots to be more accurate --- src/tmt500rs/hid-tmt500rs.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index f6d0314d..fbabb523 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -57,8 +57,13 @@ #define T500RS_SAT_INERTIA 100 /* Hardware limits */ -#define T500RS_MAX_EFFECTS 16 -#define T500RS_MAX_HW_EFFECTS T500RS_MAX_EFFECTS +/* Advertise 15 logical effect slots to the framework (logical IDs 0..14). + * The device/hardware ID space remains 0..15 (16 entries), but we avoid using + * the hardware slot 0 (driver maps logical -> hw as logical+1). This prevents + * producing invalid hw_id == 16 when logical IDs of 0..15 are allowed. + */ +#define T500RS_MAX_EFFECTS 15 +#define T500RS_MAX_HW_EFFECTS 16 #define T500RS_BUFFER_LENGTH 32 /* HID report max packet size */ #define T500RS_HID_TIMEOUT 1000 /* 1 second */ From b7b1c29a06e5d0b829bb535ff236ec9db74e3f41 Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Thu, 18 Dec 2025 15:32:09 +0100 Subject: [PATCH 09/15] Remove un-necessary clamp for direction --- src/tmt500rs/hid-tmt500rs.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index 822119fa..a0d9a23b 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -1107,11 +1107,12 @@ static int t500rs_upload_effect(void *data, } /* Validate common parameters */ - if (effect->direction > 35999) { - hid_err(t500rs->hdev, "Direction %u exceeds maximum 35999\n", - effect->direction); - return -EINVAL; - } + /* Direction is provided by the Linux FF subsystem as 0..65535 (u16). + * The device expects 0..35999 (0.01° units); scaling is done by + * t500rs_scale_direction() when sending packets. Accept the full u16 + * range here instead of rejecting values >35999 (e.g. 49152). + */ + /* no validation needed here */ if (effect->replay.delay > 65535) { hid_err(t500rs->hdev, "Delay %u exceeds maximum 65535\n", effect->replay.delay); From 00e6a23fb2938371dddb6b13131fa3c5dfd9f099 Mon Sep 17 00:00:00 2001 From: Kimplul Date: Sun, 11 Jan 2026 16:04:53 +0200 Subject: [PATCH 10/15] rename ffb docs to roughly match --- docs/{FFBEFFECTS.md => T300RS_FFBEFFECTS.md} | 0 docs/{T500RS_USB_Protocol_Analysis.md => T500RS_FFBEFFECTS.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{FFBEFFECTS.md => T300RS_FFBEFFECTS.md} (100%) rename docs/{T500RS_USB_Protocol_Analysis.md => T500RS_FFBEFFECTS.md} (100%) diff --git a/docs/FFBEFFECTS.md b/docs/T300RS_FFBEFFECTS.md similarity index 100% rename from docs/FFBEFFECTS.md rename to docs/T300RS_FFBEFFECTS.md diff --git a/docs/T500RS_USB_Protocol_Analysis.md b/docs/T500RS_FFBEFFECTS.md similarity index 100% rename from docs/T500RS_USB_Protocol_Analysis.md rename to docs/T500RS_FFBEFFECTS.md From 982174a5f2234e74bc784fe2e3a01e4af1daf591 Mon Sep 17 00:00:00 2001 From: Kimplul Date: Sun, 11 Jan 2026 16:57:34 +0200 Subject: [PATCH 11/15] stylistic changes + Removal of trailing whitespace, unnecessary unicode chars, small formatting changes (though a larger kernel style check should probably be performed) --- src/hid-tmff2.c | 64 ++++++++++--------- src/hid-tmff2.h | 4 +- src/tmt500rs/hid-tmt500rs.c | 118 ++++++++++++++++++++++-------------- src/tmt500rs/hid-tmt500rs.h | 25 ++++---- 4 files changed, 121 insertions(+), 90 deletions(-) diff --git a/src/hid-tmff2.c b/src/hid-tmff2.c index bc494a63..b5ec5c7b 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -243,30 +243,35 @@ static ssize_t gain_store(struct device *dev, gain = value; - if (!tmff2->set_gain) + if (!tmff2->set_gain) return count; /* Rationale: two-level gain model - * - The input API's set_gain (pg) is the in-game gain (0..GAIN_MAX). + * - The input API's set_gain (pending_gain) 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 + * pending_gain = 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/T500RS_USB_Protocol_Analysis.md: Report glossary mentions 0x43 (gain), - * i.e., device-side gain separate from per-effect magnitudes; drivers should + * - docs/T300RS_FFBEFFECTS.md: section "FF_GAIN" shows a dedicated + * device gain path. + * + * - docs/T500RS_FFBEFFECTS.md: Report glossary mentions 0x43 (gain), + * i.e. device-side gain separate from per-effect magnitudes; drivers should * expose both levels. */ unsigned long flags; spin_lock_irqsave(&tmff2->lock, flags); - tmff2->pending_gain_value = GAIN_MAX; + + tmff2->pending_gain = GAIN_MAX; __set_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); + spin_unlock_irqrestore(&tmff2->lock, flags); + if (!delayed_work_pending(&tmff2->work) && tmff2->allow_scheduling) schedule_delayed_work(&tmff2->work, 0); return count; @@ -294,7 +299,7 @@ static void tmff2_set_gain(struct input_dev *dev, uint16_t value) /* Defer to workqueue: store pending gain and schedule */ spin_lock_irqsave(&tmff2->lock, flags); - tmff2->pending_gain_value = value; + tmff2->pending_gain = value; __set_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); spin_unlock_irqrestore(&tmff2->lock, flags); @@ -317,7 +322,7 @@ static void tmff2_set_autocenter(struct input_dev *dev, uint16_t value) /* Defer to workqueue: store pending autocenter and schedule */ spin_lock_irqsave(&tmff2->lock, flags); - tmff2->pending_autocenter_value = value; + tmff2->pending_autocenter = value; __set_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags); spin_unlock_irqrestore(&tmff2->lock, flags); @@ -335,34 +340,35 @@ static void tmff2_work_handler(struct work_struct *w) unsigned long time_now; __u16 effect_delay, effect_length; + uint16_t pending_gain = 0, pending_autocenter = 0; + bool set_gain = 0, set_autocenter = 0; 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 (test_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags)) { - pg = tmff2->pending_gain_value; - __clear_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); - do_gain = 1; - } - if (test_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags)) { - pac = tmff2->pending_autocenter_value; - __clear_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags); - do_ac = 1; - } - spin_unlock_irqrestore(&tmff2->lock, f2); + spin_lock_irqsave(&tmff2->lock, lock_flags); - 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); + if (test_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags)) { + pending_gain = tmff2->pending_gain; + __clear_bit(FF_EFFECT_QUEUE_GAIN, &tmff2->pending_flags); + set_gain = 1; } + if (test_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags)) { + pending_autocenter = tmff2->pending_autocenter; + __clear_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags); + set_autocenter = 1; + } + + spin_unlock_irqrestore(&tmff2->lock, lock_flags); + + if (set_gain && tmff2->set_gain) + tmff2->set_gain(tmff2->data, (pending_gain * gain) / GAIN_MAX); + + if (set_gain && tmff2->set_autocenter) + tmff2->set_autocenter(tmff2->data, pending_autocenter); + for (effect_id = 0; effect_id < tmff2->max_effects; ++effect_id) { unsigned long actions = 0; struct tmff2_effect_state effect; diff --git a/src/hid-tmff2.h b/src/hid-tmff2.h index c605a32d..39217f38 100644 --- a/src/hid-tmff2.h +++ b/src/hid-tmff2.h @@ -67,8 +67,8 @@ 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; + uint16_t pending_gain; + uint16_t pending_autocenter; unsigned long pending_flags; int allow_scheduling; diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index a0d9a23b..fe25bff9 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later /* * HID driver for Thrustmaster T500RS wheel base that provides Force feedback * - * Protocol documentation: docs/T500RS_USB_Protocol_Analysis.md - * + * Protocol documentation: docs/T500RS_FFBEFFECTS.md + * * Copyright (c) 2025 Casimir Bonnet */ @@ -70,7 +70,7 @@ static inline u8 t500rs_scale_mag_u7(int magnitude) { if (magnitude > 32767) magnitude = 32767; - /* Use 32-bit arithmetic to prevent overflow */ + /* Use long long arithmetic to prevent overflow */ return (u8)((magnitude * 127LL) / 32767); } @@ -227,7 +227,7 @@ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, u8 code, u8 magnitude, s8 offset, u8 phase, u16 period_ms) { /* Byte order per Windows USB captures (example: 04 2a 00 06 00 3f 0a 00): - * b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset, + * b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset, * b5=phase, b6-b7=period */ memset(p, 0, sizeof(*p)); @@ -366,7 +366,7 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, /* * Build a 0x05 conditional effect packet. * - * Per captures (T500RS_USB_Protocol_Analysis.md): + * Per captures (T500RS_FFBEFFECTS.md): * - packet structure with u8 coefficients and proper field layout * - Coefficients are sent as 0-10 scale (not zero) * - Center and deadband are scaled from Linux FFB ranges @@ -391,7 +391,7 @@ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, p->right_coeff = (u8)((right_coeff * 10) / 32767); p->left_coeff = (u8)((left_coeff * 10) / 32767); - /* Scale center from Linux ±32767 range to device s16 LE (approx ±500) */ + /* Scale center from Linux +-32767 range to device s16 LE (approx +-500) */ p->center = cpu_to_le16((s16)(center / 65)); /* Scale deadband from Linux 0-65535 range to device u16 LE (0-1008) */ @@ -446,7 +446,7 @@ static inline u8 t500rs_scale_envelope_level(u16 os_ffb_level) { if (os_ffb_level > 32767) os_ffb_level = 32767; - /* Use 32-bit arithmetic to prevent overflow */ + /* Use long long arithmetic to prevent overflow */ return (u8)((os_ffb_level * 255LL) / 32767); } @@ -470,8 +470,7 @@ static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, p->subtype = subtype; /* - * CRITICAL FIRMWARE BUG WORKAROUND: - * Per T500RS_USB_Protocol_Analysis.md, the device firmware rejects + * Per T500RS_EFFECTS.md, the device firmware rejects * non-zero envelope values for periodic and constant effects with * EPROTO (-71). Only ramp effects can safely use envelopes. * @@ -504,18 +503,25 @@ const signed short t500rs_effects[] = { FF_SAW_DOWN, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; /* Forward declarations to avoid implicit declarations before worker uses them */ -static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, +static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, size_t len); + static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, u8 hw_effect_id); + 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); @@ -544,16 +550,19 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, case T500RS_SEQ_STOP: ret = t500rs_send_stop(t500rs, hw_id); break; + case T500RS_SEQ_SYNC_42_05: buf[0] = 0x42; buf[1] = 0x05; ret = t500rs_send_hid(t500rs, buf, 2); break; + case T500RS_SEQ_SYNC_42_04: buf[0] = 0x42; buf[1] = 0x04; ret = t500rs_send_hid(t500rs, buf, 2); break; + case T500RS_SEQ_ENVELOPE: { struct t500rs_pkt_r02_envelope *env = (struct t500rs_pkt_r02_envelope *)buf; @@ -572,6 +581,7 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r02_envelope)); break; } + case T500RS_SEQ_CONSTANT: { s8 level = t500rs_scale_const_with_direction(effect->u.constant.level, effect->direction); @@ -580,6 +590,7 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); break; } + case T500RS_SEQ_PERIODIC_RAMP: { if (effect->type == FF_RAMP) { struct t500rs_pkt_r04_periodic_ramp *p = @@ -610,6 +621,7 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, break; break; } + case T500RS_SEQ_CONDITION_X: { u8 saturation = 0; const struct ff_condition_effect *cond = &effect->u.condition[0]; @@ -638,6 +650,7 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); break; } + case T500RS_SEQ_CONDITION_Y: { u8 saturation = 0; /* Y-axis: use condition[1] if available, else zeros */ @@ -667,6 +680,7 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); break; } + case T500RS_SEQ_MAIN: { u8 effect_type = 0; switch (effect->type) { @@ -712,19 +726,24 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, default: return -EINVAL; } + u16 duration_ms = effect->replay.length ? effect->replay.length : 0xffff; u16 delay_ms = effect->replay.delay; + struct t500rs_pkt_r01_main *m = (struct t500rs_pkt_r01_main *)buf; ret = t500rs_build_r01_main(m, hw_id, effect_type, duration_ms, delay_ms, param_sub, env_sub); if (ret) break; + ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r01_main)); break; } + default: ret = -EINVAL; } + if (ret) { hid_err(t500rs->hdev, "Sequence failed at step %zu/%zu (packet type 0x%02x): %d\n", @@ -756,7 +775,6 @@ static int t500rs_set_gain(void *data, u16 gain) { hid_info(t500rs->hdev, "FFB: set_gain %u -> device %u\n", gain, device_gain_byte); - /* Safe buffer access with bounds checking */ buf[0] = T500RS_PKT_GAIN; buf[1] = device_gain_byte; @@ -769,7 +787,7 @@ static int t500rs_set_gain(void *data, u16 gain) { } /* Send data via HID output report (blocking) */ -static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, +static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, size_t len) { int ret; @@ -780,16 +798,18 @@ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, return -EINVAL; } - ret = hid_hw_output_report(t500rs->hdev, (u8 *)data, len); + ret = hid_hw_output_report(t500rs->hdev, data, len); if (ret < 0) { hid_err(t500rs->hdev, "HID output report failed: %d\n", ret); return ret; } + if (ret != len) { hid_err(t500rs->hdev, "HID output report truncated: sent %d, expected %zu\n", ret, len); return -EIO; } + return 0; } @@ -801,19 +821,19 @@ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, const u8 *data, */ static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, u8 hw_effect_id) { - u8 *buf; struct t500rs_r41_cmd *r41; if (!t500rs) return -ENODEV; - buf = t500rs->send_buffer; - if (!buf) + + r41 = (struct t500rs_r41_cmd *)t500rs->send_buffer; + if (!r41) return -ENOMEM; - r41 = (struct t500rs_r41_cmd *)buf; + r41->id = 0x41; r41->effect_id = hw_effect_id; r41->command = 0x00; /* STOP */ r41->arg = 0x01; - return t500rs_send_hid(t500rs, buf, sizeof(*r41)); + return t500rs_send_hid(t500rs, (u8 *)r41, sizeof(*r41)); } /* @@ -821,18 +841,19 @@ static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, */ static inline int t500rs_send_start(struct t500rs_device_entry *t500rs, u8 hw_effect_id) { - u8 *buf; struct t500rs_r41_cmd *r41; if (!t500rs) return -ENODEV; - buf = t500rs->send_buffer; - r41 = (struct t500rs_r41_cmd *)buf; + r41 = (struct t500rs_r41_cmd *)t500rs->send_buffer; + if (!r41) + return -ENOMEM; + r41->id = 0x41; r41->effect_id = hw_effect_id; r41->command = 0x41; /* START */ r41->arg = 0x01; - return t500rs_send_hid(t500rs, buf, sizeof(*r41)); + return t500rs_send_hid(t500rs, (u8 *)r41, sizeof(*r41)); } /* Upload constant force effect */ @@ -866,7 +887,7 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, /* * Upload spring/damper/friction/inertia effect. * - * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * Per Windows captures (T500RS_FFBEFFECTS.md): * - 0x01 packet: direction=0x4000, code1=0x002a, code2=0x0038 * - Two 0x05 packets: X-axis (code 0x2a) and Y-axis (code 0x38) * - Saturation values 0x54 (84) for spring, 0x64 (100) for damper/friction @@ -926,7 +947,7 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, /* * Upload periodic effect (sine, square, triangle, saw). * - * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * Per Windows captures (T500RS_FFBEFFECTS.md): * - Waveform type is NOT encoded in USB packets; determined by Linux FFB subsystem * - 0x01 packet: direction, duration, delay, code1=0x000e, code2=0x001c * - 0x02 packet: envelope with subtype 0x1c @@ -1004,7 +1025,7 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, /* * Upload ramp effect. * - * Per Windows captures (T500RS_USB_Protocol_Analysis.md): + * Per Windows captures (T500RS_FFBEFFECTS.md): * - Ramp uses same 0x04 packet structure as periodic (code 0x2a) * - Packet sequence: 0x01 + 0x02 + 0x04 + 0x41 * - Start/end levels encoded in magnitude/offset fields @@ -1108,7 +1129,7 @@ static int t500rs_upload_effect(void *data, /* Validate common parameters */ /* Direction is provided by the Linux FF subsystem as 0..65535 (u16). - * The device expects 0..35999 (0.01° units); scaling is done by + * The device expects 0..35999 (0.01 degree units); scaling is done by * t500rs_scale_direction() when sending packets. Accept the full u16 * range here instead of rejecting values >35999 (e.g. 49152). */ @@ -1254,6 +1275,7 @@ static int t500rs_update_effect(void *data, t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), signed_level); return t500rs_send_hid(t500rs, (u8 *)r3, sizeof(*r3)); } + case FF_PERIODIC: { /* Apply direction projection to magnitude and adjust phase if needed */ u16 phase_raw = effect->u.periodic.phase; @@ -1263,6 +1285,7 @@ static int t500rs_update_effect(void *data, u8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); u16 period_ms = effect->u.periodic.period; u16 param_sub, env_sub; + if (period_ms == 0) { hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); return -EINVAL; @@ -1275,20 +1298,24 @@ static int t500rs_update_effect(void *data, period_ms); return t500rs_send_hid(t500rs, buf, sizeof(*p)); } + case FF_RAMP: { u16 duration_ms = effect->replay.length; if (duration_ms == 0) { hid_err(t500rs->hdev, "Ramp effect duration cannot be zero\n"); return -EINVAL; } + u16 param_sub, env_sub; t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); struct t500rs_pkt_r04_periodic_ramp *p = (struct t500rs_pkt_r04_periodic_ramp *)buf; t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), effect->u.ramp.start_level, effect->u.ramp.end_level, duration_ms); + return t500rs_send_hid(t500rs, buf, sizeof(*p)); } + case FF_SPRING: case FF_DAMPER: case FF_FRICTION: @@ -1329,12 +1356,14 @@ static int t500rs_update_effect(void *data, saturation = T500RS_SAT_DAMPER; /* Default to damper level */ break; } + struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), cond->right_coeff, cond->left_coeff, saturation, cond->deadband, cond->center); return t500rs_send_hid(t500rs, buf, sizeof(*p)); } + default: return 0; } @@ -1403,14 +1432,13 @@ static int t500rs_set_range(void *data, u16 range) { u16 range_value; /* Validate range - minimum 40 degrees, maximum 1080 degrees */ - if (range < T500RS_RANGE_MIN) { + if (range < T500RS_RANGE_MIN) range = T500RS_RANGE_MIN; - } - if (range > T500RS_RANGE_MAX) { + + if (range > T500RS_RANGE_MAX) range = T500RS_RANGE_MAX; - } - /* Use DMA-safe preallocated buffer */ + /* Use preallocated buffer */ buf = t500rs->send_buffer; T500RS_DBG(t500rs, "Setting wheel range to %u degrees\n", range); @@ -1448,8 +1476,9 @@ static int t500rs_set_range(void *data, u16 range) { /* Initialize T500RS device */ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { struct t500rs_device_entry *t500rs = NULL; - u8 *init_buf; /* Will use send_buffer for DMA-safe transfers */ + u8 *init_buf; /* Will use send_buffer for transfers */ int ret; + /* Sanity check protocol main-upload packet size against documentation */ BUILD_BUG_ON(sizeof(struct t500rs_pkt_r01_main) != 15); @@ -1458,9 +1487,10 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { pr_err("t500rs_wheel_init: NULL tmff2 structure\n"); return -EINVAL; } + if (!tmff2->hdev || !tmff2->input_dev) { - pr_err("t500rs_wheel_init: Invalid tmff2 structure (missing hdev or " - "input_dev)\n"); + pr_err("t500rs_wheel_init: Invalid tmff2 structure" + " (missing hdev or input_dev)\n"); return -EINVAL; } @@ -1505,25 +1535,22 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { init_buf[0] = 0x42; init_buf[1] = 0x04; ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) { + if (ret) hid_warn(t500rs->hdev, "Init command 0x42 0x04 failed: %d\n", ret); - } memset(init_buf, 0, 2); init_buf[0] = 0x42; init_buf[1] = 0x05; ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) { + if (ret) hid_warn(t500rs->hdev, "Init command 0x42 0x05 failed: %d\n", ret); - } memset(init_buf, 0, 2); init_buf[0] = 0x42; init_buf[1] = 0x00; ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) { + if (ret) hid_warn(t500rs->hdev, "Init command 0x42 0x00 failed: %d\n", ret); - } /* Report 0x40 - Enable FFB (4 bytes) * Magic value seen in captures that enables FFB on the base. @@ -1534,9 +1561,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { init_buf[2] = 0x42; init_buf[3] = 0x7b; ret = t500rs_send_hid(t500rs, init_buf, 4); - if (ret) { + if (ret) hid_warn(t500rs->hdev, "Init command 2 (0x40 enable) failed: %d\n", ret); - } /* Report 0x40 - Disable built-in autocenter (4 bytes) */ memset(init_buf, 0, 4); @@ -1549,9 +1575,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { init_buf[2] = 0x00; init_buf[3] = 0x00; ret = t500rs_send_hid(t500rs, init_buf, 4); - if (ret) { + if (ret) hid_warn(t500rs->hdev, "Init command 3 (0x40 config) failed: %d\n", ret); - } /* Report 0x43 - Set global gain (2 bytes) * Start at maximum device gain; the FFB gain callback will adjust later. @@ -1560,9 +1585,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { init_buf[0] = 0x43; init_buf[1] = 0xFF; ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) { + if (ret) hid_warn(t500rs->hdev, "Init command 4 (0x43) failed: %d\n", ret); - } hid_info(t500rs->hdev, "T500RS initialized successfully (HID mode)\n"); T500RS_DBG(t500rs, "Buffer: %zu bytes\n", t500rs->buffer_length); diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index fbabb523..c832ab48 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -1,17 +1,17 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: GPL-2.0-or-later /* * T500RS Force Feedback Protocol Constants and Structures for * Thrustmaster T500RS wheel base. * - * Protocol documentation: docs/T500RS_USB_Protocol_Analysis.md + * Protocol documentation: docs/T500RS_FFBEFFECTS.md * This header defines all protocol-specific constants and packet structures * for the Thrustmaster T500RS racing wheel force feedback implementation. * * Copyright (c) 2025 Casimir Bonnet */ -#ifndef __T500RS_H -#define __T500RS_H +#ifndef __HID_TMT500RS_H +#define __HID_TMT500RS_H #include @@ -113,7 +113,7 @@ extern const signed short t500rs_effects[]; * * Packet format: * - b0: packet type (0x01) - * - b1: hardware effect slot ID (0-15, assigned by driver) + * - b1: hardware effect slot ID (1-15, assigned by driver) * - b2: effect type (T500RS_EFFECT_* constants) * - b3: control flags (always 0x40) * - b4-b5: duration in milliseconds (LE) @@ -125,7 +125,7 @@ extern const signed short t500rs_effects[]; */ struct t500rs_pkt_r01_main { u8 id; /* b0: T500RS_PKT_MAIN */ - u8 effect_id; /* b1: hardware effect slot ID (0-15) */ + u8 effect_id; /* b1: hardware effect slot ID (1-15) */ u8 effect_type; /* b2: effect type (T500RS_EFFECT_*) */ u8 control; /* b3: always T500RS_CONTROL_DEFAULT (0x40) */ __le16 duration_ms; /* b4-b5: duration in ms (LE) */ @@ -148,10 +148,11 @@ struct t500rs_pkt_r01_main { * - b2: reserved (0x00) * - b3: magnitude (0-127, scaled from SDL 0-32767) * - b4: offset (signed -127 to +127, scaled from SDL -32768 to +32767) - * - b5: phase (0-255 for 360°, scaled from SDL 0-35999) + * - b5: phase (0-255 for 360 degrees, scaled from SDL 0-35999) * - b6-b7: period in milliseconds (LE, no Hz conversion!) * - * For ramp effects: phase=0, period=ramp duration, magnitude/offset encode start/end levels. + * For ramp effects: phase=0, period=ramp duration, magnitude/offset encode + * start/end levels. */ struct t500rs_pkt_r04_periodic_ramp { u8 id; /* b0: T500RS_PKT_PERIODIC */ @@ -172,15 +173,15 @@ struct t500rs_pkt_r04_periodic_ramp { * - b2: reserved (always 0x00) * - b3: right coefficient (u8, 0-10 scale) * - b4: left coefficient (u8, 0-10 scale) - * - b5-b6: center/offset (s16 LE, scaled from Linux ±32767 range) + * - b5-b6: center/offset (s16 LE, scaled from Linux +-32767 range) * - b7-b8: deadband (u16 LE, scaled from Linux 0-65535 range) * - b9: right saturation (0-100, controls effect strength) * - b10: left saturation (0-100, controls effect strength) * * Scaling (from Linux FFB to device): - * - Coefficients: (value * 10) / 32767 → 0-10 u8 - * - Center: value / 65 → s16 LE (approx ±500 range) - * - Deadband: value / 65 → u16 LE (0-1008 range) + * - Coefficients: (value * 10) / 32767 -> 0-10 u8 + * - Center: value / 65 -> s16 LE (approx +-500 range) + * - Deadband: value / 65 -> u16 LE (0-1008 range) * - Saturation: 0-100 (no scaling) */ struct t500rs_pkt_r05_condition { From be64e21862e09a8bf1137817f57548f0a3665319 Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Tue, 13 Jan 2026 11:29:13 +0100 Subject: [PATCH 12/15] Code and documentation fixes - Unicode char replacement - Documented duration/delays with packet upload - Code tidy-up (parameters renaming) - Set structs for init sequence - Invalid conditional effect types now cause an error instead of silently defaulting - Ported check for modified parameters to all effects - Fix saturation handling in T500RS driver & update documentation to reflect it - Update documentation to clarify dynamic packet code values - Update examples to use hardware ID 1 instead of 0 - Moved Subtype system section to the bottom - Hardened documentation style - Provide details for "DC bias" terminology - Do not discard 100% gain set but kept warning, more explicit. --- Kbuild | 2 +- docs/T500RS_FFBEFFECTS.md | 630 ++++++++++++++++++------------------ src/tmt500rs/hid-tmt500rs.c | 198 ++++++------ src/tmt500rs/hid-tmt500rs.h | 14 +- 4 files changed, 411 insertions(+), 433 deletions(-) diff --git a/Kbuild b/Kbuild index 5562215e..84a37c50 100644 --- a/Kbuild +++ b/Kbuild @@ -6,4 +6,4 @@ hid-tmff-new-y := \ src/tmtx/hid-tmtx.o \ src/tmtsxw/hid-tmtsxw.o \ src/tmtspc/hid-tmtspc.o \ - src/tmt500rs/hid-tmt500rs.o \ No newline at end of file + src/tmt500rs/hid-tmt500rs.o diff --git a/docs/T500RS_FFBEFFECTS.md b/docs/T500RS_FFBEFFECTS.md index 77240f75..7ca198d0 100644 --- a/docs/T500RS_FFBEFFECTS.md +++ b/docs/T500RS_FFBEFFECTS.md @@ -1,10 +1,40 @@ # T500RS USB Force Feedback Protocol Analysis + ## Comprehensive Effect Implementation Reference -This is the result of the deep analysis of captures made using ffbsdl tool on windows and the implementation iterations to get a working driver supporting (hopefully) all effects on-par with windows official driver. +This document provides a detailed analysis of the T500RS force feedback protocol, based on USB captures using the ffbsdl tool on Windows and implementation iterations to create a Linux driver that supports all effects on par with the official Windows driver. + +All values are little-endian unless specified otherwise. + +> **NOTE:** All values documented here are examples of actual commands captured on the USB interface, not the only possible values. --- -## USB Packet Types Overview +## GENERAL CONCEPTS + +### Device Overview +The T500RS is a single-axis force feedback wheel with a rotating range that can be configured (typically 900 degrees or 1080 degrees). It uses a proprietary USB protocol for force feedback effects, distinct from the T300RS and other Thrustmaster wheels. + +### Effect Playing and Stopping + +#### Playing an Effect +``` +41 00 41 01 - START command +``` +- **Packet Type:** 0x41 (Command) +- **Effect ID:** 0x00 (always 0x00 for T500RS) +- **Command:** 0x41 (START) +- **Argument:** 0x01 (play count, 0x01 = play once) + +#### Stopping an Effect +``` +41 00 00 01 - STOP command +``` +- **Packet Type:** 0x41 (Command) +- **Effect ID:** 0x00 (always 0x00 for T500RS) +- **Command:** 0x00 (STOP) +- **Argument:** 0x01 (stop parameter) + +### USB Packet Types Overview | Packet ID | Name | Size | Purpose | |-----------|------|------|---------| @@ -19,47 +49,10 @@ This is the result of the deep analysis of captures made using ffbsdl tool on wi | 0x49 | Polling | 7-16 bytes | Status polling (high frequency) | | 0x07 | Telemetry | 15 bytes | Position feedback (high frequency) | -**Note:** Envelope support (0x02) is limited on T500RS hardware. Non-zero envelope values cause EPROTO errors on periodic and constant effects. Always send zeros for envelope parameters on these effect types. - ---- - -## Subtype System and Effect Indexing - -On this wheel the last six bytes of the 0x01 main upload (bytes 9–14) do **not** contain envelope timings/levels directly compared to the T300RS. Instead they carry two 16‑bit "subtype" values that act as per‑effect indices: - -- Bytes 9–10 → `parameter_subtype` -- Bytes 11–12 → `envelope_subtype` -- Bytes 13–14 → padding (always 0x0000 in captures) - -These subtype values are then copied into the "code" / "subtype" field of other packets (0x02, 0x03, 0x04, 0x05) so the device can associate parameter/envelope packets with a particular logical effect. - -For effect index **n** (0‑based) the wheel uses a simple arithmetic progression: - -- `parameter_subtype = 0x000e + 0x001c * n` -- `envelope_subtype = 0x001c + 0x001c * n` - -Observed pairs from captures: - -| Effect index n | parameter_subtype | envelope_subtype | -|----------------|-------------------|------------------| -| 0 | 0x000e | 0x001c | -| 1 | 0x002a | 0x0038 | -| 2 | 0x0046 | 0x0054 | -| 3 | 0x0062 | 0x0070 | -| 4 | 0x007e | 0x008c | -| 5 | 0x009a | 0x00a8 | -| 6 | 0x00b6 | 0x00c4 | - -Implications for the driver: - -- `effect_id` (byte 1 of 0x01) stays **0x00** for normal uploads; logical effect slots are selected purely via these subtype pairs. -- The same subtype values appear in: - - 0x02 envelope packets (`subtype = envelope_subtype`) - - 0x03 constant packets (`code = parameter_subtype`) - - 0x04 periodic/ramp packets (`code = parameter_subtype`) - - 0x05 condition packets (`code = parameter_subtype` for the first, `code = envelope_subtype` for the second) - -Envelope attack/fade length and level values themselves live **only** in the 0x02 packets; bytes 9–14 of 0x01 are *references* to those blocks, not the envelope parameters. +**Important Notes:** +- **Envelope Support:** Envelope packets (0x02) have limited support. Non-zero envelope values cause EPROTO errors on periodic and constant effects. Always send zeros for envelope parameters on these effect types. +- **Runtime Updates:** Effect updates (via `update_effect` callback) only modify parameter-specific packets (0x03, 0x04, 0x05). Duration and delay changes require re-uploading the entire effect. +- **Effect Indexing:** The T500RS uses a unique subtype system for effect indexing. See the [Subtype System and Effect Indexing](#subtype-system-and-effect-indexing) section for details. --- @@ -106,8 +99,8 @@ Offset | Size | Field | Description - Alternative codes observed: 0x00b6/0x00c4 (newer captures), 0x0046/0x0054, 0x0062/0x0070, 0x007e/0x008c, 0x009a/0x00a8 **Examples:** -- `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00` - Constant effect with envelope - - Effect ID: 0x00 +- `01 01 00 40 f4 01 00 00 0e 00 1c 00 00 00` - Constant effect with envelope + - Effect ID: 0x01 (hardware slot 1, logical 0) - Effect type: 0x00 (constant) - Control: 0x40 - Duration: 0x01f4 = 500ms @@ -116,8 +109,8 @@ Offset | Size | Field | Description - Packet codes: 0x000e (constant), 0x001c (envelope) - Reserved2: 0x0000 -- `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` - Conditional effect - - Effect ID: 0x00 +- `01 01 40 40 d0 07 00 00 2a 00 38 00 00 00` - Conditional effect + - Effect ID: 0x01 (hardware slot 1, logical 0) - Effect type: 0x40 (conditional) - Control: 0x40 - Duration: 0x07d0 = 2000ms @@ -131,7 +124,7 @@ Offset | Size | Field | Description Offset | Size | Field | Description -------|------|----------------|---------------------------------- 0 | 1 | packet_type | 0x02 -1 | 1 | subtype | 0x1c +1 | 1 | subtype | Low byte of envelope_subtype from 0x01 packet (dynamic) 2 | 2 | attack_len_ms | Attack duration in ms, little-endian 4 | 1 | attack_level | Attack level 0-255 5 | 2 | fade_len_ms | Fade duration in ms, little-endian @@ -156,7 +149,7 @@ for these effect types. The Linux driver must also send zeros to avoid crashes. Offset | Size | Field | Description -------|------|----------------|---------------------------------- 0 | 1 | packet_type | 0x03 -1 | 1 | code | 0x0e +1 | 1 | code | Low byte of parameter_subtype from 0x01 packet (dynamic) 2 | 1 | reserved | 0x00 3 | 1 | level | Signed -127 to +127 ``` @@ -179,14 +172,14 @@ Offset | Size | Field | Description 7 | 1 | reserved | 0x00 ``` -**Code Values:** The code in byte 1 matches bytes 9-10 of the 0x01 packet (0x2a, 0xb6, 0x46, etc.) +**Code Values:** The code in byte 1 is the low byte of the parameter_subtype from the 0x01 packet (dynamically calculated based on effect index) -**Period Encoding:** Period is in MILLISECONDS (not Hz×100). No conversion needed. +**Period Encoding:** Period is in MILLISECONDS (not Hz*100). No conversion needed. **Examples:** - `04 2a 00 00 00 0a 00 00` - Code 0x2a, magnitude 0, period 10ms -- `04 2a 06 00 3f 0a 00 00` - Code 0x2a, magnitude 6, phase 63 (88.6°), period 10ms -- `04 2a 09 00 7f 64 00 00` - Code 0x2a, magnitude 9, phase 127 (178.6°), period 100ms +- `04 2a 06 00 3f 0a 00 00` - Code 0x2a, magnitude 6, phase 63 (88.6degrees), period 10ms +- `04 2a 09 00 7f 64 00 00` - Code 0x2a, magnitude 9, phase 127 (178.6degrees), period 100ms - `04 b6 00 00 7f 00 00 00` - Code 0xb6, magnitude 0, phase 127 (ramp effect) ### 0x05 - Conditional Effect Packet (11 bytes) @@ -198,7 +191,7 @@ Offset | Size | Field | Description Offset | Size | Field | Description -------|------|----------------|---------------------------------- 0 | 1 | packet_type | 0x05 -1 | 1 | code | Variable (from 0x01 packet, e.g. 0x0e, 0x1c, 0x2a, 0x38) +1 | 1 | code | Low byte of parameter_subtype or envelope_subtype from 0x01 packet (dynamic) 2 | 1 | reserved | Always 0x00 3 | 1 | right_coeff | Right/positive coefficient (0-10 scale, u8) 4 | 1 | left_coeff | Left/negative coefficient (0-10 scale, u8) @@ -212,11 +205,11 @@ Offset | Size | Field | Description **NOTE:** T500RS is single-axis, so Y-axis packet typically contains zeros. -**Parameter Scaling (Linux FFB → Device):** -- **Coefficients:** 0-10000 → Device 0-10 (divide by 1000) -- **Center/Offset:** -10000 to +10000 → Device s16 LE (divide by 20) -- **Deadband:** 0-10000 → Device u16 LE (divide by 10) -- **Saturation:** 0-100 percentage +**Parameter Scaling (Linux FFB -> Device):** +- **Coefficients:** 0-32767 -> Device 0-10 (multiply by 10/32767) +- **Center/Offset:** -32767 to +32767 -> Device s16 LE (divide by 65) +- **Deadband:** 0-65535 -> Device u16 LE (divide by 65) +- **Saturation:** 0-65535 -> Device 0-100 (multiply by 100/65535) **Examples from Captures:** - `05 0e 00 0a 0a 00 00 00 00 64 64` - Coeffs=10,10, center=0, deadband=0, sat=100 @@ -243,197 +236,265 @@ Offset | Size | Field | Description ### 1. CONSTANT FORCE EFFECTS -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Constant Zero** | level=0, dir=0°, len=500ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`03 0e 00 00`
`41 00 41 01` | -| **Constant Low** | level=8000, dir=0°, len=2000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 06 00 00 06 00`
`03 0e 00 03`
`41 00 41 01` | -| **Constant Medium** | level=24000, dir=0°, len=2000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 12 00 00 12 00`
`03 0e 00 09`
`41 00 41 01` | -| **Constant High** | level=48000, dir=0°, len=5000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 88 13 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 0d 00 00 0d 00`
`03 0e 00 f9`
`41 00 41 01` | -| **Constant Max** | level=65535, dir=180°, len=2000ms | 1. Upload
2. Envelope
3. Constant
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`03 0e 00 00`
`41 00 41 01` | +**Capture Examples:** +- Zero force: `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `03 0e 00 00` +- Low positive force: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 06 00 00 06 00` `03 0e 00 03` +- Medium force: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 12 00 00 12 00` `03 0e 00 09` +- High negative force: `01 00 00 40 88 13 00 00 0e 00 1c 00 00 00` `02 1c 00 00 0d 00 00 0d 00` `03 0e 00 f9` +- Maximum force with direction: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `03 0e 00 00` -**Scaling Notes:** -- SDL2 level (0-65535) → Device level (-127 to +127) -- Envelope attack/fade level (0-32767) → Device level (0-255) -- Direction (0-35999) in 0.01 degree units +**Packet Structure:** +- Main packet: `01 [effect_id] 00 40 [duration] [delay] 00 0e 00 1c 00 00 00` + - effect_type = 0x00 (constant) + - codes: 0x000e (constant parameter), 0x001c (envelope) +- Envelope packet: `02 1c [attack_len] [attack_lvl] [fade_len] [fade_lvl] 00` +- Constant packet: `03 0e 00 [level]` + +**Parameter Details:** +- Force level: s8 (-127 to +127, scaled from Linux 0-65535 range) +- Direction: Applied during level scaling (projection onto wheel axis) +- Envelope: Attack/fade levels scaled 0-255 from Linux 0-32767 +- Duration/Delay: Direct milliseconds in main packet ### 2. PERIODIC EFFECTS - SINE WAVE -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Sine Zero** | mag=0, period=10ms, phase=0°, dir=0° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 00 00 00 0a 00 00`
`41 00 41 01` | -| **Sine Low** | mag=8000, period=10ms, phase=90°, dir=90° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 06 00 00 06 00`
`04 2a 06 00 3f 0a 00 00`
`41 00 41 01` | -| **Sine Medium** | mag=24000, period=100ms, phase=180°, dir=180° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 12 00 00 12 00`
`04 2a 09 00 7f 64 00 00`
`41 00 41 01` | -| **Sine with Envelope** | mag=24000, period=100ms, attack=500ms, fade=500ms | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c f4 01 12 f4 01 12 00`
`04 2a 09 00 00 64 00 00`
`41 00 41 01` | +**Capture Examples:** +- Zero magnitude: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `04 2a 00 00 00 0a 00 00` +- Low magnitude with phase: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 06 00 00 06 00` `04 2a 06 00 3f 0a 00 00` +- Medium magnitude: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 12 00 00 12 00` `04 2a 09 00 7f 64 00 00` +- With envelope: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c f4 01 12 f4 01 12 00` `04 2a 09 00 00 64 00 00` -**Periodic Effect Notes:** -- Magnitude (0-32767) → Device magnitude (0-127) -- Phase (0-35999, 0.01° units) → Device phase (0-255, 256 steps for 360°) -- Period in milliseconds (no conversion needed) -- Code 0x2a is used (NOT 0x0e as in current driver!) +**Packet Structure:** +- Main packet: `01 [effect_id] 22 40 [duration] [delay] 00 2a 00 1c 00 00 00` + - effect_type = 0x22 (sine wave) + - codes: 0x002a (periodic parameters), 0x001c (envelope) +- Envelope packet: `02 1c [attack_len] [attack_lvl] [fade_len] [fade_lvl] 00` +- Periodic packet: `04 2a [magnitude] [offset] [phase] [period_ms] 00` + +**Parameter Details:** +- Magnitude: 0-127 (scaled from Linux 0-32767) +- Offset: s8 (-128 to +127, DC bias (Direct Current bias - a constant force offset), scaled from Linux -32768 to +32767) +- Phase: 0-255 (256 steps for 360 degrees, scaled from Linux 0-35999) +- Period: Direct milliseconds (no Hz conversion!) +- Direction: Applied during magnitude scaling (projection onto wheel axis) ### 3. PERIODIC EFFECTS - TRIANGLE WAVE -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Triangle Medium** | mag=24000, period=100ms, phase=180°, dir=180° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 12 00 00 12 00`
`04 2a 09 00 7f 64 00 00`
`41 00 41 01` | +**Capture Examples:** +- Triangle wave: `01 00 21 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 12 00 00 12 00` `04 2a 09 00 7f 64 00 00` + +**Packet Structure:** +- Main packet: `01 [effect_id] 21 40 [duration] [delay] 00 2a 00 1c 00 00 00` + - effect_type = 0x21 (triangle wave) +- Same envelope and periodic packet structure as sine wave -**Note:** Triangle uses same packet structure as sine; waveform type is determined by effect type in SDL2 upload, not in USB packets. +**Note:** Waveform type determined by effect_type in main packet, not in periodic packet parameters. ### 4. PERIODIC EFFECTS - SAWTOOTH UP -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Sawtooth Up High** | mag=48000, period=100ms, phase=270°, dir=270° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 0d 00 00 0d 00`
`04 2a 06 00 bf 64 00 00`
`41 00 41 01` | +**Capture Examples:** +- Sawtooth up: `01 00 23 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 0d 00 00 0d 00` `04 2a 06 00 bf 64 00 00` + +**Packet Structure:** +- Main packet: `01 [effect_id] 23 40 [duration] [delay] 00 2a 00 1c 00 00 00` + - effect_type = 0x23 (sawtooth up) +- Same envelope and periodic packet structure as sine wave ### 5. PERIODIC EFFECTS - SAWTOOTH DOWN -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Sawtooth Down Max** | mag=65535, period=1000ms, phase=0°, dir=0°, offset=+16000 | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 00 05 7f e8 03 00`
`41 00 41 01` | +**Capture Examples:** +- Sawtooth down with offset: `01 00 24 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `04 2a 00 05 7f e8 03 00` -**Note:** Offset field (byte 3 of 0x04 packet) allows DC bias on periodic effects. +**Packet Structure:** +- Main packet: `01 [effect_id] 24 40 [duration] [delay] 00 2a 00 1c 00 00 00` + - effect_type = 0x24 (sawtooth down) +- Same envelope and periodic packet structure as sine wave + +**Note:** Offset field allows DC bias (Direct Current bias - a constant force offset) - useful for asymmetric waveforms like sawtooth where you want to shift the entire waveform up or down. This creates a net force in one direction over time. ### 6. RAMP EFFECTS -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Ramp Up Low→High** | start=8000, end=48000, len=1000ms, dir=0° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 e8 03 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 03 00 00 e8 03 00`
`41 00 41 01` | -| **Ramp Down High→Low** | start=48000, end=8000, len=1000ms, dir=180° | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 e8 03 00 00 0e 00 1c 00 00 00`
`02 1c 00 00 00 00 00 00 00`
`04 2a 03 00 00 e8 03 00`
`41 00 41 01` | -| **Ramp with Envelope** | start=8000, end=65535, len=5000ms, attack=500ms, fade=500ms | 1. Upload
2. Envelope
3. Periodic
4. START | `01 00 00 40 88 13 00 00 0e 00 1c 00 00 00`
`02 1c f4 01 12 f4 01 12 00`
`04 2a 03 00 00 27 10 00`
`41 00 41 01` | +**Capture Examples:** +- Ramp up: `01 00 24 40 e8 03 00 00 2a 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `04 2a 03 00 00 e8 03 00` +- Ramp down: `01 00 24 40 e8 03 00 00 2a 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `04 2a 03 00 00 e8 03 00` +- Ramp with envelope: `01 00 24 40 88 13 00 00 2a 00 1c 00 00 00` `02 1c f4 01 12 f4 01 12 00` `04 2a 03 00 00 27 10 00` -**Ramp Effect Notes:** -- Ramp effects use 0x04 packet type (same as periodic) -- Start/end levels encoded in magnitude and offset fields -- Period field may encode ramp duration or rate +**Packet Structure:** +- Main packet: `01 [effect_id] 24 40 [duration] [delay] 00 2a 00 1c 00 00 00` + - effect_type = 0x24 (sawtooth down - used for ramps) + - codes: 0x002a (ramp parameters), 0x001c (envelope) +- Envelope packet: `02 1c [attack_len] [attack_lvl] [fade_len] [fade_lvl] 00` +- Ramp packet: `04 2a [magnitude] [offset] [phase] [period_ms] 00` + +**Parameter Details:** +- Magnitude: Average of start/end levels (0-127 scale) +- Offset: Difference between start/end levels (direction encoding) +- Phase: 0x7f for positive ramp (startend) +- Period: Ramp duration in milliseconds +- Direction: Applied during magnitude calculation (projection onto wheel axis) ### 7. CONDITIONAL EFFECTS - SPRING -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Spring Low/High** | right_coeff=low, left_coeff=low, right_sat=high, left_sat=high | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 00 00 54 54`
`05 38 00 00 00 00 00 00 00 54 54`
`41 00 41 01` | -| **Spring Deadband** | right_coeff=medium, left_coeff=medium, deadband=500, center=0 | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 07 00 54 54`
`05 38 00 00 00 00 00 00 00 54 54`
`41 00 41 01` | -| **Spring Asymmetric** | right_coeff=0, left_coeff=high, deadband=5000, center=0 | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 99 00 4c 00 54 54`
`05 38 00 00 00 00 00 00 00 54 54`
`41 00 41 01` | - -**Conditional Packet Structure (0x05):** -- Two packets sent per conditional effect (one per axis) -- First packet: code 0x2a (X-axis parameters) -- Second packet: code 0x38 (Y-axis parameters) -- Bytes 2-3: Right coefficient (little-endian, 0-65535 scaled) -- Bytes 4-5: Left coefficient (little-endian, 0-65535 scaled) -- Bytes 6-7: Deadband (little-endian, scaled) -- Byte 8: Center offset (signed) -- Bytes 9-10: Right/Left saturation (0x5454 = 84,84) +**Capture Examples:** +- Basic spring with low coefficients: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 00 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` +- Spring with deadband: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 07 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` +- Asymmetric spring: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 99 00 4c 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` + +**Packet Structure:** +- Main packet: `01 [effect_id] 40 40 [duration] [delay] 00 2a 00 38 00 00 00` + - effect_type = 0x40 (spring) + - codes: 0x002a (X-axis), 0x0038 (Y-axis) +- First 0x05 packet (X-axis): `05 2a [right_coeff] [left_coeff] [center] [deadband] [right_sat] [left_sat]` +- Second 0x05 packet (Y-axis): `05 38 [right_coeff] [left_coeff] [center] [deadband] [right_sat] [left_sat]` + +**Parameter Details:** +- Coefficients: 0-10 scale (Linux 0-32767 range) +- Center: s16 LE (+-500 range from Linux +-32767) +- Deadband: u16 LE (0-1008 from Linux 0-65535) +- Saturation: Dynamic right/left saturation (0-100 scale from Linux 0-65535 range) +- Y-axis typically uses zeros for single-axis wheel ### 8. CONDITIONAL EFFECTS - DAMPER -| Test Case | SDL2 Parameters | Packet Sequence | Packet Payloads | -|-----------|----------------|-----------------|-----------------| -| **Damper Low** | right_coeff=low, left_coeff=low, right_sat=max, left_sat=max | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 00 00 64 64`
`05 38 00 00 00 00 00 00 00 64 64`
`41 00 41 01` | -| **Damper High** | right_coeff=high, left_coeff=high | 1. Upload
2. Cond Axis 1
3. Cond Axis 2
4. START | `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00`
`05 2a 00 00 00 00 00 00 00 64 64`
`05 38 00 00 00 00 00 00 00 64 64`
`41 00 41 01` | +**Capture Examples:** +- Basic damper: `01 00 41 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 00 00 64 64` `05 38 00 00 00 00 00 00 00 64 64` +- Damper with coefficients: `01 00 41 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 0a 0a 00 00 00 00 64 64` `05 38 00 00 00 00 00 00 00 64 64` -**Note:** Damper uses same 0x05 packet structure as spring. Saturation values differ (0x6464 for damper vs 0x5454 for spring). +**Packet Structure:** +- Main packet: `01 [effect_id] 41 40 [duration] [delay] 00 2a 00 38 00 00 00` + - effect_type = 0x41 (damper/friction/inertia) + - codes: 0x002a (X-axis), 0x0038 (Y-axis) +- First 0x05 packet (X-axis): `05 2a [right_coeff] [left_coeff] [center] [deadband] [right_sat] [left_sat]` +- Second 0x05 packet (Y-axis): `05 38 [right_coeff] [left_coeff] [center] [deadband] [right_sat] [left_sat]` + +**Parameter Details:** +- Same structure as spring effects +- Saturation: Dynamic right/left saturation (0-100 scale from Linux 0-65535 range) +- Windows driver typically sends zero coefficients, relying on saturation +- FFEdit captures show non-zero coefficients may provide finer control ### 10. CONDITIONAL EFFECTS - INERTIA -**Capture Example:** -- Input: Axis0 coefficient=10000, offset=-7439; Axis1 coefficient=10000, offset=0 -- Main packet: `01 04 41 40 7a 09 00 ff ff 42 01 50 01 00 00` - - effect_type = 0x41 (same as damper) - - duration = 0x097a (2426ms) - - codes: 0x0142, 0x0150 - -- First 0x05 packet (X-axis): `05 42 01 0a 0a 8c fe 00 00 64 64` - - code = 0x42 - - right_coeff = 0x0a01 (2561) - **NON-ZERO!** - - left_coeff = 0x8c0a (35850 or -29686 signed) - **NON-ZERO!** - - deadband = 0x00fe (254) - - center = 0x00 - - saturation = 0x64 (100) - -- Second 0x05 packet (Y-axis): `05 50 01 00 00 00 00 00 00 64 64` - - code = 0x50 - - right_coeff = 0x0001 (1) - - left_coeff = 0x0000 (0) - - deadband = 0x0000 (0) - - center = 0x00 - - saturation = 0x64 (100) - -**Implication:** The current driver sends zero coefficients for all conditional effects (matching Windows driver behavior for spring/damper). However, inertia may require non-zero coefficients for proper behavior. This needs further testing. - -**Expected Structure:** -- Two 0x05 packets with codes from 0x01 bytes 9-12 -- Same parameter layout: right_coeff, left_coeff, deadband, center, saturation -- Saturation value: 0x64 (100) - same as damper -- Coefficients: May need to be non-zero for proper inertia feel - -### 11. CONDITIONAL EFFECTS - FRICTION - -**Status:** Limited capture data available. Assumed to use same 0x05 packet structure as spring/damper. - -**Expected Structure:** -- Two 0x05 packets with codes from 0x01 bytes 9-12 -- Same parameter layout: right_coeff, left_coeff, deadband, center, saturation -- Saturation value: 0x64 (100) - same as damper -- Coefficients: May need to be non-zero (similar to inertia) - needs verification - -### 12. MULTI-EFFECT SCENARIOS - -#### Sequential Effects (No Overlap) - -| Scenario | Description | Effect IDs Used | Packet Sequence | -|----------|-------------|-----------------|-----------------| -| **Constant → Sine** | Constant 1500ms, then Sine 1500ms | 0x00, then 0x01 | Effect 1 (ID=0x00): Upload→Envelope→Constant→START→[wait]→STOP
Effect 2 (ID=0x01): Upload→Envelope→Periodic→START→[wait]→STOP | -| **Sine → Triangle** | Sine 3000ms, then Triangle 3000ms | 0x00, then 0x01 | Effect 1 (ID=0x00): Upload→Envelope→Periodic→START→[wait]→STOP
Effect 2 (ID=0x01): Upload→Envelope→Periodic→START→[wait]→STOP | - -**Sequential Effect Notes:** -- Each effect gets full upload sequence with unique effect ID -- Effect IDs are hardware slot numbers in the range 0-15 (0x00-0x0F) -- Previous effect must be stopped before starting next -- Gap between effects depends on timing in test - -#### Overlapping Effects - -| Scenario | Description | Effect IDs Used | Packet Sequence | -|----------|-------------|-----------------|-----------------| -| **Sine + Triangle Overlap** | Sine starts, Triangle joins after 1s, both run for 2s | 0x00, 0x01 | Effect 1 (ID=0x00): Upload→Envelope→Periodic→START
[wait 1s]
Effect 2 (ID=0x01): Upload→Envelope→Periodic→START
[wait 2s]
Effect 1 (ID=0x00): STOP
Effect 2 (ID=0x01): STOP | - -**Overlapping Effect Notes:** -- T500RS supports up to 16 simultaneous effects (hardware capability) -- Effect IDs are hardware slot numbers (0x00-0x0F, i.e., 0-15) -- Effects are uploaded and started independently with different IDs -- Device mixes/sums the forces internally -- Driver uses sequential assignment (0x00, 0x01, 0x02, etc.) for simplicity - -#### Rapid Sequential Effects - -| Scenario | Description | Timing | -|----------|-------------|--------| -| **Short Rapid** | 3 effects × 200ms back-to-back | Constant→Sine→Spring, no gaps | -| **Short with Gaps** | 3 effects × 300ms with 100ms gaps | Sine→[100ms]→Constant→[100ms]→Damper | - -**Rapid Effect Notes:** -- Device handles rapid effect changes (200ms duration) -- No special packet sequence needed for rapid changes -- Standard upload→start→stop sequence for each effect +**Implementation Note:** The current driver implementation for inertia effects matches the behavior of the Windows driver, which uses: +- Effect type: 0x41 (same as damper/friction) +- Two 0x05 packets with subtype codes from 0x01 bytes 9-12 +- Right/left coefficients: Scaled from Linux 0-32767 range to device 0-10 scale +- Saturation: Dynamic right/left saturation (0-100 scale from Linux 0-65535 range) + +**Driver Behavior:** +The driver will send non-zero coefficients for inertia effects if they are provided by the Linux FFB subsystem. However, based on Windows captures, the device may work with zero coefficients and rely solely on saturation values for effect strength. + +**Parameter Details:** +- Same structure as damper effects +- Saturation: Dynamic right/left saturation (0-100 scale from Linux 0-65535 range) +- Coefficients: May be non-zero for fine-tuning inertia feel +- Windows driver typically uses saturation values around 100% for strong inertia effects + +### 9. CONDITIONAL EFFECTS - FRICTION + +**Status:** Limited capture data available. Uses same 0x05 packet structure as spring/damper. + +**Capture Examples:** +- Basic friction: `01 00 41 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 00 00 64 64` `05 38 00 00 00 00 00 00 00 64 64` +- Friction with asymmetric coefficients: `01 00 41 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 08 05 00 00 00 00 64 64` `05 38 00 00 00 00 00 00 00 64 64` + +**Packet Structure:** +- Main packet: `01 [effect_id] 41 40 [duration] [delay] 00 2a 00 38 00 00 00` + - effect_type = 0x41 (same as damper/inertia) + - codes: 0x002a (X-axis), 0x0038 (Y-axis) +- First 0x05 packet (X-axis): `05 2a [right_coeff] [left_coeff] [center] [deadband] [right_sat] [left_sat]` +- Second 0x05 packet (Y-axis): `05 38 [right_coeff] [left_coeff] [center] [deadband] [right_sat] [left_sat]` + +**Parameter Details:** +- Same structure as damper effects +- Saturation: Dynamic right/left saturation (0-100 scale from Linux 0-65535 range) +- May require non-zero coefficients for proper friction feel +- FFEdit captures suggest asymmetric coefficients (stronger in one direction) + +--- + +## COMMON PITFALLS AND IMPLEMENTATION TIPS + +### Effect Indexing +- **Hardware ID Allocation:** The driver intentionally avoids hardware index 0, which has quirky behavior (only valid for constant effects). Instead, it maps logical IDs 0-14 to hardware IDs 1-15. +- **Subtype Calculation:** For hardware effect ID `n`, use: + - `parameter_subtype = 0x000e + 0x001c * n` + - `envelope_subtype = 0x001c + 0x001c * n` + +### Envelope Limitations +- **Periodic/Constant Effects:** Non-zero envelope values cause EPROTO errors. Always send zero envelope parameters for these effect types. +- **Ramp Effects:** Only ramp effects support envelopes. Send actual envelope values for ramp effects. + +### Runtime Updates +- Only parameter-specific packets (0x03, 0x04, 0x05) can be updated at runtime. Duration and delay changes require re-uploading the entire effect. + +### Conditional Effects +- **Saturation:** Use dynamic saturation values from effect parameters instead of hardcoded values. The device supports 0-100 range for both right and left saturation. +- **Coefficients:** Coefficients are scaled to 0-10 range. Non-zero coefficients may provide finer control, but Windows driver typically sends zeros. + +### Direction Handling +- **Periodic Effects:** Direction affects the phase. Negative projections are handled by taking absolute value and adding 180 degrees to phase. + +--- + +## Subtype System and Effect Indexing + +The T500RS uses a unique subtype system for effect indexing. The last six bytes of the 0x01 main upload (bytes 9-14) carry two 16-bit "subtype" values that act as per-effect indices: + +- Bytes 9-10 -> `parameter_subtype` (for 0x03, 0x04, and first 0x05 packets) +- Bytes 11-12 -> `envelope_subtype` (for 0x02 and second 0x05 packets) +- Bytes 13-14 -> padding (always 0x0000 in captures) + +These subtype values are then copied into the "code" or "subtype" field of other packets so the device can associate parameter/envelope packets with a particular logical effect. + +### Subtype Calculation +For hardware effect ID **n** (1-15), the wheel uses a simple arithmetic progression: + +```c +parameter_subtype = 0x000e + 0x001c * n; +envelope_subtype = 0x001c + 0x001c * n; +``` + +### Observed Subtype Pairs +| Hardware ID (n) | parameter_subtype | envelope_subtype | +|-----------------|-------------------|------------------| +| 1 | 0x002a | 0x0038 | +| 2 | 0x0046 | 0x0054 | +| 3 | 0x0062 | 0x0070 | +| 4 | 0x007e | 0x008c | +| 5 | 0x009a | 0x00a8 | +| 6 | 0x00b6 | 0x00c4 | + +### Driver Implementation Notes +- **Effect ID Handling:** The driver uses hardware IDs 1-15 to avoid quirky behavior with hardware index 0 (only valid for constant effects). +- **Logical to Hardware ID Mapping:** `hw_id = logical_id + 1` (logical 0-14 -> hardware 1-15) +- **Subtype Usage in Packets:** + - 0x02 envelope packets: `subtype = envelope_subtype & 0xff` + - 0x03 constant packets: `code = parameter_subtype & 0xff` + - 0x04 periodic/ramp packets: `code = parameter_subtype & 0xff` + - 0x05 condition packets: First uses `parameter_subtype & 0xff`, second uses `envelope_subtype & 0xff` + +### Envelope Parameters +Envelope attack/fade length and level values live **only** in the 0x02 packets; bytes 9-14 of 0x01 are *references* to those blocks, not the envelope parameters. --- ## Parameter Encoding Reference ### Direction Encoding -- **SDL2 Format:** 0-35999 (0.01 degree units) -- **Device Format:** 16-bit little-endian in 0x01 packet -- **Conversion:** Direct copy, no scaling +- **Linux FFB Format:** 0-65535 (0 = forward, 16384 = right, 32768 = back, 49152 = left) +- **Device Format:** 16-bit little-endian (0-35999 in 0.01 degree units) +- **Conversion:** `device_dir = (os_ffb_dir * 36000) / 65536` - **Examples:** - - 0° = 0x0000 - - 90° = 0x2328 (9000 decimal) - - 180° = 0x4650 (18000 decimal) - - 270° = 0x6978 (27000 decimal) + - 0degrees = 0x0000 + - 90degrees = 0x2328 (9000 decimal) + - 180degrees = 0x4650 (18000 decimal) + - 270degrees = 0x6978 (27000 decimal) ### Duration Encoding -- **SDL2 Format:** Milliseconds +- **Linux FFB Format:** Milliseconds - **Device Format:** 16-bit little-endian in 0x01 packet -- **Conversion:** Direct copy +- **Conversion:** Direct copy (0xffff for infinite duration) - **Examples:** - 500ms = 0x01f4 - 1000ms = 0x03e8 @@ -441,43 +502,39 @@ Offset | Size | Field | Description - 5000ms = 0x1388 ### Force Level Encoding (Constant) -- **SDL2 Format:** 0-65535 (unsigned) +- **Linux FFB Format:** -32767 to +32767 (signed) - **Device Format:** -127 to +127 (signed 8-bit) -- **Conversion:** Scale and sign -- **Formula:** `device_level = (sdl_level * 255 / 65535) - 127` +- **Conversion:** `device_level = (os_ffb_level * 127LL) / 32767` - **Examples:** - - SDL 0 → Device 0 - - SDL 8000 → Device 3 - - SDL 24000 → Device 9 - - SDL 48000 → Device -7 (0xf9) - - SDL 65535 → Device 127 (max positive) + - Linux -32767 -> Device -127 (max negative) + - Linux 0 -> Device 0 (neutral) + - Linux 16384 -> Device 63 (medium positive) + - Linux 32767 -> Device 127 (max positive) ### Magnitude Encoding (Periodic) -- **SDL2 Format:** 0-32767 (unsigned) +- **Linux FFB Format:** 0-32767 (unsigned) - **Device Format:** 0-127 (unsigned 8-bit) -- **Conversion:** Scale down -- **Formula:** `device_mag = sdl_mag * 127 / 32767` +- **Conversion:** `device_mag = (os_ffb_mag * 127LL) / 32767` - **Examples:** - - SDL 0 → Device 0 - - SDL 8000 → Device 6 - - SDL 24000 → Device 9 - - SDL 32767 → Device 127 + - Linux 0 -> Device 0 + - Linux 8000 -> Device 6 + - Linux 24000 -> Device 9 + - Linux 32767 -> Device 127 ### Phase Encoding (Periodic) -- **SDL2 Format:** 0-35999 (0.01 degree units, 0-359.99°) -- **Device Format:** 0-255 (256 steps for 360°) -- **Conversion:** Scale to 256 steps -- **Formula:** `device_phase = (sdl_phase * 256 / 36000) & 0xFF` +- **Linux FFB Format:** 0-35999 (0.01 degree units, 0-359.99degrees) +- **Device Format:** 0-255 (256 steps for 360degrees) +- **Conversion:** `device_phase = (os_ffb_phase * 256) / 36000` - **Examples:** - - 0° (0) → 0x00 - - 90° (9000) → 0x40 (64) - - 180° (18000) → 0x80 (128) - - 270° (27000) → 0xC0 (192) + - 0degrees (0) -> 0x00 + - 90degrees (9000) -> 0x40 (64) + - 180degrees (18000) -> 0x80 (128) + - 270degrees (27000) -> 0xC0 (192) ### Period Encoding (Periodic) -- **SDL2 Format:** Milliseconds +- **Linux FFB Format:** Milliseconds - **Device Format:** 16-bit little-endian in 0x04 packet -- **Conversion:** Direct copy (keep in milliseconds, NOT Hz×100!) +- **Conversion:** Direct copy (keep in milliseconds, NOT Hz*100!) - **Examples:** - 10ms = 0x000a - 50ms = 0x0032 @@ -485,90 +542,23 @@ Offset | Size | Field | Description - 1000ms = 0x03e8 ### Envelope Level Encoding -- **SDL2 Format:** 0-32767 (unsigned) +- **Linux FFB Format:** 0-32767 (unsigned) - **Device Format:** 0-255 (unsigned 8-bit) -- **Conversion:** Scale down -- **Formula:** `device_env = sdl_env * 255 / 32767` +- **Conversion:** `device_env = (os_ffb_env * 255LL) / 32767` - **Examples:** - - SDL 0 → Device 0 - - SDL 8000 → Device 6 - - SDL 16000 → Device 12 - - SDL 24000 → Device 18 - - SDL 32767 → Device 255 - ---- - -## FFEdit Capture Analysis (December 2025) - -New captures from FFEdit (Force Feedback Editor) tool provide comprehensive protocol understanding. - -### Capture Files Analyzed - -**Initial Captures:** -- `inertia_Axis0_10000_offset_-7439_axis1_10000_offset_0.pcapng` -- `square_medium.pcapng`, `square_max.pcapng` -- `Ramp.pcapng` - -**Systematic Conditional Effect Captures (Dec 11, 2025):** -- **Damper:** 8 captures with varying coefficients, offsets, deadbands -- **Friction:** 5 captures with varying positive/negative coefficients -- **Inertia:** 8 captures with varying coefficients and offsets - -### Key Findings - -#### 1. Square Wave Effect Type (0x20) ✅ IMPLEMENTED -FFEdit confirms square wave uses effect type **0x20**: -- Example: `01 04 20 40 1d 10 00 ff ff 42 01 50 01 00 00` -- Uses same 0x04 packet structure as other periodic effects - -#### 2. Corrected 0x05 Packet Structure -Previous understanding was incorrect. FFEdit analysis reveals: -- **Coefficients are u8 (not u16!)** - scaled 0-10 for 0-10000 FFEdit range -- **Center/offset is s16 LE** at bytes 5-6 (not single byte at position 8) -- **Deadband is u16 LE** at bytes 7-8 - -**Verified Examples:** -| FFEdit Parameters | 0x05 Packet | Decoded | -|-------------------|-------------|---------| -| friction P=10000,N=10000 | `050e000a0a000000006464` | coeff=10,10 | -| friction P=7500,N=5000 | `050e000805000000006464` | coeff=8,5 (rounded) | -| damper PC=6000,NC=4000,O=5000 | `0546000604fa00000064 64` | coeff=6,4, center=250 | -| damper O=-7439,D=4500 | `05460a0a8cfec2016464` | center=-372, deadband=450 | -| inertia P=6000,N=4000,O=-4000 | `052a00060438ff00006464` | coeff=6,4, center=-200 | - -#### 3. Coefficient Scaling Formula -``` -device_coeff = ffb_coeff / 1000 (for 0-10000 range) → 0-10 u8 -device_coeff = ffb_coeff / 3277 (for 0-32767 Linux range) → 0-10 u8 -``` - -#### 4. Center/Offset Scaling Formula -``` -device_center = ffb_center / 20 (for ±10000 FFEdit range) → s16 LE -device_center = ffb_center / 65 (for ±32767 Linux range) → approx ±500 -``` - -#### 5. Deadband Scaling Formula -``` -device_deadband = ffb_deadband / 10 (for 0-10000 range) → u16 LE -device_deadband = ffb_deadband / 65 (for 0-65535 Linux range) → 0-1008 -``` - -#### 6. Windows vs FFEdit Behavior -- **Windows driver:** Sends zeros for coefficients/center/deadband, relies on saturation only -- **FFEdit:** Sends actual coefficient values; device accepts both approaches -- **Conclusion:** Coefficients are optional for basic functionality but enable finer control - -#### 7. Ramp Effect Phase Field (✅ IMPLEMENTED) -Ramp captures confirm phase encodes ramp direction: -- `049a0000007f0000` - phase 0x7f (127) = positive/up ramp (start < end) -- `049a000c00000000` - phase 0x00 = negative/down ramp (start > end) - -**Implementation:** -```c -/* Phase encodes ramp direction: - * - Positive ramp (start < end): phase = 0x7f - * - Negative ramp (start > end): phase = 0x00 - */ -phase = (start_level < end_level) ? 0x7f : 0x00; -``` + - Linux 0 -> Device 0 + - Linux 8000 -> Device 6 + - Linux 16000 -> Device 12 + - Linux 24000 -> Device 18 + - Linux 32767 -> Device 255 + +### Conditional Effect Parameter Encoding +- **Coefficients (Right/Left):** Linux 0-32767 -> Device 0-10 (u8) + - Formula: `device_coeff = (os_ffb_coeff * 10) / 32767` +- **Center Offset:** Linux -32767 to +32767 -> Device s16 LE (approx +-500) + - Formula: `device_center = (os_ffb_center / 65)` +- **Deadband:** Linux 0-65535 -> Device u16 LE (0-1008) + - Formula: `device_deadband = (os_ffb_deadband / 65)` +- **Saturation (Right/Left):** Linux 0-65535 -> Device 0-100 (u8) + - Formula: `device_sat = (os_ffb_sat * 100) / 65535` + \ No newline at end of file diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index fe25bff9..3b598abc 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -143,8 +143,8 @@ static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { * - effect_id: 16-bit LE hardware effect slot (0..15 for now) * - duration_ms: duration in milliseconds * - delay_ms: delay before effect starts - * - code1: parameter subtype (used by 0x03/0x04/0x05) - * - code2: envelope subtype (used by 0x02), or second conditional subtype + * - param_sub: parameter subtype (used by 0x03/0x04/0x05) + * - envelope_sub: envelope subtype (used by 0x02), or second conditional subtype * * Per Windows captures, effect_type values are: * - 0x00 = Constant @@ -160,7 +160,7 @@ static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { */ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, u8 effect_type, u16 duration_ms, u16 delay_ms, - u16 code1, u16 code2) { + u16 param_sub, u16 envelope_sub) { /* Validate effect_id */ if (effect_id >= T500RS_MAX_HW_EFFECTS) { pr_err("t500rs: Invalid effect_id %u (max %d)\n", @@ -185,9 +185,9 @@ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, } /* Validate packet codes are non-zero (0x0000 likely indicates bug) */ - if (code1 == 0 || code2 == 0) { - pr_warn("t500rs: Suspicious packet codes: code1=0x%04x code2=0x%04x\n", - code1, code2); + if (param_sub == 0 || envelope_sub == 0) { + pr_warn("t500rs: Suspicious packet codes: param_sub=0x%04x envelope_sub=0x%04x\n", + param_sub, envelope_sub); } memset(p, 0, sizeof(*p)); @@ -198,8 +198,8 @@ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, p->duration_ms = cpu_to_le16(duration_ms); p->delay_ms = cpu_to_le16(delay_ms); p->reserved1 = 0; - p->packet_code_1 = cpu_to_le16(code1); - p->packet_code_2 = cpu_to_le16(code2); + p->packet_code_1 = cpu_to_le16(param_sub); + p->packet_code_2 = cpu_to_le16(envelope_sub); p->reserved2 = 0; return 0; @@ -381,7 +381,7 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, */ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, u8 code, s16 right_coeff, s16 left_coeff, - u8 saturation, u16 deadband, s16 center) { + u8 right_sat, u8 left_sat, u16 deadband, s16 center) { memset(p, 0, sizeof(*p)); p->id = T500RS_PKT_CONDITIONAL; p->code = code; @@ -397,8 +397,8 @@ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, /* Scale deadband from Linux 0-65535 range to device u16 LE (0-1008) */ p->deadband = cpu_to_le16((u16)(deadband / 65)); - p->right_sat = saturation; - p->left_sat = saturation; + p->right_sat = right_sat; + p->left_sat = left_sat; } /* @@ -623,59 +623,31 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, } case T500RS_SEQ_CONDITION_X: { - u8 saturation = 0; const struct ff_condition_effect *cond = &effect->u.condition[0]; - switch (effect->type) { - case FF_SPRING: - saturation = T500RS_SAT_SPRING; - break; - case FF_DAMPER: - saturation = T500RS_SAT_DAMPER; - break; - case FF_FRICTION: - saturation = T500RS_SAT_FRICTION; - break; - case FF_INERTIA: - saturation = T500RS_SAT_INERTIA; - break; - default: - saturation = T500RS_SAT_DAMPER; - break; - } + /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) */ + u8 right_sat = (cond->right_saturation * 100) / 65535; + u8 left_sat = (cond->left_saturation * 100) / 65535; + struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; t500rs_build_r05_condition(p, (u8)(param_sub), cond->right_coeff, - cond->left_coeff, saturation, cond->deadband, + cond->left_coeff, right_sat, left_sat, cond->deadband, cond->center); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); break; } case T500RS_SEQ_CONDITION_Y: { - u8 saturation = 0; /* Y-axis: use condition[1] if available, else zeros */ const struct ff_condition_effect *cond = &effect->u.condition[1]; - switch (effect->type) { - case FF_SPRING: - saturation = T500RS_SAT_SPRING; - break; - case FF_DAMPER: - saturation = T500RS_SAT_DAMPER; - break; - case FF_FRICTION: - saturation = T500RS_SAT_FRICTION; - break; - case FF_INERTIA: - saturation = T500RS_SAT_INERTIA; - break; - default: - saturation = T500RS_SAT_DAMPER; - break; - } + /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) */ + u8 right_sat = (cond->right_saturation * 100) / 65535; + u8 left_sat = (cond->left_saturation * 100) / 65535; + struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), cond->right_coeff, - cond->left_coeff, saturation, cond->deadband, + cond->left_coeff, right_sat, left_sat, cond->deadband, cond->center); ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); break; @@ -888,7 +860,7 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, * Upload spring/damper/friction/inertia effect. * * Per Windows captures (T500RS_FFBEFFECTS.md): - * - 0x01 packet: direction=0x4000, code1=0x002a, code2=0x0038 + * - 0x01 packet: direction=0x4000, param_sub=0x002a, envelope_sub=0x0038 * - Two 0x05 packets: X-axis (code 0x2a) and Y-axis (code 0x38) * - Saturation values 0x54 (84) for spring, 0x64 (100) for damper/friction */ @@ -949,7 +921,7 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, * * Per Windows captures (T500RS_FFBEFFECTS.md): * - Waveform type is NOT encoded in USB packets; determined by Linux FFB subsystem - * - 0x01 packet: direction, duration, delay, code1=0x000e, code2=0x001c + * - 0x01 packet: direction, duration, delay, param_sub=0x000e, envelope_sub=0x001c * - 0x02 packet: envelope with subtype 0x1c * - 0x04 packet: code=0x2a (NOT 0x0e!), magnitude, offset, phase, period_ms * - Period is in MILLISECONDS (no Hz*100 conversion) @@ -1242,9 +1214,17 @@ static int t500rs_stop_effect(void *data, return t500rs_send_stop(t500rs, (u8)hw_id); } -/* Update effect - send parameter updates without re-uploading */ +/* + * Update effect - send parameter updates without re-uploading + * + * Note: Only parameter-specific packets (0x03, 0x04, 0x05) are updated. + * Duration and delay changes (from 0x01 packet) require full re-upload. + * This limitation is acceptable as duration/delay modifications are rare + * in gaming applications and the hardware may not support runtime updates + * of these fields. + */ static int t500rs_update_effect(void *data, - const struct tmff2_effect_state *state) { + 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; @@ -1277,6 +1257,14 @@ static int t500rs_update_effect(void *data, } case FF_PERIODIC: { + /* Skip update if parameters unchanged */ + if (effect->u.periodic.magnitude == old->u.periodic.magnitude && + effect->u.periodic.offset == old->u.periodic.offset && + effect->u.periodic.phase == old->u.periodic.phase && + effect->u.periodic.period == old->u.periodic.period && + effect->direction == old->direction) + return 0; + /* Apply direction projection to magnitude and adjust phase if needed */ u16 phase_raw = effect->u.periodic.phase; u8 mag = t500rs_scale_periodic_with_direction(effect->u.periodic.magnitude, @@ -1300,6 +1288,12 @@ static int t500rs_update_effect(void *data, } case FF_RAMP: { + /* Skip update if parameters unchanged */ + if (effect->u.ramp.start_level == old->u.ramp.start_level && + effect->u.ramp.end_level == old->u.ramp.end_level && + effect->replay.length == old->replay.length) + return 0; + u16 duration_ms = effect->replay.length; if (duration_ms == 0) { hid_err(t500rs->hdev, "Ramp effect duration cannot be zero\n"); @@ -1337,35 +1331,19 @@ static int t500rs_update_effect(void *data, return 0; t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - /* Calculate saturation value based on effect type */ - u8 saturation; - switch (effect->type) { - case FF_SPRING: - saturation = T500RS_SAT_SPRING; - break; - case FF_DAMPER: - saturation = T500RS_SAT_DAMPER; - break; - case FF_FRICTION: - saturation = T500RS_SAT_FRICTION; - break; - case FF_INERTIA: - saturation = T500RS_SAT_INERTIA; - break; - default: - saturation = T500RS_SAT_DAMPER; /* Default to damper level */ - break; - } + /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) */ + u8 right_sat = (cond->right_saturation * 100) / 65535; + u8 left_sat = (cond->left_saturation * 100) / 65535; struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), cond->right_coeff, - cond->left_coeff, saturation, cond->deadband, + cond->left_coeff, right_sat, left_sat, cond->deadband, cond->center); return t500rs_send_hid(t500rs, buf, sizeof(*p)); } default: - return 0; + return -EOPNOTSUPP; } } @@ -1382,14 +1360,12 @@ 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. */ + * 100%% at startup. That leaves a permanent strong + * centering force which masks/overpowers other forces. To avoid this, message the + * requests for the user to revert the gain value to expected value. */ if (autocenter_percent >= 100) { hid_warn(t500rs->hdev, - "Ignoring 100%% autocenter request (Wine/LFS compatibility)"); - return 0; + "Game might have set autocenter to 100%%, you might want to set it back to expected value using oversteer (or keep oversteer open) or system gain."); } buf = t500rs->send_buffer; @@ -1397,19 +1373,25 @@ static int t500rs_set_autocenter(void *data, u16 autocenter) { return -ENOMEM; /* Enable autocenter: Report 0x40 0x04 0x01 */ - buf[0] = 0x40; - buf[1] = 0x04; - buf[2] = 0x01; /* Enable */ - buf[3] = 0x00; + { + struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x04; + config->data1 = 0x01; /* Enable */ + config->data2 = 0x00; + } ret = t500rs_send_hid(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; + { + struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x03; + config->data1 = autocenter_percent; /* 0-100 percentage */ + config->data2 = 0x00; + } ret = t500rs_send_hid(t500rs, buf, 4); if (ret) return ret; @@ -1447,10 +1429,13 @@ static int t500rs_set_range(void *data, u16 range) { range_value = range * 60; /* 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 */ + { + struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x11; + config->data1 = range_value & 0xFF; /* Low byte first (little-endian) */ + config->data2 = (range_value >> 8) & 0xFF; /* High byte second */ + } ret = t500rs_send_hid(t500rs, buf, 4); if (ret) { @@ -1555,25 +1540,26 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) { /* 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; - init_buf[2] = 0x42; - init_buf[3] = 0x7b; + { + struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)init_buf; + config->id = 0x40; + config->subcmd = 0x11; + config->data1 = 0x42; + config->data2 = 0x7b; + } ret = t500rs_send_hid(t500rs, init_buf, 4); if (ret) hid_warn(t500rs->hdev, "Init command 2 (0x40 enable) failed: %d\n", ret); /* 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. - * Keep explicit zeros even though memset() clears them, to document the - * wire image. - */ - init_buf[2] = 0x00; - init_buf[3] = 0x00; + { + struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)init_buf; + config->id = 0x40; + config->subcmd = 0x04; + // Keep explicit zeros even though memset() clears them. + config->data1 = 0x00; + config->data2 = 0x00; + } ret = t500rs_send_hid(t500rs, init_buf, 4); if (ret) hid_warn(t500rs->hdev, "Init command 3 (0x40 config) failed: %d\n", ret); diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index c832ab48..0e4a4980 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -50,12 +50,6 @@ #define T500RS_EFFECT_FRICTION 0x41 #define T500RS_EFFECT_INERTIA 0x41 -/* Saturation values for conditional effects */ -#define T500RS_SAT_SPRING 84 -#define T500RS_SAT_DAMPER 100 -#define T500RS_SAT_FRICTION 100 -#define T500RS_SAT_INERTIA 100 - /* Hardware limits */ /* Advertise 15 logical effect slots to the framework (logical IDs 0..14). * The device/hardware ID space remains 0..15 (16 entries), but we avoid using @@ -223,4 +217,12 @@ struct t500rs_pkt_r02_envelope { u8 reserved; /* 0x00 */ } __packed; +/* 0x40 - Configuration packet (4 bytes) */ +struct t500rs_pkt_r40_config { + u8 id; /* 0x40 */ + u8 subcmd; /* subcommand */ + u8 data1; /* first data byte */ + u8 data2; /* second data byte */ +} __packed; + #endif /* __HID_TMT500RS_H */ From 3e7bfd080be9e7712b4f3ab39b0ed59585bf013d Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Fri, 16 Jan 2026 14:47:18 +0100 Subject: [PATCH 13/15] clang-format pass --- src/tmt500rs/hid-tmt500rs.c | 2465 ++++++++++++++++++----------------- src/tmt500rs/hid-tmt500rs.h | 112 +- 2 files changed, 1355 insertions(+), 1222 deletions(-) diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index 3b598abc..5d5ef8b9 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -7,46 +7,47 @@ * Copyright (c) 2025 Casimir Bonnet */ -#include "../hid-tmff2.h" #include "hid-tmt500rs.h" +#include "../hid-tmff2.h" #include #include /* Packet sequence templates for each effect type */ static const enum t500rs_seq_packet t500rs_seq_constant[] = { - T500RS_SEQ_ENVELOPE, - T500RS_SEQ_CONSTANT, - T500RS_SEQ_MAIN, + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_CONSTANT, + T500RS_SEQ_MAIN, }; static const enum t500rs_seq_packet t500rs_seq_periodic[] = { - T500RS_SEQ_STOP, T500RS_SEQ_SYNC_42_05, T500RS_SEQ_SYNC_42_04, - T500RS_SEQ_ENVELOPE, T500RS_SEQ_PERIODIC_RAMP, T500RS_SEQ_MAIN, + T500RS_SEQ_STOP, T500RS_SEQ_SYNC_42_05, T500RS_SEQ_SYNC_42_04, + T500RS_SEQ_ENVELOPE, T500RS_SEQ_PERIODIC_RAMP, T500RS_SEQ_MAIN, }; static const enum t500rs_seq_packet t500rs_seq_ramp[] = { - T500RS_SEQ_STOP, - T500RS_SEQ_ENVELOPE, - T500RS_SEQ_PERIODIC_RAMP, - T500RS_SEQ_MAIN, + T500RS_SEQ_STOP, + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_PERIODIC_RAMP, + T500RS_SEQ_MAIN, }; static const enum t500rs_seq_packet t500rs_seq_condition[] = { - T500RS_SEQ_CONDITION_X, - T500RS_SEQ_CONDITION_Y, - T500RS_SEQ_MAIN, + T500RS_SEQ_CONDITION_X, + T500RS_SEQ_CONDITION_Y, + T500RS_SEQ_MAIN, }; /* Scale constant level (-32767..32767) to signed 8-bit (-127..127) */ -static inline s8 t500rs_scale_const_level_s8(int level) { - /* Input validation and clamping */ - if (level > 32767) - level = 32767; - if (level < -32767) - level = -32767; - - /* Use 32-bit arithmetic to prevent overflow */ - return (s8)((level * 127LL) / 32767); +static inline s8 t500rs_scale_const_level_s8(int level) +{ + /* Input validation and clamping */ + if (level > 32767) + level = 32767; + if (level < -32767) + level = -32767; + + /* Use 32-bit arithmetic to prevent overflow */ + return (s8)((level * 127LL) / 32767); } /* Apply effect direction to a constant level and convert to s8. @@ -54,24 +55,26 @@ static inline s8 t500rs_scale_const_level_s8(int level) { * 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; +static inline s8 t500rs_scale_const_with_direction(int level, u16 direction) +{ + int projected; - projected = (level * fixp_sin16(direction * 360 / 0x10000)) / 0x7fff; + projected = (level * fixp_sin16(direction * 360 / 0x10000)) / 0x7fff; - return t500rs_scale_const_level_s8(projected); + 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) { - /* Input validation and clamping */ - if (magnitude < 0) - magnitude = -magnitude; - if (magnitude > 32767) - magnitude = 32767; - - /* Use long long arithmetic to prevent overflow */ - return (u8)((magnitude * 127LL) / 32767); +static inline u8 t500rs_scale_mag_u7(int magnitude) +{ + /* Input validation and clamping */ + if (magnitude < 0) + magnitude = -magnitude; + if (magnitude > 32767) + magnitude = 32767; + + /* Use long long arithmetic to prevent overflow */ + return (u8)((magnitude * 127LL) / 32767); } /* @@ -85,11 +88,12 @@ static inline u8 t500rs_scale_mag_u7(int magnitude) { * Trade-off: 15 effect slots instead of 16, but simpler code and * no risk of index 0 misuse. Most games don't need 16 simultaneous effects. */ -static inline unsigned int t500rs_logical_to_hw_id(unsigned int logical_id) { - /* Clamp to valid range: logical 0-14 -> hw 1-15 */ - if (logical_id >= T500RS_MAX_EFFECTS) - logical_id = T500RS_MAX_EFFECTS - 1; - return logical_id + 1; +static inline unsigned int t500rs_logical_to_hw_id(unsigned int logical_id) +{ + /* Clamp to valid range: logical 0-14 -> hw 1-15 */ + if (logical_id >= T500RS_MAX_EFFECTS) + logical_id = T500RS_MAX_EFFECTS - 1; + return logical_id + 1; } /* Map hardware effect index to parameter/envelope subtypes as per protocol: @@ -99,14 +103,15 @@ static inline unsigned int t500rs_logical_to_hw_id(unsigned int logical_id) { * idx is the hardware effect ID (1..15 with simplified architecture). */ static inline void t500rs_index_to_subtypes(unsigned int idx, u16 *param_sub, - u16 *env_sub) { - /* Validate inputs */ - if (idx >= T500RS_MAX_HW_EFFECTS) { - idx = T500RS_MAX_HW_EFFECTS - 1; /* Clamp to valid range */ - } - - *param_sub = 0x000e + (0x001c * idx); - *env_sub = 0x001c + (0x001c * idx); + u16 *env_sub) +{ + /* Validate inputs */ + if (idx >= T500RS_MAX_HW_EFFECTS) { + idx = T500RS_MAX_HW_EFFECTS - 1; /* Clamp to valid range */ + } + + *param_sub = 0x000e + (0x001c * idx); + *env_sub = 0x001c + (0x001c * idx); } /* Debug logging helper: pass struct t500rs_device_entry * explicitly */ @@ -114,26 +119,27 @@ static inline void t500rs_index_to_subtypes(unsigned int idx, u16 *param_sub, /* T500RS device data */ struct t500rs_device_entry { - struct hid_device *hdev; - struct input_dev *input_dev; + struct hid_device *hdev; + struct input_dev *input_dev; - u8 *send_buffer; - size_t buffer_length; + u8 *send_buffer; + size_t buffer_length; }; /* * Scale direction from Linux ff_effect format to T500RS protocol format. * * Linux ff_effect.direction: 0-65535 (0 = forward, 16384 = right, 32768 = back, - * 49152 = left) T500RS protocol: 0-35999 in 0.01 degree units (0 = 0 degrees, 9000 = - * 90 degrees, 18000 = 180 degrees, etc.) + * 49152 = left) T500RS protocol: 0-35999 in 0.01 degree units (0 = 0 degrees, + * 9000 = 90 degrees, 18000 = 180 degrees, etc.) * * Conversion: device_dir = (os_ffb_dir * 36000) / 65536 * This maps 0-65535 -> 0-35999 (approximately, since 65535 -> 35999.45) */ -static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { - /* Use 32-bit arithmetic to avoid overflow */ - return (u16)(((u32)os_ffb_dir * 36000) / 65536); +static inline u16 t500rs_scale_direction(u16 os_ffb_dir) +{ + /* Use 32-bit arithmetic to avoid overflow */ + return (u16)(((u32)os_ffb_dir * 36000) / 65536); } /* @@ -144,7 +150,8 @@ static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { * - duration_ms: duration in milliseconds * - delay_ms: delay before effect starts * - param_sub: parameter subtype (used by 0x03/0x04/0x05) - * - envelope_sub: envelope subtype (used by 0x02), or second conditional subtype + * - envelope_sub: envelope subtype (used by 0x02), or second conditional + * subtype * * Per Windows captures, effect_type values are: * - 0x00 = Constant @@ -159,50 +166,52 @@ static inline u16 t500rs_scale_direction(u16 os_ffb_dir) { * not in this 0x01 packet. */ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, - u8 effect_type, u16 duration_ms, u16 delay_ms, - u16 param_sub, u16 envelope_sub) { - /* Validate effect_id */ - if (effect_id >= T500RS_MAX_HW_EFFECTS) { - pr_err("t500rs: Invalid effect_id %u (max %d)\n", - effect_id, T500RS_MAX_HW_EFFECTS - 1); - return -EINVAL; - } - - /* Validate effect_type against known constants */ - switch (effect_type) { - case T500RS_EFFECT_CONSTANT: - case T500RS_EFFECT_SQUARE: - case T500RS_EFFECT_SINE: - case T500RS_EFFECT_TRIANGLE: - case T500RS_EFFECT_SAW_UP: - case T500RS_EFFECT_SAW_DOWN: - case T500RS_EFFECT_SPRING: - case T500RS_EFFECT_DAMPER: /* Note: DAMPER, FRICTION, INERTIA all use 0x41 */ - break; - default: - pr_err("t500rs: Unknown effect_type 0x%02x\n", effect_type); - return -EINVAL; - } - - /* Validate packet codes are non-zero (0x0000 likely indicates bug) */ - if (param_sub == 0 || envelope_sub == 0) { - pr_warn("t500rs: Suspicious packet codes: param_sub=0x%04x envelope_sub=0x%04x\n", - param_sub, envelope_sub); - } - - memset(p, 0, sizeof(*p)); - p->id = T500RS_PKT_MAIN; - p->effect_id = effect_id; - p->effect_type = effect_type; - p->control = T500RS_CONTROL_DEFAULT; - p->duration_ms = cpu_to_le16(duration_ms); - p->delay_ms = cpu_to_le16(delay_ms); - p->reserved1 = 0; - p->packet_code_1 = cpu_to_le16(param_sub); - p->packet_code_2 = cpu_to_le16(envelope_sub); - p->reserved2 = 0; - - return 0; + u8 effect_type, u16 duration_ms, u16 delay_ms, + u16 param_sub, u16 envelope_sub) +{ + /* Validate effect_id */ + if (effect_id >= T500RS_MAX_HW_EFFECTS) { + pr_err("t500rs: Invalid effect_id %u (max %d)\n", effect_id, + T500RS_MAX_HW_EFFECTS - 1); + return -EINVAL; + } + + /* Validate effect_type against known constants */ + switch (effect_type) { + case T500RS_EFFECT_CONSTANT: + case T500RS_EFFECT_SQUARE: + case T500RS_EFFECT_SINE: + case T500RS_EFFECT_TRIANGLE: + case T500RS_EFFECT_SAW_UP: + case T500RS_EFFECT_SAW_DOWN: + case T500RS_EFFECT_SPRING: + case T500RS_EFFECT_DAMPER: /* Note: DAMPER, FRICTION, INERTIA all use 0x41 */ + break; + default: + pr_err("t500rs: Unknown effect_type 0x%02x\n", effect_type); + return -EINVAL; + } + + /* Validate packet codes are non-zero (0x0000 likely indicates bug) */ + if (param_sub == 0 || envelope_sub == 0) { + pr_warn("t500rs: Suspicious packet codes: param_sub=0x%04x " + "envelope_sub=0x%04x\n", + param_sub, envelope_sub); + } + + memset(p, 0, sizeof(*p)); + p->id = T500RS_PKT_MAIN; + p->effect_id = effect_id; + p->effect_type = effect_type; + p->control = T500RS_CONTROL_DEFAULT; + p->duration_ms = cpu_to_le16(duration_ms); + p->delay_ms = cpu_to_le16(delay_ms); + p->reserved1 = 0; + p->packet_code_1 = cpu_to_le16(param_sub); + p->packet_code_2 = cpu_to_le16(envelope_sub); + p->reserved2 = 0; + + return 0; } /* @@ -224,20 +233,21 @@ static int t500rs_build_r01_main(struct t500rs_pkt_r01_main *p, u8 effect_id, * period_ms = direct copy (no frequency conversion) */ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, - u8 code, u8 magnitude, s8 offset, - u8 phase, u16 period_ms) { - /* Byte order per Windows USB captures (example: 04 2a 00 06 00 3f 0a 00): + u8 code, u8 magnitude, s8 offset, + u8 phase, u16 period_ms) +{ + /* Byte order per Windows USB captures (example: 04 2a 00 06 00 3f 0a 00): * b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset, * b5=phase, b6-b7=period */ - memset(p, 0, sizeof(*p)); - p->id = T500RS_PKT_PERIODIC; /* b0 */ - p->code = code; /* b1 */ - p->reserved1 = 0; /* b2: always 0x00 */ - p->magnitude = magnitude; /* b3 */ - p->offset = (u8)offset; /* b4 */ - p->phase = phase; /* b5 */ - p->period_ms = cpu_to_le16(period_ms); /* b6-b7 */ + memset(p, 0, sizeof(*p)); + p->id = T500RS_PKT_PERIODIC; /* b0 */ + p->code = code; /* b1 */ + p->reserved1 = 0; /* b2: always 0x00 */ + p->magnitude = magnitude; /* b3 */ + p->offset = (u8)offset; /* b4 */ + p->phase = phase; /* b5 */ + p->period_ms = cpu_to_le16(period_ms); /* b6-b7 */ } /* @@ -251,39 +261,42 @@ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, * 2. Add 180 degrees to the phase to maintain correct force direction * * Linux FFB magnitude: 0..32767 (unsigned) - * Linux FFB direction: 0..65535 (0=forward, 16384=right, 32768=back, 49152=left) - * Linux FFB phase: 0..65535 (0..360 degrees in 1/65536 units) + * Linux FFB direction: 0..65535 (0=forward, 16384=right, 32768=back, + * 49152=left) Linux FFB phase: 0..65535 (0..360 degrees in 1/65536 units) * Device magnitude: 0..127 * * @param os_ffb_mag: Original magnitude from Linux FFB (0..32767) * @param direction: Effect direction from Linux FFB (0..65535) - * @param phase_ptr: Pointer to phase value; will be adjusted if projection is negative + * @param phase_ptr: Pointer to phase value; will be adjusted if projection is + * negative * @return: Scaled magnitude (0..127) */ static inline u8 t500rs_scale_periodic_with_direction(int os_ffb_mag, - u16 direction, - u16 *phase_ptr) { - int projected; + u16 direction, + u16 *phase_ptr) +{ + int projected; - /* Project magnitude based on direction (same formula as T300RS) */ - projected = (os_ffb_mag * fixp_sin16(direction * 360 / 0x10000)) / 0x7fff; + /* Project magnitude based on direction (same formula as T300RS) */ + projected = + (os_ffb_mag * fixp_sin16(direction * 360 / 0x10000)) / 0x7fff; - if (projected < 0) { - /* Wheel handles positive magnitudes only */ - projected = -projected; + if (projected < 0) { + /* Wheel handles positive magnitudes only */ + projected = -projected; - /* Add 180 degrees to phase to maintain correct force direction. + /* Add 180 degrees to phase to maintain correct force direction. * Phase is in 0..65535 range (Linux FFB), 180 degrees = 0x8000 */ - if (phase_ptr) - *phase_ptr = (*phase_ptr + 0x8000) % 0x10000; - } + if (phase_ptr) + *phase_ptr = (*phase_ptr + 0x8000) % 0x10000; + } - /* Clamp to valid range */ - if (projected > 32767) - projected = 32767; + /* Clamp to valid range */ + if (projected > 32767) + projected = 32767; - /* Scale to device range: 0..32767 -> 0..127 */ - return (u8)((projected * 127LL) / 32767); + /* Scale to device range: 0..32767 -> 0..127 */ + return (u8)((projected * 127LL) / 32767); } /* @@ -291,11 +304,12 @@ static inline u8 t500rs_scale_periodic_with_direction(int os_ffb_mag, * Linux FFB: 0..35999 (0.01 degree units, 0-359.99 degrees) * Device: 0..255 (256 steps for 360 degrees) */ -static inline u8 t500rs_scale_periodic_phase(u16 os_ffb_phase) { - /* Clamp to valid range just in case */ - if (os_ffb_phase > 35999) - os_ffb_phase = 35999; - return (u8)((os_ffb_phase * 256) / 36000); +static inline u8 t500rs_scale_periodic_phase(u16 os_ffb_phase) +{ + /* Clamp to valid range just in case */ + if (os_ffb_phase > 35999) + os_ffb_phase = 35999; + return (u8)((os_ffb_phase * 256) / 36000); } /* @@ -304,8 +318,9 @@ static inline u8 t500rs_scale_periodic_phase(u16 os_ffb_phase) { * Device: signed, stored as s8 (-128..127) * Note: exact mapping TBD based on testing; using simple /256 for now. */ -static inline s8 t500rs_scale_periodic_offset(s16 os_ffb_offset) { - return (s8)(os_ffb_offset / 256); +static inline s8 t500rs_scale_periodic_offset(s16 os_ffb_offset) +{ + return (s8)(os_ffb_offset / 256); } /* @@ -323,24 +338,25 @@ static inline s8 t500rs_scale_periodic_offset(s16 os_ffb_offset) { * Current implementation uses a simple average for magnitude. */ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, - u8 code, s16 start_level, s16 end_level, - u16 duration_ms) { - int avg_level; - u8 magnitude; - s8 offset; - u8 phase; + u8 code, s16 start_level, s16 end_level, + u16 duration_ms) +{ + int avg_level; + u8 magnitude; + s8 offset; + u8 phase; - memset(p, 0, sizeof(*p)); + memset(p, 0, sizeof(*p)); - /* Compute average magnitude from start/end levels */ - avg_level = (abs(start_level) + abs(end_level)) / 2; - magnitude = (u8)((avg_level * 127) / 32767); + /* Compute average magnitude from start/end levels */ + avg_level = (abs(start_level) + abs(end_level)) / 2; + magnitude = (u8)((avg_level * 127) / 32767); - /* Offset encodes direction: positive = ramping up, negative = ramping down */ - /* Simple approximation: (end - start) / 512 to fit in s8 range */ - offset = (s8)((end_level - start_level) / 512); + /* Offset encodes direction: positive = ramping up, negative = ramping down */ + /* Simple approximation: (end - start) / 512 to fit in s8 range */ + offset = (s8)((end_level - start_level) / 512); - /* + /* * Phase encodes ramp direction per FFEdit captures: * - Positive ramp (start < end): phase = 0x7f (127) * - Negative ramp (start > end): phase = 0x00 @@ -350,17 +366,17 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, * - 049a0000007f0000 - phase 0x7f = positive/up direction * - 049a000c00000000 - phase 0x00 = negative/down direction */ - phase = (start_level < end_level) ? 0x7f : 0x00; + phase = (start_level < end_level) ? 0x7f : 0x00; - /* Byte order per USB captures: b0=id, b1=code, b2=reserved1, b3=mag, + /* Byte order per USB captures: b0=id, b1=code, b2=reserved1, b3=mag, * b4=offset, b5=phase, b6-b7=period */ - p->id = 0x04; /* b0 */ - p->code = code; /* b1 */ - p->reserved1 = 0; /* b2: always 0x00 */ - p->magnitude = magnitude; /* b3 */ - p->offset = (u8)offset; /* b4 */ - p->phase = phase; /* b5: direction (0x7f=up, 0x00=down) */ - p->period_ms = cpu_to_le16(duration_ms); /* b6-b7 */ + p->id = 0x04; /* b0 */ + p->code = code; /* b1 */ + p->reserved1 = 0; /* b2: always 0x00 */ + p->magnitude = magnitude; /* b3 */ + p->offset = (u8)offset; /* b4 */ + p->phase = phase; /* b5: direction (0x7f=up, 0x00=down) */ + p->period_ms = cpu_to_le16(duration_ms); /* b6-b7 */ } /* @@ -380,25 +396,27 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, * - center: Center offset from ff_condition_effect (-32767 to +32767) */ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, - u8 code, s16 right_coeff, s16 left_coeff, - u8 right_sat, u8 left_sat, u16 deadband, s16 center) { - memset(p, 0, sizeof(*p)); - p->id = T500RS_PKT_CONDITIONAL; - p->code = code; - p->reserved = 0x00; - - /* Scale coefficients from Linux 0-32767 range to device 0-10 u8 scale */ - p->right_coeff = (u8)((right_coeff * 10) / 32767); - p->left_coeff = (u8)((left_coeff * 10) / 32767); - - /* Scale center from Linux +-32767 range to device s16 LE (approx +-500) */ - p->center = cpu_to_le16((s16)(center / 65)); - - /* Scale deadband from Linux 0-65535 range to device u16 LE (0-1008) */ - p->deadband = cpu_to_le16((u16)(deadband / 65)); - - p->right_sat = right_sat; - p->left_sat = left_sat; + u8 code, s16 right_coeff, s16 left_coeff, + u8 right_sat, u8 left_sat, u16 deadband, + s16 center) +{ + memset(p, 0, sizeof(*p)); + p->id = T500RS_PKT_CONDITIONAL; + p->code = code; + p->reserved = 0x00; + + /* Scale coefficients from Linux 0-32767 range to device 0-10 u8 scale */ + p->right_coeff = (u8)((right_coeff * 10) / 32767); + p->left_coeff = (u8)((left_coeff * 10) / 32767); + + /* Scale center from Linux +-32767 range to device s16 LE (approx +-500) */ + p->center = cpu_to_le16((s16)(center / 65)); + + /* Scale deadband from Linux 0-65535 range to device u16 LE (0-1008) */ + p->deadband = cpu_to_le16((u16)(deadband / 65)); + + p->right_sat = right_sat; + p->left_sat = left_sat; } /* @@ -414,9 +432,10 @@ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, * Linux FFB 32767 -> Device 0 (neutral) * Linux FFB 65535 -> Device +127 (max positive) */ -static inline s8 t500rs_scale_constant_level(u16 os_ffb_level) { - s32 tmp = ((s32)os_ffb_level * 255) / 65535; - return (s8)(tmp - 127); +static inline s8 t500rs_scale_constant_level(u16 os_ffb_level) +{ + s32 tmp = ((s32)os_ffb_level * 255) / 65535; + return (s8)(tmp - 127); } /* @@ -428,11 +447,12 @@ static inline s8 t500rs_scale_constant_level(u16 os_ffb_level) { * - level: signed -127 to +127 */ static void t500rs_build_r03_constant(struct t500rs_r03_const *p, u8 code, - s8 level) { - p->id = T500RS_PKT_CONSTANT; - p->code = code; - p->zero = 0x00; - p->level = level; + s8 level) +{ + p->id = T500RS_PKT_CONSTANT; + p->code = code; + p->zero = 0x00; + p->level = level; } /* @@ -441,13 +461,14 @@ static void t500rs_build_r03_constant(struct t500rs_r03_const *p, u8 code, * Device: 0-255 * Formula: device_level = os_ffb_level * 255 / 32767 */ -static inline u8 t500rs_scale_envelope_level(u16 os_ffb_level) { - /* Input validation and clamping */ - if (os_ffb_level > 32767) - os_ffb_level = 32767; - - /* Use long long arithmetic to prevent overflow */ - return (u8)((os_ffb_level * 255LL) / 32767); +static inline u8 t500rs_scale_envelope_level(u16 os_ffb_level) +{ + /* Input validation and clamping */ + if (os_ffb_level > 32767) + os_ffb_level = 32767; + + /* Use long long arithmetic to prevent overflow */ + return (u8)((os_ffb_level * 255LL) / 32767); } /* @@ -462,14 +483,14 @@ static inline u8 t500rs_scale_envelope_level(u16 os_ffb_level) { * - reserved: always 0x00 */ static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, - u8 subtype, - const struct ff_envelope *env, - bool allow_nonzero) { - memset(p, 0, sizeof(*p)); - p->id = 0x02; - p->subtype = subtype; - - /* + u8 subtype, const struct ff_envelope *env, + bool allow_nonzero) +{ + memset(p, 0, sizeof(*p)); + p->id = 0x02; + p->subtype = subtype; + + /* * Per T500RS_EFFECTS.md, the device firmware rejects * non-zero envelope values for periodic and constant effects with * EPROTO (-71). Only ramp effects can safely use envelopes. @@ -477,312 +498,360 @@ static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, * Windows driver always sends zeros for periodic/constant: * 02 38 00 00 00 00 00 00 00 */ - if (env && allow_nonzero) { - p->attack_len = cpu_to_le16(env->attack_length); - p->attack_level = t500rs_scale_envelope_level(env->attack_level); - p->fade_len = cpu_to_le16(env->fade_length); - p->fade_level = t500rs_scale_envelope_level(env->fade_level); - } else { - /* + if (env && allow_nonzero) { + p->attack_len = cpu_to_le16(env->attack_length); + p->attack_level = + t500rs_scale_envelope_level(env->attack_level); + p->fade_len = cpu_to_le16(env->fade_length); + p->fade_level = t500rs_scale_envelope_level(env->fade_level); + } else { + /* * User requested envelope but device doesn't support it. * Log once to inform user, then send zeros. */ - pr_warn_once("t500rs: Envelope requested but not supported for this effect type\n"); - } + pr_warn_once( + "t500rs: Envelope requested but not supported for this effect type\n"); + } } /* Supported parameters */ static unsigned long t500rs_params = PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | - PARAM_FRICTION_LEVEL | PARAM_GAIN | - PARAM_RANGE; + PARAM_FRICTION_LEVEL | PARAM_GAIN | + PARAM_RANGE; /* Supported effects. */ -const signed short t500rs_effects[] = { - FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_FRICTION, FF_INERTIA, - FF_PERIODIC, FF_SQUARE, FF_SINE, FF_TRIANGLE, FF_SAW_UP, - FF_SAW_DOWN, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1}; +const signed short t500rs_effects[] = { FF_CONSTANT, FF_SPRING, FF_DAMPER, + FF_FRICTION, FF_INERTIA, FF_PERIODIC, + FF_SQUARE, FF_SINE, FF_TRIANGLE, + FF_SAW_UP, FF_SAW_DOWN, FF_RAMP, + FF_GAIN, FF_AUTOCENTER, -1 }; -/* Forward declarations to avoid implicit declarations before worker uses them */ +/* Forward declarations to avoid implicit declarations before worker uses them + */ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, - size_t len); + size_t len); static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, - u8 hw_effect_id); + u8 hw_effect_id); 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); + const struct tmff2_effect_state *state); static int t500rs_update_effect(void *data, - const struct tmff2_effect_state *state); + const struct tmff2_effect_state *state); static int t500rs_play_effect(void *data, - const struct tmff2_effect_state *state); + const struct tmff2_effect_state *state); static int t500rs_stop_effect(void *data, - const struct tmff2_effect_state *state); + const struct tmff2_effect_state *state); /* * Send a sequence of packets for effect upload. * Abstracts the hardcoded packet orders in upload functions. */ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, - const struct tmff2_effect_state *state, - u8 hw_id, - const enum t500rs_seq_packet *sequence, - size_t seq_len) { - const struct ff_effect *effect = &state->effect; - u8 *buf = t500rs->send_buffer; - int ret; - u16 param_sub, env_sub; - - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - - for (size_t i = 0; i < seq_len; i++) { - /* Log sequence progress for debugging */ - T500RS_DBG(t500rs, "Sequence step %zu/%zu: packet type 0x%02x\n", - i + 1, seq_len, sequence[i]); - - switch (sequence[i]) { - case T500RS_SEQ_STOP: - ret = t500rs_send_stop(t500rs, hw_id); - break; - - case T500RS_SEQ_SYNC_42_05: - buf[0] = 0x42; - buf[1] = 0x05; - ret = t500rs_send_hid(t500rs, buf, 2); - break; - - case T500RS_SEQ_SYNC_42_04: - buf[0] = 0x42; - buf[1] = 0x04; - ret = t500rs_send_hid(t500rs, buf, 2); - break; - - case T500RS_SEQ_ENVELOPE: { - struct t500rs_pkt_r02_envelope *env = - (struct t500rs_pkt_r02_envelope *)buf; - const struct ff_envelope *envelope = NULL; - bool allow_envelope = false; - - if (effect->type == FF_RAMP) { - envelope = &effect->u.ramp.envelope; - allow_envelope = true; /* Ramp supports envelope */ - } else if (effect->type == FF_CONSTANT || effect->type == FF_PERIODIC) { - envelope = &effect->u.periodic.envelope; - allow_envelope = false; /* Firmware bug: must send zeros */ - } - - t500rs_build_r02_envelope(env, (u8)(env_sub & 0xff), envelope, allow_envelope); - ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r02_envelope)); - break; - } - - case T500RS_SEQ_CONSTANT: { - s8 level = t500rs_scale_const_with_direction(effect->u.constant.level, - effect->direction); - struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; - t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), level); - ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_r03_const)); - break; - } - - case T500RS_SEQ_PERIODIC_RAMP: { - if (effect->type == FF_RAMP) { - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), - effect->u.ramp.start_level, - effect->u.ramp.end_level, effect->replay.length); - } else { - /* Apply direction projection to magnitude and adjust phase if needed */ - u16 phase_raw = effect->u.periodic.phase; - u8 mag = t500rs_scale_periodic_with_direction( - effect->u.periodic.magnitude, effect->direction, &phase_raw); - u8 phase = t500rs_scale_periodic_phase(phase_raw); - s8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); - u16 period_ms = effect->u.periodic.period; - if (period_ms == 0) { - hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); - return -EINVAL; - } - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, - period_ms); - } - ret = t500rs_send_hid(t500rs, buf, - sizeof(struct t500rs_pkt_r04_periodic_ramp)); - if (ret) - break; - break; - } - - case T500RS_SEQ_CONDITION_X: { - const struct ff_condition_effect *cond = &effect->u.condition[0]; - /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) */ - u8 right_sat = (cond->right_saturation * 100) / 65535; - u8 left_sat = (cond->left_saturation * 100) / 65535; - - struct t500rs_pkt_r05_condition *p = - (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub), cond->right_coeff, - cond->left_coeff, right_sat, left_sat, cond->deadband, - cond->center); - ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); - break; - } - - case T500RS_SEQ_CONDITION_Y: { - /* Y-axis: use condition[1] if available, else zeros */ - const struct ff_condition_effect *cond = &effect->u.condition[1]; - /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) */ - u8 right_sat = (cond->right_saturation * 100) / 65535; - u8 left_sat = (cond->left_saturation * 100) / 65535; - - struct t500rs_pkt_r05_condition *p = - (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), cond->right_coeff, - cond->left_coeff, right_sat, left_sat, cond->deadband, - cond->center); - ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r05_condition)); - break; - } - - case T500RS_SEQ_MAIN: { - u8 effect_type = 0; - switch (effect->type) { - case FF_CONSTANT: - effect_type = T500RS_EFFECT_CONSTANT; - break; - case FF_SPRING: - effect_type = T500RS_EFFECT_SPRING; - break; - case FF_DAMPER: - effect_type = T500RS_EFFECT_DAMPER; - break; - case FF_FRICTION: - effect_type = T500RS_EFFECT_FRICTION; - break; - case FF_INERTIA: - effect_type = T500RS_EFFECT_INERTIA; - break; - case FF_PERIODIC: - switch (effect->u.periodic.waveform) { - case FF_SQUARE: - effect_type = T500RS_EFFECT_SQUARE; - break; - case FF_SINE: - effect_type = T500RS_EFFECT_SINE; - break; - case FF_TRIANGLE: - effect_type = T500RS_EFFECT_TRIANGLE; - break; - case FF_SAW_UP: - effect_type = T500RS_EFFECT_SAW_UP; - break; - case FF_SAW_DOWN: - effect_type = T500RS_EFFECT_SAW_DOWN; - break; - default: - return -EINVAL; - } - break; - case FF_RAMP: - effect_type = T500RS_EFFECT_SAW_DOWN; - break; - default: - return -EINVAL; - } - - u16 duration_ms = effect->replay.length ? effect->replay.length : 0xffff; - u16 delay_ms = effect->replay.delay; - - struct t500rs_pkt_r01_main *m = (struct t500rs_pkt_r01_main *)buf; - ret = t500rs_build_r01_main(m, hw_id, effect_type, duration_ms, delay_ms, - param_sub, env_sub); - if (ret) - break; - - ret = t500rs_send_hid(t500rs, buf, sizeof(struct t500rs_pkt_r01_main)); - break; - } - - default: - ret = -EINVAL; - } - - if (ret) { - hid_err(t500rs->hdev, - "Sequence failed at step %zu/%zu (packet type 0x%02x): %d\n", - i + 1, seq_len, sequence[i], ret); - return ret; - } - } - - T500RS_DBG(t500rs, "Sequence completed successfully (%zu packets)\n", seq_len); - return 0; + const struct tmff2_effect_state *state, + u8 hw_id, + const enum t500rs_seq_packet *sequence, + size_t seq_len) +{ + const struct ff_effect *effect = &state->effect; + u8 *buf = t500rs->send_buffer; + int ret; + u16 param_sub, env_sub; + + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + + for (size_t i = 0; i < seq_len; i++) { + /* Log sequence progress for debugging */ + T500RS_DBG(t500rs, + "Sequence step %zu/%zu: packet type 0x%02x\n", i + 1, + seq_len, sequence[i]); + + switch (sequence[i]) { + case T500RS_SEQ_STOP: + ret = t500rs_send_stop(t500rs, hw_id); + break; + + case T500RS_SEQ_SYNC_42_05: + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_hid(t500rs, buf, 2); + break; + + case T500RS_SEQ_SYNC_42_04: + buf[0] = 0x42; + buf[1] = 0x04; + ret = t500rs_send_hid(t500rs, buf, 2); + break; + + case T500RS_SEQ_ENVELOPE: { + struct t500rs_pkt_r02_envelope *env = + (struct t500rs_pkt_r02_envelope *)buf; + const struct ff_envelope *envelope = NULL; + bool allow_envelope = false; + + if (effect->type == FF_RAMP) { + envelope = &effect->u.ramp.envelope; + allow_envelope = + true; /* Ramp supports envelope */ + } else if (effect->type == FF_CONSTANT || + effect->type == FF_PERIODIC) { + envelope = &effect->u.periodic.envelope; + allow_envelope = + false; /* Firmware bug: must send zeros */ + } + + t500rs_build_r02_envelope(env, (u8)(env_sub & 0xff), + envelope, allow_envelope); + ret = t500rs_send_hid( + t500rs, buf, + sizeof(struct t500rs_pkt_r02_envelope)); + break; + } + + case T500RS_SEQ_CONSTANT: { + s8 level = t500rs_scale_const_with_direction( + effect->u.constant.level, effect->direction); + struct t500rs_r03_const *r3 = + (struct t500rs_r03_const *)buf; + t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), + level); + ret = t500rs_send_hid(t500rs, buf, + sizeof(struct t500rs_r03_const)); + break; + } + + case T500RS_SEQ_PERIODIC_RAMP: { + if (effect->type == FF_RAMP) { + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *) + buf; + t500rs_build_r04_ramp( + p, (u8)(param_sub & 0xff), + effect->u.ramp.start_level, + effect->u.ramp.end_level, + effect->replay.length); + } else { + /* Apply direction projection to magnitude and adjust phase if needed */ + u16 phase_raw = effect->u.periodic.phase; + u8 mag = t500rs_scale_periodic_with_direction( + effect->u.periodic.magnitude, + effect->direction, &phase_raw); + u8 phase = + t500rs_scale_periodic_phase(phase_raw); + s8 offset = t500rs_scale_periodic_offset( + effect->u.periodic.offset); + u16 period_ms = effect->u.periodic.period; + if (period_ms == 0) { + hid_err(t500rs->hdev, + "Periodic effect period cannot be zero\n"); + return -EINVAL; + } + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *) + buf; + t500rs_build_r04_periodic( + p, (u8)(param_sub & 0xff), mag, offset, + phase, period_ms); + } + ret = t500rs_send_hid( + t500rs, buf, + sizeof(struct t500rs_pkt_r04_periodic_ramp)); + if (ret) + break; + break; + } + + case T500RS_SEQ_CONDITION_X: { + const struct ff_condition_effect *cond = + &effect->u.condition[0]; + /* Scale saturation from Linux FFB range (0..65535) to device range + * (0..100) */ + u8 right_sat = (cond->right_saturation * 100) / 65535; + u8 left_sat = (cond->left_saturation * 100) / 65535; + + struct t500rs_pkt_r05_condition *p = + (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, (u8)(param_sub), + cond->right_coeff, + cond->left_coeff, right_sat, + left_sat, cond->deadband, + cond->center); + ret = t500rs_send_hid( + t500rs, buf, + sizeof(struct t500rs_pkt_r05_condition)); + break; + } + + case T500RS_SEQ_CONDITION_Y: { + /* Y-axis: use condition[1] if available, else zeros */ + const struct ff_condition_effect *cond = + &effect->u.condition[1]; + /* Scale saturation from Linux FFB range (0..65535) to device range + * (0..100) */ + u8 right_sat = (cond->right_saturation * 100) / 65535; + u8 left_sat = (cond->left_saturation * 100) / 65535; + + struct t500rs_pkt_r05_condition *p = + (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), + cond->right_coeff, + cond->left_coeff, right_sat, + left_sat, cond->deadband, + cond->center); + ret = t500rs_send_hid( + t500rs, buf, + sizeof(struct t500rs_pkt_r05_condition)); + break; + } + + case T500RS_SEQ_MAIN: { + u8 effect_type = 0; + switch (effect->type) { + case FF_CONSTANT: + effect_type = T500RS_EFFECT_CONSTANT; + break; + case FF_SPRING: + effect_type = T500RS_EFFECT_SPRING; + break; + case FF_DAMPER: + effect_type = T500RS_EFFECT_DAMPER; + break; + case FF_FRICTION: + effect_type = T500RS_EFFECT_FRICTION; + break; + case FF_INERTIA: + effect_type = T500RS_EFFECT_INERTIA; + break; + case FF_PERIODIC: + switch (effect->u.periodic.waveform) { + case FF_SQUARE: + effect_type = T500RS_EFFECT_SQUARE; + break; + case FF_SINE: + effect_type = T500RS_EFFECT_SINE; + break; + case FF_TRIANGLE: + effect_type = T500RS_EFFECT_TRIANGLE; + break; + case FF_SAW_UP: + effect_type = T500RS_EFFECT_SAW_UP; + break; + case FF_SAW_DOWN: + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + return -EINVAL; + } + break; + case FF_RAMP: + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + return -EINVAL; + } + + u16 duration_ms = effect->replay.length ? + effect->replay.length : + 0xffff; + u16 delay_ms = effect->replay.delay; + + struct t500rs_pkt_r01_main *m = + (struct t500rs_pkt_r01_main *)buf; + ret = t500rs_build_r01_main(m, hw_id, effect_type, + duration_ms, delay_ms, + param_sub, env_sub); + if (ret) + break; + + ret = t500rs_send_hid( + t500rs, buf, + sizeof(struct t500rs_pkt_r01_main)); + break; + } + + default: + ret = -EINVAL; + } + + if (ret) { + hid_err(t500rs->hdev, + "Sequence failed at step %zu/%zu (packet type 0x%02x): %d\n", + i + 1, seq_len, sequence[i], ret); + return ret; + } + } + + T500RS_DBG(t500rs, "Sequence completed successfully (%zu packets)\n", + seq_len); + return 0; } -static int t500rs_set_gain(void *data, u16 gain) { - struct t500rs_device_entry *t500rs = data; - u8 *buf; - u8 device_gain_byte; - int ret; +static int t500rs_set_gain(void *data, u16 gain) +{ + struct t500rs_device_entry *t500rs = data; + u8 *buf; + u8 device_gain_byte; + int ret; - if (!t500rs->send_buffer) { - hid_err(t500rs->hdev, "t500rs_set_gain: NULL send buffer\n"); - return -ENOMEM; - } + if (!t500rs->send_buffer) { + hid_err(t500rs->hdev, "t500rs_set_gain: NULL send buffer\n"); + return -ENOMEM; + } - buf = t500rs->send_buffer; + buf = t500rs->send_buffer; - /* Scale 0..65535 to device 0..255 */ - device_gain_byte = (u8)((gain * 255ULL) / T500RS_GAIN_MAX); + /* Scale 0..65535 to device 0..255 */ + device_gain_byte = (u8)((gain * 255ULL) / T500RS_GAIN_MAX); - hid_info(t500rs->hdev, "FFB: set_gain %u -> device %u\n", gain, - device_gain_byte); + hid_info(t500rs->hdev, "FFB: set_gain %u -> device %u\n", gain, + device_gain_byte); - buf[0] = T500RS_PKT_GAIN; - buf[1] = device_gain_byte; + buf[0] = T500RS_PKT_GAIN; + buf[1] = device_gain_byte; - ret = t500rs_send_hid(t500rs, buf, 2); - if (ret == 0) - hid_info(t500rs->hdev, "FFB: Gain set successfully\n"); - else - hid_err(t500rs->hdev, "FFB: Failed to set gain: %d\n", ret); - return ret; + ret = t500rs_send_hid(t500rs, buf, 2); + if (ret == 0) + hid_info(t500rs->hdev, "FFB: Gain set successfully\n"); + else + hid_err(t500rs->hdev, "FFB: Failed to set gain: %d\n", ret); + return ret; } /* Send data via HID output report (blocking) */ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, - size_t len) { - int ret; - - /* Input validation */ - if (len == 0 || len > T500RS_BUFFER_LENGTH) { - hid_err(t500rs->hdev, "t500rs_send_hid: Invalid length %zu (max %d)\n", len, - T500RS_BUFFER_LENGTH); - return -EINVAL; - } - - ret = hid_hw_output_report(t500rs->hdev, data, len); - if (ret < 0) { - hid_err(t500rs->hdev, "HID output report failed: %d\n", ret); - return ret; - } - - if (ret != len) { - hid_err(t500rs->hdev, - "HID output report truncated: sent %d, expected %zu\n", ret, len); - return -EIO; - } - - return 0; + size_t len) +{ + int ret; + + /* Input validation */ + if (len == 0 || len > T500RS_BUFFER_LENGTH) { + hid_err(t500rs->hdev, + "t500rs_send_hid: Invalid length %zu (max %d)\n", len, + T500RS_BUFFER_LENGTH); + return -EINVAL; + } + + ret = hid_hw_output_report(t500rs->hdev, data, len); + if (ret < 0) { + hid_err(t500rs->hdev, "HID output report failed: %d\n", ret); + return ret; + } + + if (ret != len) { + hid_err(t500rs->hdev, + "HID output report truncated: sent %d, expected %zu\n", + ret, len); + return -EIO; + } + + return 0; } /* @@ -792,68 +861,72 @@ static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, * command = 0x00 for STOP, 0x41 for START */ static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, - u8 hw_effect_id) { - struct t500rs_r41_cmd *r41; - if (!t500rs) - return -ENODEV; - - r41 = (struct t500rs_r41_cmd *)t500rs->send_buffer; - if (!r41) - return -ENOMEM; - - r41->id = 0x41; - r41->effect_id = hw_effect_id; - r41->command = 0x00; /* STOP */ - r41->arg = 0x01; - return t500rs_send_hid(t500rs, (u8 *)r41, sizeof(*r41)); + u8 hw_effect_id) +{ + struct t500rs_r41_cmd *r41; + if (!t500rs) + return -ENODEV; + + r41 = (struct t500rs_r41_cmd *)t500rs->send_buffer; + if (!r41) + return -ENOMEM; + + r41->id = 0x41; + r41->effect_id = hw_effect_id; + r41->command = 0x00; /* STOP */ + r41->arg = 0x01; + return t500rs_send_hid(t500rs, (u8 *)r41, sizeof(*r41)); } /* * Send START command for a specific hardware effect ID. */ static inline int t500rs_send_start(struct t500rs_device_entry *t500rs, - u8 hw_effect_id) { - struct t500rs_r41_cmd *r41; - if (!t500rs) - return -ENODEV; - - r41 = (struct t500rs_r41_cmd *)t500rs->send_buffer; - if (!r41) - return -ENOMEM; - - r41->id = 0x41; - r41->effect_id = hw_effect_id; - r41->command = 0x41; /* START */ - r41->arg = 0x01; - return t500rs_send_hid(t500rs, (u8 *)r41, sizeof(*r41)); + u8 hw_effect_id) +{ + struct t500rs_r41_cmd *r41; + if (!t500rs) + return -ENODEV; + + r41 = (struct t500rs_r41_cmd *)t500rs->send_buffer; + if (!r41) + return -ENOMEM; + + r41->id = 0x41; + r41->effect_id = hw_effect_id; + r41->command = 0x41; /* START */ + r41->arg = 0x01; + return t500rs_send_hid(t500rs, (u8 *)r41, sizeof(*r41)); } /* 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; - int ret; - int hw_id; - int level = effect->u.constant.level; - - /* Note: Gain is applied in play_effect, not here */ - T500RS_DBG(t500rs, "Upload constant: id=%d, level=%d, dir=%u\n", effect->id, - level, effect->direction); - - hw_id = t500rs_logical_to_hw_id(effect->id); - - /* Send packet sequence for constant effect */ - ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_constant, - sizeof(t500rs_seq_constant) / - sizeof(t500rs_seq_constant[0])); - if (ret) { - hid_err(t500rs->hdev, "Failed to send constant effect sequence: %d\n", ret); - return ret; - } - - T500RS_DBG(t500rs, "Constant effect %d uploaded (hw_id=%d)\n", effect->id, - hw_id); - return 0; + const struct tmff2_effect_state *state) +{ + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + int level = effect->u.constant.level; + + /* Note: Gain is applied in play_effect, not here */ + T500RS_DBG(t500rs, "Upload constant: id=%d, level=%d, dir=%u\n", + effect->id, level, effect->direction); + + hw_id = t500rs_logical_to_hw_id(effect->id); + + /* Send packet sequence for constant effect */ + ret = t500rs_send_packet_sequence( + t500rs, state, hw_id, t500rs_seq_constant, + sizeof(t500rs_seq_constant) / sizeof(t500rs_seq_constant[0])); + if (ret) { + hid_err(t500rs->hdev, + "Failed to send constant effect sequence: %d\n", ret); + return ret; + } + + T500RS_DBG(t500rs, "Constant effect %d uploaded (hw_id=%d)\n", + effect->id, hw_id); + return 0; } /* @@ -865,63 +938,66 @@ static int t500rs_upload_constant(struct t500rs_device_entry *t500rs, * - Saturation values 0x54 (84) for spring, 0x64 (100) for damper/friction */ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, - const struct tmff2_effect_state *state) { - const struct ff_effect *effect = &state->effect; - int ret; - int hw_id; - u8 effect_gain; - const char *type_name; - /* + const struct tmff2_effect_state *state) +{ + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + u8 effect_gain; + const char *type_name; + /* * Determine effect type code and gain level. * Per Windows captures: Spring=0x40, Damper/Friction/Inertia=0x41 */ - u8 effect_type; - switch (effect->type) { - case FF_SPRING: - type_name = "spring"; - effect_gain = spring_level; - effect_type = T500RS_EFFECT_SPRING; - break; - case FF_DAMPER: - type_name = "damper"; - effect_gain = damper_level; - effect_type = T500RS_EFFECT_DAMPER; - break; - case FF_FRICTION: - type_name = "friction"; - effect_gain = friction_level; - effect_type = T500RS_EFFECT_FRICTION; - break; - case FF_INERTIA: - type_name = "inertia"; - effect_gain = 100; - effect_type = T500RS_EFFECT_INERTIA; - break; - default: - return -EINVAL; - } - - hw_id = t500rs_logical_to_hw_id(effect->id); - - /* Send packet sequence for conditional effect */ - ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_condition, - sizeof(t500rs_seq_condition) / - sizeof(t500rs_seq_condition[0])); - if (ret) { - hid_err(t500rs->hdev, "Failed to send %s effect sequence: %d\n", type_name, - ret); - return ret; - } - - return 0; + u8 effect_type; + switch (effect->type) { + case FF_SPRING: + type_name = "spring"; + effect_gain = spring_level; + effect_type = T500RS_EFFECT_SPRING; + break; + case FF_DAMPER: + type_name = "damper"; + effect_gain = damper_level; + effect_type = T500RS_EFFECT_DAMPER; + break; + case FF_FRICTION: + type_name = "friction"; + effect_gain = friction_level; + effect_type = T500RS_EFFECT_FRICTION; + break; + case FF_INERTIA: + type_name = "inertia"; + effect_gain = 100; + effect_type = T500RS_EFFECT_INERTIA; + break; + default: + return -EINVAL; + } + + hw_id = t500rs_logical_to_hw_id(effect->id); + + /* Send packet sequence for conditional effect */ + ret = t500rs_send_packet_sequence( + t500rs, state, hw_id, t500rs_seq_condition, + sizeof(t500rs_seq_condition) / sizeof(t500rs_seq_condition[0])); + if (ret) { + hid_err(t500rs->hdev, "Failed to send %s effect sequence: %d\n", + type_name, ret); + return ret; + } + + return 0; } /* * Upload periodic effect (sine, square, triangle, saw). * * Per Windows captures (T500RS_FFBEFFECTS.md): - * - Waveform type is NOT encoded in USB packets; determined by Linux FFB subsystem - * - 0x01 packet: direction, duration, delay, param_sub=0x000e, envelope_sub=0x001c + * - Waveform type is NOT encoded in USB packets; determined by Linux FFB + * subsystem + * - 0x01 packet: direction, duration, delay, param_sub=0x000e, + * envelope_sub=0x001c * - 0x02 packet: envelope with subtype 0x1c * - 0x04 packet: code=0x2a (NOT 0x0e!), magnitude, offset, phase, period_ms * - Period is in MILLISECONDS (no Hz*100 conversion) @@ -931,14 +1007,15 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, * may have been incorrect and is removed. */ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, - const struct tmff2_effect_state *state) { - const struct ff_effect *effect = &state->effect; - int ret; - int hw_id; - const char *type_name; - u8 effect_type; - - /* + const struct tmff2_effect_state *state) +{ + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + const char *type_name; + u8 effect_type; + + /* * Determine waveform name and effect_type for 0x01 packet. * * Per Windows captures, waveform type IS encoded in the 0x01 packet's @@ -951,47 +1028,47 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, * - 0x23 = Sawtooth Up * - 0x24 = Sawtooth Down */ - switch (effect->u.periodic.waveform) { - case FF_SQUARE: - type_name = "square"; - effect_type = T500RS_EFFECT_SQUARE; - break; - case FF_TRIANGLE: - type_name = "triangle"; - effect_type = T500RS_EFFECT_TRIANGLE; - break; - case FF_SINE: - type_name = "sine"; - effect_type = T500RS_EFFECT_SINE; - break; - case FF_SAW_UP: - type_name = "sawtooth_up"; - effect_type = T500RS_EFFECT_SAW_UP; - break; - case FF_SAW_DOWN: - type_name = "sawtooth_down"; - effect_type = T500RS_EFFECT_SAW_DOWN; - break; - default: - hid_err(t500rs->hdev, "Unsupported periodic waveform: %d\n", - effect->u.periodic.waveform); - return -EINVAL; - } - - hw_id = t500rs_logical_to_hw_id(effect->id); - - /* Send packet sequence for periodic effect */ - ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_periodic, - sizeof(t500rs_seq_periodic) / - sizeof(t500rs_seq_periodic[0])); - if (ret) { - hid_err(t500rs->hdev, "Failed to send %s effect sequence: %d\n", type_name, - ret); - return ret; - } - - T500RS_DBG(t500rs, "%s effect %d uploaded\n", type_name, effect->id); - return 0; + switch (effect->u.periodic.waveform) { + case FF_SQUARE: + type_name = "square"; + effect_type = T500RS_EFFECT_SQUARE; + break; + case FF_TRIANGLE: + type_name = "triangle"; + effect_type = T500RS_EFFECT_TRIANGLE; + break; + case FF_SINE: + type_name = "sine"; + effect_type = T500RS_EFFECT_SINE; + break; + case FF_SAW_UP: + type_name = "sawtooth_up"; + effect_type = T500RS_EFFECT_SAW_UP; + break; + case FF_SAW_DOWN: + type_name = "sawtooth_down"; + effect_type = T500RS_EFFECT_SAW_DOWN; + break; + default: + hid_err(t500rs->hdev, "Unsupported periodic waveform: %d\n", + effect->u.periodic.waveform); + return -EINVAL; + } + + hw_id = t500rs_logical_to_hw_id(effect->id); + + /* Send packet sequence for periodic effect */ + ret = t500rs_send_packet_sequence( + t500rs, state, hw_id, t500rs_seq_periodic, + sizeof(t500rs_seq_periodic) / sizeof(t500rs_seq_periodic[0])); + if (ret) { + hid_err(t500rs->hdev, "Failed to send %s effect sequence: %d\n", + type_name, ret); + return ret; + } + + T500RS_DBG(t500rs, "%s effect %d uploaded\n", type_name, effect->id); + return 0; } /* @@ -1004,187 +1081,202 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, * - Period field encodes ramp duration */ static int t500rs_upload_ramp(struct t500rs_device_entry *t500rs, - const struct tmff2_effect_state *state) { - const struct ff_effect *effect = &state->effect; - int ret; - int hw_id; - - hw_id = t500rs_logical_to_hw_id(effect->id); - - /* Send packet sequence for ramp effect */ - ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_ramp, - sizeof(t500rs_seq_ramp) / - sizeof(t500rs_seq_ramp[0])); - if (ret) { - hid_err(t500rs->hdev, "Failed to send ramp effect sequence: %d\n", ret); - return ret; - } - - T500RS_DBG(t500rs, "Ramp effect %d uploaded\n", effect->id); - return 0; + const struct tmff2_effect_state *state) +{ + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + + hw_id = t500rs_logical_to_hw_id(effect->id); + + /* Send packet sequence for ramp effect */ + ret = t500rs_send_packet_sequence(t500rs, state, hw_id, t500rs_seq_ramp, + sizeof(t500rs_seq_ramp) / + sizeof(t500rs_seq_ramp[0])); + if (ret) { + hid_err(t500rs->hdev, + "Failed to send ramp effect sequence: %d\n", ret); + return ret; + } + + T500RS_DBG(t500rs, "Ramp effect %d uploaded\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; - int ret; - - effect = &state->effect; - - /* Validate effect ID range */ - if (effect->id >= T500RS_MAX_EFFECTS) { - hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", effect->id, - T500RS_MAX_EFFECTS); - return -EINVAL; - } - - /* Validate effect parameters based on type */ - switch (effect->type) { - case FF_CONSTANT: - /* Validate constant force level */ - if (effect->u.constant.level < -32767 || effect->u.constant.level > 32767) { - hid_err(t500rs->hdev, "Constant level %d out of range [-32767, 32767]\n", - effect->u.constant.level); - return -EINVAL; - } - break; - - case FF_PERIODIC: - /* Validate periodic effect parameters */ - if (effect->u.periodic.magnitude < 0 || - effect->u.periodic.magnitude > 32767) { - hid_err(t500rs->hdev, "Periodic magnitude %d out of range [0, 32767]\n", - effect->u.periodic.magnitude); - return -EINVAL; - } - if (effect->u.periodic.offset < -32768 || - effect->u.periodic.offset > 32767) { - hid_err(t500rs->hdev, "Periodic offset %d out of range [-32768, 32767]\n", - effect->u.periodic.offset); - return -EINVAL; - } - if (effect->u.periodic.phase > 35999) { - hid_err(t500rs->hdev, "Periodic phase %u exceeds maximum 35999\n", - effect->u.periodic.phase); - return -EINVAL; - } - break; - - case FF_RAMP: - /* Validate ramp effect parameters */ - if (effect->u.ramp.start_level < -32767 || - effect->u.ramp.start_level > 32767) { - hid_err(t500rs->hdev, - "Ramp start level %d out of range [-32767, 32767]\n", - effect->u.ramp.start_level); - return -EINVAL; - } - if (effect->u.ramp.end_level < -32767 || effect->u.ramp.end_level > 32767) { - hid_err(t500rs->hdev, "Ramp end level %d out of range [-32767, 32767]\n", - effect->u.ramp.end_level); - return -EINVAL; - } - break; - - case FF_SPRING: - case FF_DAMPER: - case FF_FRICTION: - case FF_INERTIA: - break; - - default: - hid_err(t500rs->hdev, "Unsupported effect type: %d\n", effect->type); - return -EINVAL; - } - - /* Validate common parameters */ - /* Direction is provided by the Linux FF subsystem as 0..65535 (u16). + const struct tmff2_effect_state *state) +{ + struct t500rs_device_entry *t500rs = data; + const struct ff_effect *effect; + int ret; + + effect = &state->effect; + + /* Validate effect ID range */ + if (effect->id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", + effect->id, T500RS_MAX_EFFECTS); + return -EINVAL; + } + + /* Validate effect parameters based on type */ + switch (effect->type) { + case FF_CONSTANT: + /* Validate constant force level */ + if (effect->u.constant.level < -32767 || + effect->u.constant.level > 32767) { + hid_err(t500rs->hdev, + "Constant level %d out of range [-32767, 32767]\n", + effect->u.constant.level); + return -EINVAL; + } + break; + + case FF_PERIODIC: + /* Validate periodic effect parameters */ + if (effect->u.periodic.magnitude < 0 || + effect->u.periodic.magnitude > 32767) { + hid_err(t500rs->hdev, + "Periodic magnitude %d out of range [0, 32767]\n", + effect->u.periodic.magnitude); + return -EINVAL; + } + if (effect->u.periodic.offset < -32768 || + effect->u.periodic.offset > 32767) { + hid_err(t500rs->hdev, + "Periodic offset %d out of range [-32768, 32767]\n", + effect->u.periodic.offset); + return -EINVAL; + } + if (effect->u.periodic.phase > 35999) { + hid_err(t500rs->hdev, + "Periodic phase %u exceeds maximum 35999\n", + effect->u.periodic.phase); + return -EINVAL; + } + break; + + case FF_RAMP: + /* Validate ramp effect parameters */ + if (effect->u.ramp.start_level < -32767 || + effect->u.ramp.start_level > 32767) { + hid_err(t500rs->hdev, + "Ramp start level %d out of range [-32767, 32767]\n", + effect->u.ramp.start_level); + return -EINVAL; + } + if (effect->u.ramp.end_level < -32767 || + effect->u.ramp.end_level > 32767) { + hid_err(t500rs->hdev, + "Ramp end level %d out of range [-32767, 32767]\n", + effect->u.ramp.end_level); + return -EINVAL; + } + break; + + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + break; + + default: + hid_err(t500rs->hdev, "Unsupported effect type: %d\n", + effect->type); + return -EINVAL; + } + + /* Validate common parameters */ + /* Direction is provided by the Linux FF subsystem as 0..65535 (u16). * The device expects 0..35999 (0.01 degree units); scaling is done by * t500rs_scale_direction() when sending packets. Accept the full u16 * range here instead of rejecting values >35999 (e.g. 49152). */ - /* no validation needed here */ - if (effect->replay.delay > 65535) { - hid_err(t500rs->hdev, "Delay %u exceeds maximum 65535\n", - effect->replay.delay); - return -EINVAL; - } - - switch (effect->type) { - case FF_CONSTANT: - ret = t500rs_upload_constant(t500rs, state); - break; - case FF_SPRING: - case FF_DAMPER: - case FF_FRICTION: - case FF_INERTIA: - ret = t500rs_upload_condition(t500rs, state); - break; - case FF_PERIODIC: - case FF_SQUARE: - case FF_SINE: - case FF_TRIANGLE: - case FF_SAW_UP: - case FF_SAW_DOWN: - ret = t500rs_upload_periodic(t500rs, state); - break; - case FF_RAMP: - ret = t500rs_upload_ramp(t500rs, state); - break; - default: - hid_err(t500rs->hdev, "Unsupported effect type: %d\n", effect->type); - return -EINVAL; - } - - if (ret < 0) { - hid_err(t500rs->hdev, "Failed to upload effect type %d, id %d: %d\n", - effect->type, effect->id, ret); - } - return ret; + /* no validation needed here */ + if (effect->replay.delay > 65535) { + hid_err(t500rs->hdev, "Delay %u exceeds maximum 65535\n", + effect->replay.delay); + return -EINVAL; + } + + switch (effect->type) { + case FF_CONSTANT: + ret = t500rs_upload_constant(t500rs, state); + break; + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + ret = t500rs_upload_condition(t500rs, state); + break; + case FF_PERIODIC: + case FF_SQUARE: + case FF_SINE: + case FF_TRIANGLE: + case FF_SAW_UP: + case FF_SAW_DOWN: + ret = t500rs_upload_periodic(t500rs, state); + break; + case FF_RAMP: + ret = t500rs_upload_ramp(t500rs, state); + break; + default: + hid_err(t500rs->hdev, "Unsupported effect type: %d\n", + effect->type); + return -EINVAL; + } + + if (ret < 0) { + hid_err(t500rs->hdev, + "Failed to upload effect type %d, id %d: %d\n", + effect->type, effect->id, ret); + } + return ret; } /* * Play effect - send START command (0x41) for the 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; - int ret; - int hw_id; - - /* Validate effect ID range */ - if (effect->id >= T500RS_MAX_EFFECTS) { - hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", effect->id, - T500RS_MAX_EFFECTS); - return -EINVAL; - } - - /* Validate effect type is supported */ - switch (effect->type) { - case FF_CONSTANT: - case FF_PERIODIC: - case FF_RAMP: - case FF_SPRING: - case FF_DAMPER: - case FF_FRICTION: - case FF_INERTIA: - break; - default: - hid_err(t500rs->hdev, "Unsupported effect type for play: %d\n", - effect->type); - return -EINVAL; - } - - hw_id = t500rs_logical_to_hw_id(effect->id); - - ret = t500rs_send_start(t500rs, (u8)hw_id); - if (ret == 0) { - T500RS_DBG(t500rs, "Started effect %d (hw_id=%d)\n", effect->id, hw_id); - } - return ret; + const struct tmff2_effect_state *state) +{ + struct t500rs_device_entry *t500rs = data; + const struct ff_effect *effect = &state->effect; + int ret; + int hw_id; + + /* Validate effect ID range */ + if (effect->id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", + effect->id, T500RS_MAX_EFFECTS); + return -EINVAL; + } + + /* Validate effect type is supported */ + switch (effect->type) { + case FF_CONSTANT: + case FF_PERIODIC: + case FF_RAMP: + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: + break; + default: + hid_err(t500rs->hdev, "Unsupported effect type for play: %d\n", + effect->type); + return -EINVAL; + } + + hw_id = t500rs_logical_to_hw_id(effect->id); + + ret = t500rs_send_start(t500rs, (u8)hw_id); + if (ret == 0) { + T500RS_DBG(t500rs, "Started effect %d (hw_id=%d)\n", effect->id, + hw_id); + } + return ret; } /* @@ -1192,26 +1284,27 @@ static int t500rs_play_effect(void *data, * No slot freeing needed with simplified hw_id = logical_id + 1 mapping. */ static int t500rs_stop_effect(void *data, - const struct tmff2_effect_state *state) { - struct t500rs_device_entry *t500rs = data; - int hw_id; - - /* Validate effect ID range */ - if (state->effect.id >= T500RS_MAX_EFFECTS) { - hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", state->effect.id, - T500RS_MAX_EFFECTS); - return -EINVAL; - } - - if (!t500rs->send_buffer) { - hid_err(t500rs->hdev, "t500rs_stop_effect: NULL send buffer\n"); - return -ENOMEM; - } - - hw_id = t500rs_logical_to_hw_id(state->effect.id); - - /* STOP command uses hw_id as effect_id to match 0x01 packet */ - return t500rs_send_stop(t500rs, (u8)hw_id); + const struct tmff2_effect_state *state) +{ + struct t500rs_device_entry *t500rs = data; + int hw_id; + + /* Validate effect ID range */ + if (state->effect.id >= T500RS_MAX_EFFECTS) { + hid_err(t500rs->hdev, "Effect ID %d exceeds maximum %d\n", + state->effect.id, T500RS_MAX_EFFECTS); + return -EINVAL; + } + + if (!t500rs->send_buffer) { + hid_err(t500rs->hdev, "t500rs_stop_effect: NULL send buffer\n"); + return -ENOMEM; + } + + hw_id = t500rs_logical_to_hw_id(state->effect.id); + + /* STOP command uses hw_id as effect_id to match 0x01 packet */ + return t500rs_send_stop(t500rs, (u8)hw_id); } /* @@ -1224,409 +1317,449 @@ static int t500rs_stop_effect(void *data, * of these fields. */ 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; - int hw_id; - - if (!t500rs) - return -ENODEV; - - buf = t500rs->send_buffer; - if (!buf) - return -ENOMEM; - - hw_id = t500rs_logical_to_hw_id(effect->id); - - switch (effect->type) { - case FF_CONSTANT: { - int level = effect->u.constant.level; - u16 direction = effect->direction; - - if (level == old->u.constant.level && direction == old->direction) - return 0; - - s8 signed_level = t500rs_scale_const_with_direction(level, direction); - u16 param_sub, env_sub; - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; - t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), signed_level); - return t500rs_send_hid(t500rs, (u8 *)r3, sizeof(*r3)); - } - - case FF_PERIODIC: { - /* Skip update if parameters unchanged */ - if (effect->u.periodic.magnitude == old->u.periodic.magnitude && - effect->u.periodic.offset == old->u.periodic.offset && - effect->u.periodic.phase == old->u.periodic.phase && - effect->u.periodic.period == old->u.periodic.period && - effect->direction == old->direction) - return 0; - - /* Apply direction projection to magnitude and adjust phase if needed */ - u16 phase_raw = effect->u.periodic.phase; - u8 mag = t500rs_scale_periodic_with_direction(effect->u.periodic.magnitude, - effect->direction, &phase_raw); - u8 phase = t500rs_scale_periodic_phase(phase_raw); - u8 offset = t500rs_scale_periodic_offset(effect->u.periodic.offset); - u16 period_ms = effect->u.periodic.period; - u16 param_sub, env_sub; - - if (period_ms == 0) { - hid_err(t500rs->hdev, "Periodic effect period cannot be zero\n"); - return -EINVAL; - } - - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, offset, phase, - period_ms); - return t500rs_send_hid(t500rs, buf, sizeof(*p)); - } - - case FF_RAMP: { - /* Skip update if parameters unchanged */ - if (effect->u.ramp.start_level == old->u.ramp.start_level && - effect->u.ramp.end_level == old->u.ramp.end_level && - effect->replay.length == old->replay.length) - return 0; - - u16 duration_ms = effect->replay.length; - if (duration_ms == 0) { - hid_err(t500rs->hdev, "Ramp effect duration cannot be zero\n"); - return -EINVAL; - } - - u16 param_sub, env_sub; - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), effect->u.ramp.start_level, - effect->u.ramp.end_level, duration_ms); - - return t500rs_send_hid(t500rs, buf, sizeof(*p)); - } - - case FF_SPRING: - case FF_DAMPER: - case FF_FRICTION: - case FF_INERTIA: { - /* + 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; + int hw_id; + + if (!t500rs) + return -ENODEV; + + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + + hw_id = t500rs_logical_to_hw_id(effect->id); + + switch (effect->type) { + case FF_CONSTANT: { + int level = effect->u.constant.level; + u16 direction = effect->direction; + + if (level == old->u.constant.level && + direction == old->direction) + return 0; + + s8 signed_level = + t500rs_scale_const_with_direction(level, direction); + u16 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; + t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), + signed_level); + return t500rs_send_hid(t500rs, (u8 *)r3, sizeof(*r3)); + } + + case FF_PERIODIC: { + /* Skip update if parameters unchanged */ + if (effect->u.periodic.magnitude == old->u.periodic.magnitude && + effect->u.periodic.offset == old->u.periodic.offset && + effect->u.periodic.phase == old->u.periodic.phase && + effect->u.periodic.period == old->u.periodic.period && + effect->direction == old->direction) + return 0; + + /* Apply direction projection to magnitude and adjust phase if needed */ + u16 phase_raw = effect->u.periodic.phase; + u8 mag = t500rs_scale_periodic_with_direction( + effect->u.periodic.magnitude, effect->direction, + &phase_raw); + u8 phase = t500rs_scale_periodic_phase(phase_raw); + u8 offset = + t500rs_scale_periodic_offset(effect->u.periodic.offset); + u16 period_ms = effect->u.periodic.period; + u16 param_sub, env_sub; + + if (period_ms == 0) { + hid_err(t500rs->hdev, + "Periodic effect period cannot be zero\n"); + return -EINVAL; + } + + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, + offset, phase, period_ms); + return t500rs_send_hid(t500rs, buf, sizeof(*p)); + } + + case FF_RAMP: { + /* Skip update if parameters unchanged */ + if (effect->u.ramp.start_level == old->u.ramp.start_level && + effect->u.ramp.end_level == old->u.ramp.end_level && + effect->replay.length == old->replay.length) + return 0; + + u16 duration_ms = effect->replay.length; + if (duration_ms == 0) { + hid_err(t500rs->hdev, + "Ramp effect duration cannot be zero\n"); + return -EINVAL; + } + + u16 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + struct t500rs_pkt_r04_periodic_ramp *p = + (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), + effect->u.ramp.start_level, + effect->u.ramp.end_level, duration_ms); + + return t500rs_send_hid(t500rs, buf, sizeof(*p)); + } + + case FF_SPRING: + case FF_DAMPER: + case FF_FRICTION: + case FF_INERTIA: { + /* * Skip update if parameters unchanged - prevents micro-pulse/rumble * when games spam identical condition updates. */ - const struct ff_condition_effect *cond = &effect->u.condition[0]; - const struct ff_condition_effect *cond_old = &old->u.condition[0]; - u16 param_sub, env_sub; - - if (cond->right_coeff == cond_old->right_coeff && - cond->left_coeff == cond_old->left_coeff && - cond->right_saturation == cond_old->right_saturation && - cond->left_saturation == cond_old->left_saturation && - cond->deadband == cond_old->deadband && - cond->center == cond_old->center && effect->type == old->type) - return 0; - - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) */ - u8 right_sat = (cond->right_saturation * 100) / 65535; - u8 left_sat = (cond->left_saturation * 100) / 65535; - - struct t500rs_pkt_r05_condition *p = (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), cond->right_coeff, - cond->left_coeff, right_sat, left_sat, cond->deadband, - cond->center); - return t500rs_send_hid(t500rs, buf, sizeof(*p)); - } - - default: - return -EOPNOTSUPP; - } + const struct ff_condition_effect *cond = + &effect->u.condition[0]; + const struct ff_condition_effect *cond_old = + &old->u.condition[0]; + u16 param_sub, env_sub; + + if (cond->right_coeff == cond_old->right_coeff && + cond->left_coeff == cond_old->left_coeff && + cond->right_saturation == cond_old->right_saturation && + cond->left_saturation == cond_old->left_saturation && + cond->deadband == cond_old->deadband && + cond->center == cond_old->center && + effect->type == old->type) + return 0; + + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) + */ + u8 right_sat = (cond->right_saturation * 100) / 65535; + u8 left_sat = (cond->left_saturation * 100) / 65535; + + struct t500rs_pkt_r05_condition *p = + (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), + cond->right_coeff, cond->left_coeff, + right_sat, left_sat, cond->deadband, + cond->center); + return t500rs_send_hid(t500rs, buf, sizeof(*p)); + } + + default: + return -EOPNOTSUPP; + } } /* Set autocenter */ -static int t500rs_set_autocenter(void *data, u16 autocenter) { - struct t500rs_device_entry *t500rs = data; - u8 *buf; - int ret; - u8 autocenter_percent; +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; + if (!t500rs) + return -ENODEV; - autocenter_percent = (u8)((autocenter * 100) / 65535); + autocenter_percent = (u8)((autocenter * 100) / 65535); - /* Wine compatibility: Some games (e.g., LFS under Wine) set autocenter to + /* Wine compatibility: Some games (e.g., LFS under Wine) set autocenter to * 100%% at startup. That leaves a permanent strong - * centering force which masks/overpowers other forces. To avoid this, message the - * requests for the user to revert the gain value to expected value. */ - if (autocenter_percent >= 100) { - hid_warn(t500rs->hdev, - "Game might have set autocenter to 100%%, you might want to set it back to expected value using oversteer (or keep oversteer open) or system gain."); - } - - buf = t500rs->send_buffer; - if (!buf) - return -ENOMEM; - - /* Enable autocenter: Report 0x40 0x04 0x01 */ - { - struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)buf; - config->id = 0x40; - config->subcmd = 0x04; - config->data1 = 0x01; /* Enable */ - config->data2 = 0x00; - } - ret = t500rs_send_hid(t500rs, buf, 4); - if (ret) - return ret; - - /* Set autocenter strength: Report 0x40 0x03 [value] */ - { - struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)buf; - config->id = 0x40; - config->subcmd = 0x03; - config->data1 = autocenter_percent; /* 0-100 percentage */ - config->data2 = 0x00; - } - ret = t500rs_send_hid(t500rs, buf, 4); - if (ret) - return ret; - - /* Apply settings: Report 0x42 0x05 */ - buf[0] = 0x42; - buf[1] = 0x05; - ret = t500rs_send_hid(t500rs, buf, 2); - if (ret) - return ret; - - return 0; + * centering force which masks/overpowers other forces. To avoid this, message + * the requests for the user to revert the gain value to expected value. */ + if (autocenter_percent >= 100) { + hid_warn( + t500rs->hdev, + "Game might have set autocenter to 100%%, you might want to set " + "it back to expected value using oversteer (or keep oversteer " + "open) or system gain."); + } + + buf = t500rs->send_buffer; + if (!buf) + return -ENOMEM; + + /* Enable autocenter: Report 0x40 0x04 0x01 */ + { + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x04; + config->data1 = 0x01; /* Enable */ + config->data2 = 0x00; + } + ret = t500rs_send_hid(t500rs, buf, 4); + if (ret) + return ret; + + /* Set autocenter strength: Report 0x40 0x03 [value] */ + { + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x03; + config->data1 = autocenter_percent; /* 0-100 percentage */ + config->data2 = 0x00; + } + ret = t500rs_send_hid(t500rs, buf, 4); + if (ret) + return ret; + + /* Apply settings: Report 0x42 0x05 */ + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_hid(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; - - /* Validate range - minimum 40 degrees, maximum 1080 degrees */ - if (range < T500RS_RANGE_MIN) - range = T500RS_RANGE_MIN; - - if (range > T500RS_RANGE_MAX) - range = T500RS_RANGE_MAX; - - /* Use preallocated buffer */ - buf = t500rs->send_buffer; - - T500RS_DBG(t500rs, "Setting wheel range to %u degrees\n", range); - - /* Device expects LITTLE-ENDIAN and value = range * 60. */ - range_value = range * 60; - - /* Send Report 0x40 0x11 [value_lo] [value_hi] to set range */ - { - struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)buf; - config->id = 0x40; - config->subcmd = 0x11; - config->data1 = range_value & 0xFF; /* Low byte first (little-endian) */ - config->data2 = (range_value >> 8) & 0xFF; /* High byte second */ - } - - ret = t500rs_send_hid(t500rs, buf, 4); - if (ret) { - hid_err(t500rs->hdev, "Failed to send range command: %d\n", ret); - return ret; - } - - /* Apply settings with Report 0x42 0x05 */ - buf[0] = 0x42; - buf[1] = 0x05; - ret = t500rs_send_hid(t500rs, buf, 2); - if (ret) { - hid_err(t500rs->hdev, "Failed to apply range settings: %d\n", ret); - return ret; - } - - T500RS_DBG(t500rs, "Range set to %u degrees (final value=0x%04x)\n", range, - range_value); - - return 0; +static int t500rs_set_range(void *data, u16 range) +{ + struct t500rs_device_entry *t500rs = data; + u8 *buf; + int ret; + u16 range_value; + + /* Validate range - minimum 40 degrees, maximum 1080 degrees */ + if (range < T500RS_RANGE_MIN) + range = T500RS_RANGE_MIN; + + if (range > T500RS_RANGE_MAX) + range = T500RS_RANGE_MAX; + + /* Use preallocated buffer */ + buf = t500rs->send_buffer; + + T500RS_DBG(t500rs, "Setting wheel range to %u degrees\n", range); + + /* Device expects LITTLE-ENDIAN and value = range * 60. */ + range_value = range * 60; + + /* Send Report 0x40 0x11 [value_lo] [value_hi] to set range */ + { + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x11; + config->data1 = range_value & + 0xFF; /* Low byte first (little-endian) */ + config->data2 = (range_value >> 8) & + 0xFF; /* High byte second */ + } + + ret = t500rs_send_hid(t500rs, buf, 4); + if (ret) { + hid_err(t500rs->hdev, "Failed to send range command: %d\n", + ret); + return ret; + } + + /* Apply settings with Report 0x42 0x05 */ + buf[0] = 0x42; + buf[1] = 0x05; + ret = t500rs_send_hid(t500rs, buf, 2); + if (ret) { + hid_err(t500rs->hdev, "Failed to apply range settings: %d\n", + ret); + return ret; + } + + T500RS_DBG(t500rs, "Range set to %u degrees (final value=0x%04x)\n", + range, 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 = NULL; - u8 *init_buf; /* Will use send_buffer for transfers */ - int ret; - - /* Sanity check protocol main-upload packet size against documentation */ - BUILD_BUG_ON(sizeof(struct t500rs_pkt_r01_main) != 15); - - /* Validate input parameters */ - if (!tmff2) { - pr_err("t500rs_wheel_init: NULL tmff2 structure\n"); - return -EINVAL; - } - - if (!tmff2->hdev || !tmff2->input_dev) { - pr_err("t500rs_wheel_init: Invalid tmff2 structure" - " (missing hdev or input_dev)\n"); - return -EINVAL; - } - - hid_dbg(tmff2->hdev, "T500RS: Initializing HID mode\n"); - - /* Allocate device data */ - t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); - if (!t500rs) { - hid_err(tmff2->hdev, "Failed to allocate t500rs device structure\n"); - ret = -ENOMEM; - goto err_alloc; - } - - /* Initialize device structure */ - t500rs->hdev = tmff2->hdev; - t500rs->input_dev = tmff2->input_dev; - - /* Allocate send buffer */ - t500rs->buffer_length = T500RS_BUFFER_LENGTH; - - t500rs->send_buffer = kzalloc(t500rs->buffer_length, GFP_KERNEL); - if (!t500rs->send_buffer) { - hid_err(tmff2->hdev, "Failed to allocate send buffer (%zu bytes)\n", - t500rs->buffer_length); - ret = -ENOMEM; - goto err_buffer_alloc; - } - - /* Store device data in tmff2 BEFORE any operations that might fail */ - tmff2->data = t500rs; - - /* Use send_buffer for all HID transfers */ - init_buf = t500rs->send_buffer; - - T500RS_DBG(t500rs, "Sending initialization sequence...\n"); - - /* Report 0x42 - Init/status commands (2 bytes each) +static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) +{ + struct t500rs_device_entry *t500rs = NULL; + u8 *init_buf; /* Will use send_buffer for transfers */ + int ret; + + /* Sanity check protocol main-upload packet size against documentation */ + BUILD_BUG_ON(sizeof(struct t500rs_pkt_r01_main) != 15); + + /* Validate input parameters */ + if (!tmff2) { + pr_err("t500rs_wheel_init: NULL tmff2 structure\n"); + return -EINVAL; + } + + if (!tmff2->hdev || !tmff2->input_dev) { + pr_err("t500rs_wheel_init: Invalid tmff2 structure" + " (missing hdev or input_dev)\n"); + return -EINVAL; + } + + hid_dbg(tmff2->hdev, "T500RS: Initializing HID mode\n"); + + /* Allocate device data */ + t500rs = kzalloc(sizeof(*t500rs), GFP_KERNEL); + if (!t500rs) { + hid_err(tmff2->hdev, + "Failed to allocate t500rs device structure\n"); + ret = -ENOMEM; + goto err_alloc; + } + + /* Initialize device structure */ + t500rs->hdev = tmff2->hdev; + t500rs->input_dev = tmff2->input_dev; + + /* Allocate send buffer */ + t500rs->buffer_length = T500RS_BUFFER_LENGTH; + + t500rs->send_buffer = kzalloc(t500rs->buffer_length, GFP_KERNEL); + if (!t500rs->send_buffer) { + hid_err(tmff2->hdev, + "Failed to allocate send buffer (%zu bytes)\n", + t500rs->buffer_length); + ret = -ENOMEM; + goto err_buffer_alloc; + } + + /* Store device data in tmff2 BEFORE any operations that might fail */ + tmff2->data = t500rs; + + /* Use send_buffer for all HID transfers */ + init_buf = t500rs->send_buffer; + + T500RS_DBG(t500rs, "Sending initialization sequence...\n"); + + /* Report 0x42 - Init/status commands (2 bytes each) * Windows sends these at startup: 0x42 0x04, 0x42 0x05, 0x42 0x00 * These appear to initialize the FFB subsystem state. */ - memset(init_buf, 0, 2); - init_buf[0] = 0x42; - init_buf[1] = 0x04; - ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) - hid_warn(t500rs->hdev, "Init command 0x42 0x04 failed: %d\n", ret); - - memset(init_buf, 0, 2); - init_buf[0] = 0x42; - init_buf[1] = 0x05; - ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) - hid_warn(t500rs->hdev, "Init command 0x42 0x05 failed: %d\n", ret); - - memset(init_buf, 0, 2); - init_buf[0] = 0x42; - init_buf[1] = 0x00; - ret = t500rs_send_hid(t500rs, init_buf, 2); - if (ret) - hid_warn(t500rs->hdev, "Init command 0x42 0x00 failed: %d\n", ret); - - /* Report 0x40 - Enable FFB (4 bytes) + memset(init_buf, 0, 2); + init_buf[0] = 0x42; + init_buf[1] = 0x04; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) + hid_warn(t500rs->hdev, "Init command 0x42 0x04 failed: %d\n", + ret); + + memset(init_buf, 0, 2); + init_buf[0] = 0x42; + init_buf[1] = 0x05; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) + hid_warn(t500rs->hdev, "Init command 0x42 0x05 failed: %d\n", + ret); + + memset(init_buf, 0, 2); + init_buf[0] = 0x42; + init_buf[1] = 0x00; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) + hid_warn(t500rs->hdev, "Init command 0x42 0x00 failed: %d\n", + ret); + + /* Report 0x40 - Enable FFB (4 bytes) * Magic value seen in captures that enables FFB on the base. */ - { - struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)init_buf; - config->id = 0x40; - config->subcmd = 0x11; - config->data1 = 0x42; - config->data2 = 0x7b; - } - ret = t500rs_send_hid(t500rs, init_buf, 4); - if (ret) - hid_warn(t500rs->hdev, "Init command 2 (0x40 enable) failed: %d\n", ret); - - /* Report 0x40 - Disable built-in autocenter (4 bytes) */ - { - struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)init_buf; - config->id = 0x40; - config->subcmd = 0x04; - // Keep explicit zeros even though memset() clears them. - config->data1 = 0x00; - config->data2 = 0x00; - } - ret = t500rs_send_hid(t500rs, init_buf, 4); - if (ret) - hid_warn(t500rs->hdev, "Init command 3 (0x40 config) failed: %d\n", ret); - - /* Report 0x43 - Set global gain (2 bytes) + { + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)init_buf; + config->id = 0x40; + config->subcmd = 0x11; + config->data1 = 0x42; + config->data2 = 0x7b; + } + ret = t500rs_send_hid(t500rs, init_buf, 4); + if (ret) + hid_warn(t500rs->hdev, + "Init command 2 (0x40 enable) failed: %d\n", ret); + + /* Report 0x40 - Disable built-in autocenter (4 bytes) */ + { + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)init_buf; + config->id = 0x40; + config->subcmd = 0x04; + // Keep explicit zeros even though memset() clears them. + config->data1 = 0x00; + config->data2 = 0x00; + } + ret = t500rs_send_hid(t500rs, init_buf, 4); + if (ret) + hid_warn(t500rs->hdev, + "Init command 3 (0x40 config) failed: %d\n", ret); + + /* 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_hid(t500rs, init_buf, 2); - if (ret) - hid_warn(t500rs->hdev, "Init command 4 (0x43) failed: %d\n", ret); + memset(init_buf, 0, 2); + init_buf[0] = 0x43; + init_buf[1] = 0xFF; + ret = t500rs_send_hid(t500rs, init_buf, 2); + if (ret) + hid_warn(t500rs->hdev, "Init command 4 (0x43) failed: %d\n", + ret); - hid_info(t500rs->hdev, "T500RS initialized successfully (HID mode)\n"); - T500RS_DBG(t500rs, "Buffer: %zu bytes\n", t500rs->buffer_length); + hid_info(t500rs->hdev, "T500RS initialized successfully (HID mode)\n"); + T500RS_DBG(t500rs, "Buffer: %zu bytes\n", 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)); + /* 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; + return 0; err_buffer_alloc: - /* t500rs structure is allocated but not yet stored in tmff2->data */ - kfree(t500rs); + /* t500rs structure is allocated but not yet stored in tmff2->data */ + kfree(t500rs); err_alloc: - return ret; + return ret; } /* Cleanup T500RS device */ -static int t500rs_wheel_destroy(void *data) { - struct t500rs_device_entry *t500rs = data; +static int t500rs_wheel_destroy(void *data) +{ + struct t500rs_device_entry *t500rs = data; - if (!t500rs) { - pr_warn("t500rs_wheel_destroy: NULL data pointer\n"); - return 0; - } + if (!t500rs) { + pr_warn("t500rs_wheel_destroy: NULL data pointer\n"); + return 0; + } - T500RS_DBG(t500rs, "T500RS: Cleaning up\n"); + T500RS_DBG(t500rs, "T500RS: Cleaning up\n"); - /* Free resources in reverse order of allocation */ - if (t500rs->send_buffer) { - kfree(t500rs->send_buffer); - t500rs->send_buffer = NULL; - } + /* Free resources in reverse order of allocation */ + if (t500rs->send_buffer) { + kfree(t500rs->send_buffer); + t500rs->send_buffer = NULL; + } - kfree(t500rs); + kfree(t500rs); - return 0; + return 0; } /* Populate API callbacks */ -int t500rs_populate_api(struct tmff2_device_entry *tmff2) { - - tmff2->play_effect = t500rs_play_effect; - tmff2->upload_effect = t500rs_upload_effect; - tmff2->update_effect = t500rs_update_effect; - tmff2->stop_effect = t500rs_stop_effect; +int t500rs_populate_api(struct tmff2_device_entry *tmff2) +{ + 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_gain = t500rs_set_gain; + tmff2->set_autocenter = t500rs_set_autocenter; + tmff2->set_range = t500rs_set_range; - tmff2->wheel_init = t500rs_wheel_init; - tmff2->wheel_destroy = t500rs_wheel_destroy; + tmff2->wheel_init = t500rs_wheel_init; + tmff2->wheel_destroy = t500rs_wheel_destroy; - return 0; + return 0; } diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index 0e4a4980..97193c7b 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -40,7 +40,7 @@ /* Effect type constants */ #define T500RS_EFFECT_CONSTANT 0x00 -#define T500RS_EFFECT_SQUARE 0x20 /* Discovered from FFEdit captures (Dec 2025) */ +#define T500RS_EFFECT_SQUARE 0x20 #define T500RS_EFFECT_SINE 0x22 #define T500RS_EFFECT_TRIANGLE 0x21 #define T500RS_EFFECT_SAW_UP 0x23 @@ -65,7 +65,7 @@ #define T500RS_GAIN_MAX 65535 /* Range limits */ -#define T500RS_RANGE_MIN 40 /* Minimum range: 40 degrees */ +#define T500RS_RANGE_MIN 40 /* Minimum range: 40 degrees */ #define T500RS_RANGE_MAX 1080 /* Maximum range: 1080 degrees */ /* @@ -75,15 +75,15 @@ * Used internally by the sequencing system to manage packet order. */ enum t500rs_seq_packet { - T500RS_SEQ_STOP, - T500RS_SEQ_SYNC_42_05, - T500RS_SEQ_SYNC_42_04, - T500RS_SEQ_ENVELOPE, - T500RS_SEQ_CONSTANT, - T500RS_SEQ_PERIODIC_RAMP, - T500RS_SEQ_CONDITION_X, - T500RS_SEQ_CONDITION_Y, - T500RS_SEQ_MAIN, + T500RS_SEQ_STOP, + T500RS_SEQ_SYNC_42_05, + T500RS_SEQ_SYNC_42_04, + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_CONSTANT, + T500RS_SEQ_PERIODIC_RAMP, + T500RS_SEQ_CONDITION_X, + T500RS_SEQ_CONDITION_Y, + T500RS_SEQ_MAIN, }; /* Supported effects */ @@ -118,16 +118,16 @@ extern const signed short t500rs_effects[]; * - b13-b14: reserved (0x0000) */ struct t500rs_pkt_r01_main { - u8 id; /* b0: T500RS_PKT_MAIN */ - u8 effect_id; /* b1: hardware effect slot ID (1-15) */ - u8 effect_type; /* b2: effect type (T500RS_EFFECT_*) */ - u8 control; /* b3: always T500RS_CONTROL_DEFAULT (0x40) */ - __le16 duration_ms; /* b4-b5: duration in ms (LE) */ - __le16 delay_ms; /* b6-b7: delay before start in ms (LE) */ - u8 reserved1; /* b8: 0x00 */ - __le16 packet_code_1; /* b9-b10: param subtype for 0x03/0x04/0x05 (LE) */ - __le16 packet_code_2; /* b11-b12: env subtype for 0x02 (LE) */ - __le16 reserved2; /* b13-b14: 0x0000 */ + u8 id; /* b0: T500RS_PKT_MAIN */ + u8 effect_id; /* b1: hardware effect slot ID (1-15) */ + u8 effect_type; /* b2: effect type (T500RS_EFFECT_*) */ + u8 control; /* b3: always T500RS_CONTROL_DEFAULT (0x40) */ + __le16 duration_ms; /* b4-b5: duration in ms (LE) */ + __le16 delay_ms; /* b6-b7: delay before start in ms (LE) */ + u8 reserved1; /* b8: 0x00 */ + __le16 packet_code_1; /* b9-b10: param subtype for 0x03/0x04/0x05 (LE) */ + __le16 packet_code_2; /* b11-b12: env subtype for 0x02 (LE) */ + __le16 reserved2; /* b13-b14: 0x0000 */ } __packed; /* @@ -149,13 +149,13 @@ struct t500rs_pkt_r01_main { * start/end levels. */ struct t500rs_pkt_r04_periodic_ramp { - u8 id; /* b0: T500RS_PKT_PERIODIC */ - u8 code; /* b1: subtype code (from 0x01 packet_code_1) */ - u8 reserved1; /* b2: always 0x00 */ - u8 magnitude; /* b3: 0..127 magnitude (scaled) */ - u8 offset; /* b4: signed -127..+127 offset (scaled) */ - u8 phase; /* b5: 0..255 phase (0-360 degrees) */ - __le16 period_ms; /* b6-b7: period in milliseconds (LE) */ + u8 id; /* b0: T500RS_PKT_PERIODIC */ + u8 code; /* b1: subtype code (from 0x01 packet_code_1) */ + u8 reserved1; /* b2: always 0x00 */ + u8 magnitude; /* b3: 0..127 magnitude (scaled) */ + u8 offset; /* b4: signed -127..+127 offset (scaled) */ + u8 phase; /* b5: 0..255 phase (0-360 degrees) */ + __le16 period_ms; /* b6-b7: period in milliseconds (LE) */ } __packed; /* @@ -179,50 +179,50 @@ struct t500rs_pkt_r04_periodic_ramp { * - Saturation: 0-100 (no scaling) */ struct t500rs_pkt_r05_condition { - u8 id; /* T500RS_PKT_CONDITIONAL */ - u8 code; /* from 0x01 code1/code2 */ - u8 reserved; /* Always 0x00 */ - u8 right_coeff; /* Right/positive coefficient (0-10 scale) */ - u8 left_coeff; /* Left/negative coefficient (0-10 scale) */ - __le16 center; /* Center offset (s16 LE, scaled by /65) */ - __le16 deadband; /* Deadband width (u16 LE, scaled by /65) */ - u8 right_sat; /* Right saturation (0-100) */ - u8 left_sat; /* Left saturation (0-100) */ + u8 id; /* T500RS_PKT_CONDITIONAL */ + u8 code; /* from 0x01 code1/code2 */ + u8 reserved; /* Always 0x00 */ + u8 right_coeff; /* Right/positive coefficient (0-10 scale) */ + u8 left_coeff; /* Left/negative coefficient (0-10 scale) */ + __le16 center; /* Center offset (s16 LE, scaled by /65) */ + __le16 deadband; /* Deadband width (u16 LE, scaled by /65) */ + u8 right_sat; /* Right saturation (0-100) */ + u8 left_sat; /* Left saturation (0-100) */ } __packed; /* 0x03 - Constant force level (4 bytes) */ struct t500rs_r03_const { - u8 id; /* T500RS_PKT_CONSTANT */ - u8 code; /* T500RS_CODE_CONSTANT */ - u8 zero; /* 0x00 */ - s8 level; /* -127..127 */ + u8 id; /* T500RS_PKT_CONSTANT */ + u8 code; /* T500RS_CODE_CONSTANT */ + u8 zero; /* 0x00 */ + s8 level; /* -127..127 */ } __packed; /* 0x41 - START/STOP command (4 bytes) */ 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 */ + u8 id; /* 0x41 */ + u8 effect_id; /* usually 0 on T500RS */ + u8 command; /* 0x41 START, 0x00 STOP, 0x00 clear in init */ + u8 arg; /* 0x01 */ } __packed; /* 0x02 - Envelope packet (9 bytes) */ struct t500rs_pkt_r02_envelope { - u8 id; /* 0x02 */ - u8 subtype; /* from 0x01 code2 (env_sub low byte) */ - __le16 attack_len; /* attack duration in ms */ - u8 attack_level; /* 0-255 */ - __le16 fade_len; /* fade duration in ms */ - u8 fade_level; /* 0-255 */ - u8 reserved; /* 0x00 */ + u8 id; /* 0x02 */ + u8 subtype; /* from 0x01 code2 (env_sub low byte) */ + __le16 attack_len; /* attack duration in ms */ + u8 attack_level; /* 0-255 */ + __le16 fade_len; /* fade duration in ms */ + u8 fade_level; /* 0-255 */ + u8 reserved; /* 0x00 */ } __packed; /* 0x40 - Configuration packet (4 bytes) */ struct t500rs_pkt_r40_config { - u8 id; /* 0x40 */ - u8 subcmd; /* subcommand */ - u8 data1; /* first data byte */ - u8 data2; /* second data byte */ + u8 id; /* 0x40 */ + u8 subcmd; /* subcommand */ + u8 data1; /* first data byte */ + u8 data2; /* second data byte */ } __packed; #endif /* __HID_TMT500RS_H */ From e8d9823c0683c0bc91c1dcdedf890003d43fc533 Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Fri, 30 Jan 2026 23:00:14 +0100 Subject: [PATCH 14/15] docs(T500RS): Address review comments - improve flow, terminology, and examples Major changes: - Add QUICK START section for immediate practical learning - Add Understanding Effects section with real-world analogies - Reorganize documentation: practice-first, reference-later - Standardize terminology: param_sub/env_sub instead of packet_code - Fix degrees spacing and improve parameter descriptions - Add consistent example format with packet-by-packet breakdowns - Fix code comment formatting to kernel style - Remove redundant & 0xff casts when casting to u8 The cast to u8 already truncates to 8 bits, making & 0xff redundant - Fix SDL references to Linux FFB subsystem in header file comments - Verify no unicode characters in source files - Remove unnecessary braces - Added helper functions for build&send packets --- docs/T500RS_FFBEFFECTS.md | 185 ++++++++---- src/tmt500rs/hid-tmt500rs.c | 571 ++++++++++++++++++++++-------------- src/tmt500rs/hid-tmt500rs.h | 6 +- 3 files changed, 472 insertions(+), 290 deletions(-) diff --git a/docs/T500RS_FFBEFFECTS.md b/docs/T500RS_FFBEFFECTS.md index 7ca198d0..e012bdc7 100644 --- a/docs/T500RS_FFBEFFECTS.md +++ b/docs/T500RS_FFBEFFECTS.md @@ -14,49 +14,50 @@ All values are little-endian unless specified otherwise. ### Device Overview The T500RS is a single-axis force feedback wheel with a rotating range that can be configured (typically 900 degrees or 1080 degrees). It uses a proprietary USB protocol for force feedback effects, distinct from the T300RS and other Thrustmaster wheels. -### Effect Playing and Stopping +### Understanding Force Feedback Effects -#### Playing an Effect -``` -41 00 41 01 - START command -``` -- **Packet Type:** 0x41 (Command) -- **Effect ID:** 0x00 (always 0x00 for T500RS) -- **Command:** 0x41 (START) -- **Argument:** 0x01 (play count, 0x01 = play once) +**What are force feedback effects?** +Force feedback effects are ways the wheel can apply physical resistance to your steering. Different effects simulate different real-world sensations: -#### Stopping an Effect -``` -41 00 00 01 - STOP command -``` -- **Packet Type:** 0x41 (Command) -- **Effect ID:** 0x00 (always 0x00 for T500RS) -- **Command:** 0x00 (STOP) -- **Argument:** 0x01 (stop parameter) - -### USB Packet Types Overview - -| Packet ID | Name | Size | Purpose | -|-----------|------|------|---------| -| 0x01 | Main Upload | 15 bytes | Effect type, direction, duration, packet codes | -| 0x02 | Envelope | 9 bytes | Attack/fade parameters (limited support) | -| 0x03 | Constant Force | 4 bytes | Force level for constant effects | -| 0x04 | Periodic/Ramp | 8 bytes | Magnitude, offset, phase, period | -| 0x05 | Conditional | 11 bytes | Spring/damper/inertia/friction parameters | -| 0x41 | Command | 4 bytes | START (0x41) / STOP (0x00) | -| 0x42 | Status/Control | 2-16 bytes | Device status queries (0x00, 0x04, 0x05) | -| 0x43 | Init/Config | 64 bytes | Device initialization | -| 0x49 | Polling | 7-16 bytes | Status polling (high frequency) | -| 0x07 | Telemetry | 15 bytes | Position feedback (high frequency) | - -**Important Notes:** -- **Envelope Support:** Envelope packets (0x02) have limited support. Non-zero envelope values cause EPROTO errors on periodic and constant effects. Always send zeros for envelope parameters on these effect types. -- **Runtime Updates:** Effect updates (via `update_effect` callback) only modify parameter-specific packets (0x03, 0x04, 0x05). Duration and delay changes require re-uploading the entire effect. -- **Effect Indexing:** The T500RS uses a unique subtype system for effect indexing. See the [Subtype System and Effect Indexing](#subtype-system-and-effect-indexing) section for details. +| Effect Type | Real-World Analogy | Use In Games | +|-------------|---------------------|--------------| +| **Constant Force** | Constant push/pull in one direction | Sustained force from collision, wind, road surface | +| **Periodic (Sine)** | Smooth vibration that oscillates | Engine rumble, gravel road texture | +| **Periodic (Square)** | Sharp on/off vibration | Hitting rumble strips, driving over grass | +| **Spring** | Wheel pulls back to center | Self-centering feeling, returning to straight | +| **Damper** | Viscous resistance when moving | Wheel gets heavier at high speeds | +| **Inertia** | Resistance to CHANGING direction | Simulates weight of the car | + +**How effects are created** (in simple terms): +1. **Main packet** (0x01): Describes the effect - type, duration, identifies which packets follow +2. **Parameter packets** (0x03, 0x04, 0x05): Set specific values - force level, vibration speed, spring strength +3. **Command packet** (0x41): START or STOP the effect + +The T500RS uses a unique "subtype system" where each effect gets a unique ID that helps the device match parameter packets to the right effect. Think of it like a mailbox number - each effect has its own mailbox for parameter updates. --- +## EFFECT EXAMPLES BY TYPE + +** NEW TO THIS DOCUMENT? START HERE:** + +1. **[Quick Start](#quick-start)** (above): Create your first effect in 3 simple steps +2. **[Understanding Effects](#understanding-effects)** (above): Learn what each effect type does +3. **[Complete Examples](#effect-examples)** (below): See working captures for each effect type with detailed breakdowns -## Packet Structure Details +** REFERENCE SECTIONS** (for deep dives): +- [Packet Structure Details](#packet-structure) - Detailed packet format reference +- [Common Pitfalls](#pitfalls) - Implementation tips and gotchas +- [Subtype System](#subtype-system) - Effect indexing deep dive +- [Parameter Encoding](#encoding-reference) - Value conversion formulas + +--- + +This section shows complete working examples for each effect type captured from actual USB traffic. Each example includes: +- The complete hexadecimal packet sequence +- Step-by-step breakdown of what each packet does +- Practical explanations of the values and their effect + +Start with these examples to understand how effects are created in practice, then consult the [Reference Sections](#reference-sections) below for detailed protocol information. ### 0x01 - Main Upload Packet (15 bytes) ``` @@ -69,8 +70,8 @@ Offset | Size | Field | Description 4 | 2 | duration_ms | Duration in milliseconds, little-endian 6 | 2 | delay_ms | Delay before start, little-endian 8 | 1 | reserved1 | 0x00 - 9 | 2 | packet_code_1 | Code for subsequent packet type (variable!) -11 | 2 | packet_code_2 | Code for second subsequent packet (variable!) + 9 | 2 | param_sub | Parameter subtype for 0x03/0x04/first 0x05 packet (variable!) + 11 | 2 | env_sub | Envelope subtype for 0x02/second 0x05 packet (variable!) 13 | 2 | reserved2 | 0x0000 ``` @@ -90,7 +91,7 @@ Offset | Size | Field | Description **Note:** Square wave (0x20) was discovered in FFEdit captures. The Windows driver may not expose this effect type through the standard API. -**IMPORTANT:** Bytes 9-12 specify the packet codes used in subsequent packets. These are NOT fixed values! +**IMPORTANT:** Bytes 9-12 specify the subtype codes (param_sub and env_sub) used in subsequent packets. These are NOT fixed values! **Common Code Combinations:** - Constant effects: bytes 9-10 = 0x000e (for 0x03 packet), bytes 11-12 = 0x001c (envelope) @@ -99,6 +100,9 @@ Offset | Size | Field | Description - Alternative codes observed: 0x00b6/0x00c4 (newer captures), 0x0046/0x0054, 0x0062/0x0070, 0x007e/0x008c, 0x009a/0x00a8 **Examples:** + +> **NOTE:** Effect IDs in examples are hardware slot IDs (1-15) assigned by the driver. The driver maps logical effect IDs (0-14) to hardware IDs (1-15). Examples show typical values. See the [Subtype System and Effect Indexing](#subtype-system-and-effect-indexing) section for details on how hardware IDs are allocated. + - `01 01 00 40 f4 01 00 00 0e 00 1c 00 00 00` - Constant effect with envelope - Effect ID: 0x01 (hardware slot 1, logical 0) - Effect type: 0x00 (constant) @@ -106,7 +110,8 @@ Offset | Size | Field | Description - Duration: 0x01f4 = 500ms - Delay: 0x0000 = 0ms - Reserved1: 0x00 - - Packet codes: 0x000e (constant), 0x001c (envelope) + - Subtype codes: param_sub=0x000e (constant), env_sub=0x001c (envelope) + - Note: Constant effects use fixed subtype 0x000e/0x001c regardless of hw_id - Reserved2: 0x0000 - `01 01 40 40 d0 07 00 00 2a 00 38 00 00 00` - Conditional effect @@ -116,7 +121,8 @@ Offset | Size | Field | Description - Duration: 0x07d0 = 2000ms - Delay: 0x0000 = 0ms - Reserved1: 0x00 - - Packet codes: 0x002a (first conditional), 0x0038 (second conditional) + - Subtype codes: param_sub=0x002a (first conditional), env_sub=0x0038 (second conditional) + - Calculation: hw_id=1, param_sub=0x000e+0x001c*1=0x002a, env_sub=0x001c+0x001c*1=0x0038 - Reserved2: 0x0000 ### 0x02 - Envelope Packet (9 bytes) @@ -178,8 +184,8 @@ Offset | Size | Field | Description **Examples:** - `04 2a 00 00 00 0a 00 00` - Code 0x2a, magnitude 0, period 10ms -- `04 2a 06 00 3f 0a 00 00` - Code 0x2a, magnitude 6, phase 63 (88.6degrees), period 10ms -- `04 2a 09 00 7f 64 00 00` - Code 0x2a, magnitude 9, phase 127 (178.6degrees), period 100ms +- `04 2a 06 00 3f 0a 00 00` - Code 0x2a, magnitude 6, phase 63 (88.6 degrees), period 10ms +- `04 2a 09 00 7f 64 00 00` - Code 0x2a, magnitude 9, phase 127 (178.6 degrees), period 100ms - `04 b6 00 00 7f 00 00 00` - Code 0xb6, magnitude 0, phase 127 (ramp effect) ### 0x05 - Conditional Effect Packet (11 bytes) @@ -234,11 +240,38 @@ Offset | Size | Field | Description ## Effect Type Implementation Table +This section shows complete working examples for each effect type captured from actual USB traffic. + +**Example Format:** +- **Complete packet sequence**: Shows all packets in hexadecimal +- **Packet breakdown**: Explains what each packet does and what the values mean +- **Additional examples**: Shows variations (different parameters, magnitudes, etc.) + +All examples follow this consistent format to help you understand both the protocol and practical usage. + +--- + ### 1. CONSTANT FORCE EFFECTS -**Capture Examples:** -- Zero force: `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `03 0e 00 00` -- Low positive force: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 06 00 00 06 00` `03 0e 00 03` +**Example: Zero Force (No force applied)** + +Complete packet sequence: `01 00 00 40 f4 01 00 00 0e 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `03 0e 00 00` + +**Packet breakdown:** +- **0x01 (Main Upload)**: Effect ID 0x00, type 0x00 (constant), duration 500ms, delay 0ms, subtypes 0x000e/0x001c +- **0x02 (Envelope)**: Subtype 0x001c, 0ms attack/fade, level 0 +- **0x03 (Constant Force)**: Subtype 0x000e, level 0 (no force) + +**Example: Low Positive Force (Weak force in one direction)** + +Complete packet sequence: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 06 00 00 06 00` `03 0e 00 03` + +**Packet breakdown:** +- **0x01 (Main Upload)**: Effect ID 0x00, type 0x00 (constant), duration 2000ms, delay 0ms, subtypes 0x000e/0x001c +- **0x02 (Envelope)**: Subtype 0x001c, 0ms attack/fade, level 6 (slight ramp) +- **0x03 (Constant Force)**: Subtype 0x000e, level 3 (weak positive force) + +**Additional Capture Examples:** - Medium force: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 12 00 00 12 00` `03 0e 00 09` - High negative force: `01 00 00 40 88 13 00 00 0e 00 1c 00 00 00` `02 1c 00 00 0d 00 00 0d 00` `03 0e 00 f9` - Maximum force with direction: `01 00 00 40 d0 07 00 00 0e 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `03 0e 00 00` @@ -258,10 +291,18 @@ Offset | Size | Field | Description ### 2. PERIODIC EFFECTS - SINE WAVE -**Capture Examples:** +**Example: Medium Magnitude Sine Wave** + +Complete packet sequence: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 12 00 00 12 00` `04 2a 09 00 7f 64 00 00` + +**Packet breakdown:** +- **0x01 (Main Upload)**: Effect ID 0x00, type 0x22 (sine), duration 2000ms, delay 0ms, subtypes 0x002a/0x001c +- **0x02 (Envelope)**: Subtype 0x001c, attack/fade 18ms, level 18 (medium ramp) +- **0x04 (Periodic)**: Subtype 0x002a, magnitude 9, offset 0, phase 127 (178.6 degrees), period 100ms + +**Additional Capture Examples:** - Zero magnitude: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 00 00 00 00 00` `04 2a 00 00 00 0a 00 00` - Low magnitude with phase: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 06 00 00 06 00` `04 2a 06 00 3f 0a 00 00` -- Medium magnitude: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c 00 00 12 00 00 12 00` `04 2a 09 00 7f 64 00 00` - With envelope: `01 00 22 40 d0 07 00 00 2a 00 1c 00 00 00` `02 1c f4 01 12 f4 01 12 00` `04 2a 09 00 00 64 00 00` **Packet Structure:** @@ -273,9 +314,9 @@ Offset | Size | Field | Description **Parameter Details:** - Magnitude: 0-127 (scaled from Linux 0-32767) -- Offset: s8 (-128 to +127, DC bias (Direct Current bias - a constant force offset), scaled from Linux -32768 to +32767) +- Offset: s8 (-128 to +127, constant force offset that shifts the waveform up or down, scaled from Linux -32768 to +32767) - Phase: 0-255 (256 steps for 360 degrees, scaled from Linux 0-35999) -- Period: Direct milliseconds (no Hz conversion!) +- Period: Direct milliseconds - Direction: Applied during magnitude scaling (projection onto wheel axis) ### 3. PERIODIC EFFECTS - TRIANGLE WAVE @@ -310,7 +351,7 @@ Offset | Size | Field | Description - effect_type = 0x24 (sawtooth down) - Same envelope and periodic packet structure as sine wave -**Note:** Offset field allows DC bias (Direct Current bias - a constant force offset) - useful for asymmetric waveforms like sawtooth where you want to shift the entire waveform up or down. This creates a net force in one direction over time. +**Note:** Offset field allows shifting the waveform up or down by adding a constant force. Useful for asymmetric waveforms like sawtooth to create a net force in one direction over time. ### 6. RAMP EFFECTS @@ -335,8 +376,16 @@ Offset | Size | Field | Description ### 7. CONDITIONAL EFFECTS - SPRING -**Capture Examples:** -- Basic spring with low coefficients: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 00 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` +**Example: Basic Spring Effect** + +Complete packet sequence: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 00 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` + +**Packet breakdown:** +- **0x01 (Main Upload)**: Effect ID 0x00, type 0x40 (spring), duration 2000ms, delay 0ms, subtypes 0x002a/0x0038 +- **0x05 (First - X-axis)**: Subtype 0x002a, coeffs 0/0, center 0, deadband 0, saturation 100/100 +- **0x05 (Second - Y-axis)**: Subtype 0x0038, coeffs 0/0, center 0, deadband 0, saturation 100/100 (unused for single-axis) + +**Additional Capture Examples:** - Spring with deadband: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 00 00 07 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` - Asymmetric spring: `01 00 40 40 d0 07 00 00 2a 00 38 00 00 00` `05 2a 00 00 00 99 00 4c 00 54 54` `05 38 00 00 00 00 00 00 00 54 54` @@ -413,6 +462,12 @@ The driver will send non-zero coefficients for inertia effects if they are provi --- +## REFERENCE SECTIONS + +The following sections provide detailed protocol information for advanced readers. For beginners, we recommend starting with the [Quick Start](#quick-start) and [Effect Examples](#effect-examples) sections above. + +--- + ## COMMON PITFALLS AND IMPLEMENTATION TIPS ### Effect Indexing @@ -486,10 +541,10 @@ Envelope attack/fade length and level values live **only** in the 0x02 packets; - **Device Format:** 16-bit little-endian (0-35999 in 0.01 degree units) - **Conversion:** `device_dir = (os_ffb_dir * 36000) / 65536` - **Examples:** - - 0degrees = 0x0000 - - 90degrees = 0x2328 (9000 decimal) - - 180degrees = 0x4650 (18000 decimal) - - 270degrees = 0x6978 (27000 decimal) + - 0 degrees = 0x0000 + - 90 degrees = 0x2328 (9000 decimal) + - 180 degrees = 0x4650 (18000 decimal) + - 270 degrees = 0x6978 (27000 decimal) ### Duration Encoding - **Linux FFB Format:** Milliseconds @@ -522,14 +577,14 @@ Envelope attack/fade length and level values live **only** in the 0x02 packets; - Linux 32767 -> Device 127 ### Phase Encoding (Periodic) -- **Linux FFB Format:** 0-35999 (0.01 degree units, 0-359.99degrees) -- **Device Format:** 0-255 (256 steps for 360degrees) +- **Linux FFB Format:** 0-35999 (0.01 degree units, 0-359.99 degrees) +- **Device Format:** 0-255 (256 steps for 360 degrees) - **Conversion:** `device_phase = (os_ffb_phase * 256) / 36000` - **Examples:** - - 0degrees (0) -> 0x00 - - 90degrees (9000) -> 0x40 (64) - - 180degrees (18000) -> 0x80 (128) - - 270degrees (27000) -> 0xC0 (192) + - 0 degrees (0) -> 0x00 + - 90 degrees (9000) -> 0x40 (64) + - 180 degrees (18000) -> 0x80 (128) + - 270 degrees (27000) -> 0xC0 (192) ### Period Encoding (Periodic) - **Linux FFB Format:** Milliseconds diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index 5d5ef8b9..fc22323a 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -20,8 +20,12 @@ static const enum t500rs_seq_packet t500rs_seq_constant[] = { }; static const enum t500rs_seq_packet t500rs_seq_periodic[] = { - T500RS_SEQ_STOP, T500RS_SEQ_SYNC_42_05, T500RS_SEQ_SYNC_42_04, - T500RS_SEQ_ENVELOPE, T500RS_SEQ_PERIODIC_RAMP, T500RS_SEQ_MAIN, + T500RS_SEQ_STOP, + T500RS_SEQ_SYNC_42_05, + T500RS_SEQ_SYNC_42_04, + T500RS_SEQ_ENVELOPE, + T500RS_SEQ_PERIODIC_RAMP, + T500RS_SEQ_MAIN, }; static const enum t500rs_seq_packet t500rs_seq_ramp[] = { @@ -237,9 +241,9 @@ static void t500rs_build_r04_periodic(struct t500rs_pkt_r04_periodic_ramp *p, u8 phase, u16 period_ms) { /* Byte order per Windows USB captures (example: 04 2a 00 06 00 3f 0a 00): - * b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset, - * b5=phase, b6-b7=period - */ + * b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset, + * b5=phase, b6-b7=period + */ memset(p, 0, sizeof(*p)); p->id = T500RS_PKT_PERIODIC; /* b0 */ p->code = code; /* b1 */ @@ -357,19 +361,19 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, offset = (s8)((end_level - start_level) / 512); /* - * Phase encodes ramp direction per FFEdit captures: - * - Positive ramp (start < end): phase = 0x7f (127) - * - Negative ramp (start > end): phase = 0x00 - * - Equal levels: treat as positive (neutral case) - * - * Example captures: - * - 049a0000007f0000 - phase 0x7f = positive/up direction - * - 049a000c00000000 - phase 0x00 = negative/down direction - */ + * Phase encodes ramp direction per FFEdit captures: + * - Positive ramp (start < end): phase = 0x7f (127) + * - Negative ramp (start > end): phase = 0x00 + * - Equal levels: treat as positive (neutral case) + * + * Example captures: + * - 049a0000007f0000 - phase 0x7f = positive/up direction + * - 049a000c00000000 - phase 0x00 = negative/down direction + */ phase = (start_level < end_level) ? 0x7f : 0x00; /* Byte order per USB captures: b0=id, b1=code, b2=reserved1, b3=mag, - * b4=offset, b5=phase, b6-b7=period */ + * b4=offset, b5=phase, b6-b7=period */ p->id = 0x04; /* b0 */ p->code = code; /* b1 */ p->reserved1 = 0; /* b2: always 0x00 */ @@ -379,6 +383,27 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, p->period_ms = cpu_to_le16(duration_ms); /* b6-b7 */ } +/* Saturation scaling constants */ +#define T500RS_SATURATION_DEVICE_MAX 100 +#define T500RS_SATURATION_LINUX_MAX 65535 + +/** + * t500rs_scale_saturation - Scale saturation from Linux FFB to device range + * @saturation: Linux FFB saturation value (0-65535) + * + * Returns: Scaled saturation value (0-100) + * + * Uses 32-bit arithmetic to prevent overflow and ensures accurate scaling. + * The result is clamped to 0-100 range. + */ +static inline u8 t500rs_scale_saturation(u16 saturation) +{ + return (u8)min_t(u32, + ((u32)saturation * T500RS_SATURATION_DEVICE_MAX) / + T500RS_SATURATION_LINUX_MAX, + T500RS_SATURATION_DEVICE_MAX); +} + /* * Build a 0x05 conditional effect packet. * @@ -419,6 +444,230 @@ static void t500rs_build_r05_condition(struct t500rs_pkt_r05_condition *p, p->left_sat = left_sat; } +/* + * Build and send a 0x05 conditional effect packet. + * + * This helper function encapsulates the common pattern of building and + * sending a condition (spring/damper/friction/inertia) packet, reducing + * code duplication and improving maintainability. + * + * Parameters: + * - t500rs: Device context + * - buf: Buffer to use for packet construction + * - code: Packet code (from param_sub or env_sub) + * - cond: Condition effect parameters + * + * Returns: 0 on success, negative errno on failure + */ +static int t500rs_send_condition_packet(struct t500rs_device_entry *t500rs, + u8 *buf, u8 code, + const struct ff_condition_effect *cond) +{ + struct t500rs_pkt_r05_condition *p; + + if (!t500rs || !buf || !cond) + return -EINVAL; + + /* Scale saturation from Linux FFB range to device range */ + u8 right_sat = t500rs_scale_saturation(cond->right_saturation); + u8 left_sat = t500rs_scale_saturation(cond->left_saturation); + + /* Build and send the condition packet */ + p = (struct t500rs_pkt_r05_condition *)buf; + t500rs_build_r05_condition(p, code, cond->right_coeff, cond->left_coeff, + right_sat, left_sat, cond->deadband, + cond->center); + + return t500rs_send_hid(t500rs, buf, + sizeof(struct t500rs_pkt_r05_condition)); +} + +/* + * Build and send a 0x03 constant force packet. + * + * This helper function encapsulates the common pattern of building and + * sending a constant force packet, reducing code duplication and improving + * maintainability. Handles level scaling with direction projection. + * + * Parameters: + * - t500rs: Device context + * - buf: Buffer to use for packet construction + * - code: Packet code (from param_sub) + * - level: Constant force level (-32767 to 32767) + * - direction: Effect direction (0-65535) + * + * Returns: 0 on success, negative errno on failure + */ +static int t500rs_send_constant_packet(struct t500rs_device_entry *t500rs, + u8 *buf, u8 code, + s16 level, u16 direction) +{ + struct t500rs_r03_const *r3; + s8 scaled_level; + + if (!t500rs || !buf) + return -EINVAL; + + /* Scale level with direction projection */ + scaled_level = t500rs_scale_const_with_direction(level, direction); + + /* Build and send packet */ + r3 = (struct t500rs_r03_const *)buf; + t500rs_build_r03_constant(r3, code, scaled_level); + + return t500rs_send_hid(t500rs, buf, sizeof(*r3)); +} + +/* + * Build and send a 0x04 periodic effect packet. + * + * This helper function encapsulates the common pattern of building and + * sending a periodic effect packet, reducing code duplication and improving + * maintainability. Handles magnitude scaling with direction projection, + * phase adjustment, and period validation. + * + * Parameters: + * - t500rs: Device context + * - buf: Buffer to use for packet construction + * - code: Packet code (from param_sub) + * - periodic: Periodic effect parameters + * - direction: Effect direction (0-65535) + * + * Returns: 0 on success, negative errno on failure + */ +static int t500rs_send_periodic_packet(struct t500rs_device_entry *t500rs, + u8 *buf, u8 code, + const struct ff_periodic_effect *periodic, + u16 direction) +{ + struct t500rs_pkt_r04_periodic_ramp *p; + u16 phase_raw; + u8 mag, phase; + s8 offset; + u16 period_ms; + + if (!t500rs || !buf || !periodic) + return -EINVAL; + + /* Validate period */ + period_ms = periodic->period; + if (period_ms == 0) { + hid_err(t500rs->hdev, + "Periodic effect period cannot be zero\n"); + return -EINVAL; + } + + /* Apply direction projection to magnitude and adjust phase */ + phase_raw = periodic->phase; + mag = t500rs_scale_periodic_with_direction( + periodic->magnitude, direction, &phase_raw); + phase = t500rs_scale_periodic_phase(phase_raw); + offset = t500rs_scale_periodic_offset(periodic->offset); + + /* Build and send packet */ + p = (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_periodic(p, code, mag, offset, phase, period_ms); + + return t500rs_send_hid(t500rs, buf, sizeof(*p)); +} + +/* + * Build and send a 0x04 ramp effect packet. + * + * This helper function encapsulates the common pattern of building and + * sending a ramp effect packet, reducing code duplication and improving + * maintainability. Ramp effects use the same 0x04 packet structure as + * periodic effects. + * + * Parameters: + * - t500rs: Device context + * - buf: Buffer to use for packet construction + * - code: Packet code (from param_sub) + * - ramp: Ramp effect parameters + * - duration_ms: Ramp duration in milliseconds + * + * Returns: 0 on success, negative errno on failure + */ +static int t500rs_send_ramp_packet(struct t500rs_device_entry *t500rs, + u8 *buf, u8 code, + const struct ff_ramp_effect *ramp, + u16 duration_ms) +{ + struct t500rs_pkt_r04_periodic_ramp *p; + + if (!t500rs || !buf || !ramp) + return -EINVAL; + + /* Validate duration */ + if (duration_ms == 0) { + hid_err(t500rs->hdev, + "Ramp effect duration cannot be zero\n"); + return -EINVAL; + } + + /* Build and send ramp packet */ + p = (struct t500rs_pkt_r04_periodic_ramp *)buf; + t500rs_build_r04_ramp(p, code, ramp->start_level, + ramp->end_level, duration_ms); + + return t500rs_send_hid(t500rs, buf, sizeof(*p)); +} + +/* + * Build and send a 0x02 envelope packet. + * + * This helper function encapsulates the common pattern of building and + * sending an envelope packet, reducing code duplication and improving + * maintainability. Determines envelope availability based on effect type. + * + * Per firmware behavior, only ramp effects support non-zero envelope values. + * Periodic and constant effects must send zero envelope values due to + * firmware limitations. + * + * Parameters: + * - t500rs: Device context + * - buf: Buffer to use for packet construction + * - subtype: Envelope subtype (from env_sub) + * - effect: Effect containing envelope parameters + * + * Returns: 0 on success, negative errno on failure + */ +static int t500rs_send_envelope_packet(struct t500rs_device_entry *t500rs, + u8 *buf, u8 subtype, + const struct ff_effect *effect) +{ + struct t500rs_pkt_r02_envelope *env; + const struct ff_envelope *envelope = NULL; + bool allow_envelope = false; + + if (!t500rs || !buf || !effect) + return -EINVAL; + + /* Determine envelope availability based on effect type */ + switch (effect->type) { + case FF_RAMP: + envelope = &effect->u.ramp.envelope; + allow_envelope = true; + break; + case FF_CONSTANT: + case FF_PERIODIC: + envelope = &effect->u.periodic.envelope; + allow_envelope = false; /* Firmware bug: must send zeros */ + break; + default: + /* No envelope for this effect type */ + envelope = NULL; + allow_envelope = false; + break; + } + + /* Build and send envelope packet */ + env = (struct t500rs_pkt_r02_envelope *)buf; + t500rs_build_r02_envelope(env, subtype, envelope, allow_envelope); + + return t500rs_send_hid(t500rs, buf, sizeof(*env)); +} + /* * Scale constant force level from Linux FFB subsystem format to device format. * @@ -491,13 +740,13 @@ static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, p->subtype = subtype; /* - * Per T500RS_EFFECTS.md, the device firmware rejects - * non-zero envelope values for periodic and constant effects with - * EPROTO (-71). Only ramp effects can safely use envelopes. - * - * Windows driver always sends zeros for periodic/constant: - * 02 38 00 00 00 00 00 00 00 - */ + * Per T500RS_EFFECTS.md, the device firmware rejects + * non-zero envelope values for periodic and constant effects with + * EPROTO (-71). Only ramp effects can safely use envelopes. + * + * Windows driver always sends zeros for periodic/constant: + * 02 38 00 00 00 00 00 00 00 + */ if (env && allow_nonzero) { p->attack_len = cpu_to_le16(env->attack_length); p->attack_level = @@ -591,101 +840,39 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, break; case T500RS_SEQ_ENVELOPE: { - struct t500rs_pkt_r02_envelope *env = - (struct t500rs_pkt_r02_envelope *)buf; - const struct ff_envelope *envelope = NULL; - bool allow_envelope = false; - - if (effect->type == FF_RAMP) { - envelope = &effect->u.ramp.envelope; - allow_envelope = - true; /* Ramp supports envelope */ - } else if (effect->type == FF_CONSTANT || - effect->type == FF_PERIODIC) { - envelope = &effect->u.periodic.envelope; - allow_envelope = - false; /* Firmware bug: must send zeros */ - } - - t500rs_build_r02_envelope(env, (u8)(env_sub & 0xff), - envelope, allow_envelope); - ret = t500rs_send_hid( - t500rs, buf, - sizeof(struct t500rs_pkt_r02_envelope)); + ret = t500rs_send_envelope_packet(t500rs, buf, + (u8)env_sub, effect); break; } case T500RS_SEQ_CONSTANT: { - s8 level = t500rs_scale_const_with_direction( - effect->u.constant.level, effect->direction); - struct t500rs_r03_const *r3 = - (struct t500rs_r03_const *)buf; - t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), - level); - ret = t500rs_send_hid(t500rs, buf, - sizeof(struct t500rs_r03_const)); + ret = t500rs_send_constant_packet(t500rs, buf, + (u8)param_sub, + effect->u.constant.level, + effect->direction); break; } case T500RS_SEQ_PERIODIC_RAMP: { if (effect->type == FF_RAMP) { - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *) - buf; - t500rs_build_r04_ramp( - p, (u8)(param_sub & 0xff), - effect->u.ramp.start_level, - effect->u.ramp.end_level, - effect->replay.length); + ret = t500rs_send_ramp_packet(t500rs, buf, + (u8)param_sub, + &effect->u.ramp, + effect->replay.length); } else { - /* Apply direction projection to magnitude and adjust phase if needed */ - u16 phase_raw = effect->u.periodic.phase; - u8 mag = t500rs_scale_periodic_with_direction( - effect->u.periodic.magnitude, - effect->direction, &phase_raw); - u8 phase = - t500rs_scale_periodic_phase(phase_raw); - s8 offset = t500rs_scale_periodic_offset( - effect->u.periodic.offset); - u16 period_ms = effect->u.periodic.period; - if (period_ms == 0) { - hid_err(t500rs->hdev, - "Periodic effect period cannot be zero\n"); - return -EINVAL; - } - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *) - buf; - t500rs_build_r04_periodic( - p, (u8)(param_sub & 0xff), mag, offset, - phase, period_ms); + ret = t500rs_send_periodic_packet(t500rs, buf, + (u8)param_sub, + &effect->u.periodic, + effect->direction); } - ret = t500rs_send_hid( - t500rs, buf, - sizeof(struct t500rs_pkt_r04_periodic_ramp)); - if (ret) - break; break; } case T500RS_SEQ_CONDITION_X: { const struct ff_condition_effect *cond = &effect->u.condition[0]; - /* Scale saturation from Linux FFB range (0..65535) to device range - * (0..100) */ - u8 right_sat = (cond->right_saturation * 100) / 65535; - u8 left_sat = (cond->left_saturation * 100) / 65535; - - struct t500rs_pkt_r05_condition *p = - (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub), - cond->right_coeff, - cond->left_coeff, right_sat, - left_sat, cond->deadband, - cond->center); - ret = t500rs_send_hid( - t500rs, buf, - sizeof(struct t500rs_pkt_r05_condition)); + ret = t500rs_send_condition_packet(t500rs, buf, + (u8)param_sub, cond); break; } @@ -693,21 +880,8 @@ static int t500rs_send_packet_sequence(struct t500rs_device_entry *t500rs, /* Y-axis: use condition[1] if available, else zeros */ const struct ff_condition_effect *cond = &effect->u.condition[1]; - /* Scale saturation from Linux FFB range (0..65535) to device range - * (0..100) */ - u8 right_sat = (cond->right_saturation * 100) / 65535; - u8 left_sat = (cond->left_saturation * 100) / 65535; - - struct t500rs_pkt_r05_condition *p = - (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(env_sub & 0xff), - cond->right_coeff, - cond->left_coeff, right_sat, - left_sat, cond->deadband, - cond->center); - ret = t500rs_send_hid( - t500rs, buf, - sizeof(struct t500rs_pkt_r05_condition)); + ret = t500rs_send_condition_packet(t500rs, buf, + (u8)env_sub, cond); break; } @@ -946,9 +1120,9 @@ static int t500rs_upload_condition(struct t500rs_device_entry *t500rs, u8 effect_gain; const char *type_name; /* - * Determine effect type code and gain level. - * Per Windows captures: Spring=0x40, Damper/Friction/Inertia=0x41 - */ + * Determine effect type code and gain level. + * Per Windows captures: Spring=0x40, Damper/Friction/Inertia=0x41 + */ u8 effect_type; switch (effect->type) { case FF_SPRING: @@ -1016,18 +1190,18 @@ static int t500rs_upload_periodic(struct t500rs_device_entry *t500rs, u8 effect_type; /* - * Determine waveform name and effect_type for 0x01 packet. - * - * Per Windows captures, waveform type IS encoded in the 0x01 packet's - * effect_type field (byte 2). - * - * Effect type values for periodic waveforms: - * - 0x20 = Square - * - 0x21 = Triangle - * - 0x22 = Sine - * - 0x23 = Sawtooth Up - * - 0x24 = Sawtooth Down - */ + * Determine waveform name and effect_type for 0x01 packet. + * + * Per Windows captures, waveform type IS encoded in the 0x01 packet's + * effect_type field (byte 2). + * + * Effect type values for periodic waveforms: + * - 0x20 = Square + * - 0x21 = Triangle + * - 0x22 = Sine + * - 0x23 = Sawtooth Up + * - 0x24 = Sawtooth Down + */ switch (effect->u.periodic.waveform) { case FF_SQUARE: type_name = "square"; @@ -1189,10 +1363,10 @@ static int t500rs_upload_effect(void *data, /* Validate common parameters */ /* Direction is provided by the Linux FF subsystem as 0..65535 (u16). - * The device expects 0..35999 (0.01 degree units); scaling is done by - * t500rs_scale_direction() when sending packets. Accept the full u16 - * range here instead of rejecting values >35999 (e.g. 49152). - */ + * The device expects 0..35999 (0.01 degree units); scaling is done by + * t500rs_scale_direction() when sending packets. Accept the full u16 + * range here instead of rejecting values >35999 (e.g. 49152). + */ /* no validation needed here */ if (effect->replay.delay > 65535) { hid_err(t500rs->hdev, "Delay %u exceeds maximum 65535\n", @@ -1336,21 +1510,15 @@ static int t500rs_update_effect(void *data, switch (effect->type) { case FF_CONSTANT: { - int level = effect->u.constant.level; - u16 direction = effect->direction; - - if (level == old->u.constant.level && - direction == old->direction) + if (effect->u.constant.level == old->u.constant.level && + effect->direction == old->direction) return 0; - s8 signed_level = - t500rs_scale_const_with_direction(level, direction); u16 param_sub, env_sub; t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - struct t500rs_r03_const *r3 = (struct t500rs_r03_const *)buf; - t500rs_build_r03_constant(r3, (u8)(param_sub & 0xff), - signed_level); - return t500rs_send_hid(t500rs, (u8 *)r3, sizeof(*r3)); + return t500rs_send_constant_packet(t500rs, buf, (u8)param_sub, + effect->u.constant.level, + effect->direction); } case FF_PERIODIC: { @@ -1362,29 +1530,11 @@ static int t500rs_update_effect(void *data, effect->direction == old->direction) return 0; - /* Apply direction projection to magnitude and adjust phase if needed */ - u16 phase_raw = effect->u.periodic.phase; - u8 mag = t500rs_scale_periodic_with_direction( - effect->u.periodic.magnitude, effect->direction, - &phase_raw); - u8 phase = t500rs_scale_periodic_phase(phase_raw); - u8 offset = - t500rs_scale_periodic_offset(effect->u.periodic.offset); - u16 period_ms = effect->u.periodic.period; u16 param_sub, env_sub; - - if (period_ms == 0) { - hid_err(t500rs->hdev, - "Periodic effect period cannot be zero\n"); - return -EINVAL; - } - t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_periodic(p, (u8)(param_sub & 0xff), mag, - offset, phase, period_ms); - return t500rs_send_hid(t500rs, buf, sizeof(*p)); + return t500rs_send_periodic_packet(t500rs, buf, (u8)param_sub, + &effect->u.periodic, + effect->direction); } case FF_RAMP: { @@ -1394,22 +1544,11 @@ static int t500rs_update_effect(void *data, effect->replay.length == old->replay.length) return 0; - u16 duration_ms = effect->replay.length; - if (duration_ms == 0) { - hid_err(t500rs->hdev, - "Ramp effect duration cannot be zero\n"); - return -EINVAL; - } - u16 param_sub, env_sub; t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - struct t500rs_pkt_r04_periodic_ramp *p = - (struct t500rs_pkt_r04_periodic_ramp *)buf; - t500rs_build_r04_ramp(p, (u8)(param_sub & 0xff), - effect->u.ramp.start_level, - effect->u.ramp.end_level, duration_ms); - - return t500rs_send_hid(t500rs, buf, sizeof(*p)); + return t500rs_send_ramp_packet(t500rs, buf, (u8)param_sub, + &effect->u.ramp, + effect->replay.length); } case FF_SPRING: @@ -1417,9 +1556,9 @@ static int t500rs_update_effect(void *data, case FF_FRICTION: case FF_INERTIA: { /* - * Skip update if parameters unchanged - prevents micro-pulse/rumble - * when games spam identical condition updates. - */ + * Skip update if parameters unchanged - prevents micro-pulse/rumble + * when games spam identical condition updates. + */ const struct ff_condition_effect *cond = &effect->u.condition[0]; const struct ff_condition_effect *cond_old = @@ -1436,18 +1575,8 @@ static int t500rs_update_effect(void *data, return 0; t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); - /* Scale saturation from Linux FFB range (0..65535) to device range (0..100) - */ - u8 right_sat = (cond->right_saturation * 100) / 65535; - u8 left_sat = (cond->left_saturation * 100) / 65535; - - struct t500rs_pkt_r05_condition *p = - (struct t500rs_pkt_r05_condition *)buf; - t500rs_build_r05_condition(p, (u8)(param_sub & 0xff), - cond->right_coeff, cond->left_coeff, - right_sat, left_sat, cond->deadband, - cond->center); - return t500rs_send_hid(t500rs, buf, sizeof(*p)); + return t500rs_send_condition_packet(t500rs, buf, + (u8)param_sub, cond); } default: @@ -1468,10 +1597,12 @@ 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. That leaves a permanent strong - * centering force which masks/overpowers other forces. To avoid this, message - * the requests for the user to revert the gain value to expected value. */ + /* + * Wine compatibility: Some games (e.g., LFS under Wine) set autocenter to + * 100%% at startup. That leaves a permanent strong + * centering force which masks/overpowers other forces. To avoid this, message + * the requests for the user to revert the gain value to expected value. + */ if (autocenter_percent >= 100) { hid_warn( t500rs->hdev, @@ -1485,27 +1616,23 @@ static int t500rs_set_autocenter(void *data, u16 autocenter) return -ENOMEM; /* Enable autocenter: Report 0x40 0x04 0x01 */ - { - struct t500rs_pkt_r40_config *config = - (struct t500rs_pkt_r40_config *)buf; - config->id = 0x40; - config->subcmd = 0x04; - config->data1 = 0x01; /* Enable */ - config->data2 = 0x00; - } + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x04; + config->data1 = 0x01; /* Enable */ + config->data2 = 0x00; ret = t500rs_send_hid(t500rs, buf, 4); if (ret) return ret; /* Set autocenter strength: Report 0x40 0x03 [value] */ - { - struct t500rs_pkt_r40_config *config = - (struct t500rs_pkt_r40_config *)buf; - config->id = 0x40; - config->subcmd = 0x03; - config->data1 = autocenter_percent; /* 0-100 percentage */ - config->data2 = 0x00; - } + struct t500rs_pkt_r40_config *config = + (struct t500rs_pkt_r40_config *)buf; + config->id = 0x40; + config->subcmd = 0x03; + config->data1 = autocenter_percent; /* 0-100 percentage */ + config->data2 = 0x00; ret = t500rs_send_hid(t500rs, buf, 4); if (ret) return ret; @@ -1636,9 +1763,9 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) T500RS_DBG(t500rs, "Sending initialization sequence...\n"); /* Report 0x42 - Init/status commands (2 bytes each) - * Windows sends these at startup: 0x42 0x04, 0x42 0x05, 0x42 0x00 - * These appear to initialize the FFB subsystem state. - */ + * Windows sends these at startup: 0x42 0x04, 0x42 0x05, 0x42 0x00 + * These appear to initialize the FFB subsystem state. + */ memset(init_buf, 0, 2); init_buf[0] = 0x42; init_buf[1] = 0x04; @@ -1664,8 +1791,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) ret); /* Report 0x40 - Enable FFB (4 bytes) - * Magic value seen in captures that enables FFB on the base. - */ + * Magic value seen in captures that enables FFB on the base. + */ { struct t500rs_pkt_r40_config *config = (struct t500rs_pkt_r40_config *)init_buf; @@ -1695,8 +1822,8 @@ static int t500rs_wheel_init(struct tmff2_device_entry *tmff2, int open_mode) "Init command 3 (0x40 config) failed: %d\n", ret); /* Report 0x43 - Set global gain (2 bytes) - * Start at maximum device gain; the FFB gain callback will adjust later. - */ + * 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; diff --git a/src/tmt500rs/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h index 97193c7b..f649cc35 100644 --- a/src/tmt500rs/hid-tmt500rs.h +++ b/src/tmt500rs/hid-tmt500rs.h @@ -140,9 +140,9 @@ struct t500rs_pkt_r01_main { * - b0: packet type (0x04) * - b1: subtype code (from 0x01 packet_code_1, typically 0x2a) * - b2: reserved (0x00) - * - b3: magnitude (0-127, scaled from SDL 0-32767) - * - b4: offset (signed -127 to +127, scaled from SDL -32768 to +32767) - * - b5: phase (0-255 for 360 degrees, scaled from SDL 0-35999) + * - b3: magnitude (0-127, scaled from Linux FFB 0-32767) + * - b4: offset (signed -127 to +127, scaled from Linux FFB -32768 to +32767) + * - b5: phase (0-255 for 360 degrees, scaled from Linux FFB 0-35999) * - b6-b7: period in milliseconds (LE, no Hz conversion!) * * For ramp effects: phase=0, period=ramp duration, magnitude/offset encode From 8d749d8f01d23fef14a8a3a4b4d0de53167801db Mon Sep 17 00:00:00 2001 From: Caz zoo Date: Wed, 25 Feb 2026 15:56:01 +0100 Subject: [PATCH 15/15] Rebase & Fix breaking build --- src/tmt500rs/hid-tmt500rs.c | 45 +++++++++++++------------------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/tmt500rs/hid-tmt500rs.c b/src/tmt500rs/hid-tmt500rs.c index fc22323a..997fd562 100644 --- a/src/tmt500rs/hid-tmt500rs.c +++ b/src/tmt500rs/hid-tmt500rs.c @@ -383,6 +383,17 @@ static void t500rs_build_r04_ramp(struct t500rs_pkt_r04_periodic_ramp *p, p->period_ms = cpu_to_le16(duration_ms); /* b6-b7 */ } +/* Forward declarations for functions used by helper functions */ +static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, + size_t len); +static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, + u8 hw_effect_id); +static void t500rs_build_r03_constant(struct t500rs_r03_const *p, u8 code, + s8 level); +static void t500rs_build_r02_envelope(struct t500rs_pkt_r02_envelope *p, + u8 subtype, const struct ff_envelope *env, + bool allow_nonzero); + /* Saturation scaling constants */ #define T500RS_SATURATION_DEVICE_MAX 100 #define T500RS_SATURATION_LINUX_MAX 65535 @@ -775,30 +786,6 @@ const signed short t500rs_effects[] = { FF_CONSTANT, FF_SPRING, FF_DAMPER, FF_SAW_UP, FF_SAW_DOWN, FF_RAMP, FF_GAIN, FF_AUTOCENTER, -1 }; -/* Forward declarations to avoid implicit declarations before worker uses them - */ -static int t500rs_send_hid(struct t500rs_device_entry *t500rs, u8 *data, - size_t len); - -static inline int t500rs_send_stop(struct t500rs_device_entry *t500rs, - u8 hw_effect_id); - -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); - /* * Send a sequence of packets for effect upload. * Abstracts the hardcoded packet orders in upload functions. @@ -1627,12 +1614,12 @@ static int t500rs_set_autocenter(void *data, u16 autocenter) return ret; /* Set autocenter strength: Report 0x40 0x03 [value] */ - struct t500rs_pkt_r40_config *config = + struct t500rs_pkt_r40_config *strength = (struct t500rs_pkt_r40_config *)buf; - config->id = 0x40; - config->subcmd = 0x03; - config->data1 = autocenter_percent; /* 0-100 percentage */ - config->data2 = 0x00; + strength->id = 0x40; + strength->subcmd = 0x03; + strength->data1 = autocenter_percent; /* 0-100 percentage */ + strength->data2 = 0x00; ret = t500rs_send_hid(t500rs, buf, 4); if (ret) return ret;