diff --git a/Kbuild b/Kbuild index 8cabb2b..84a37c5 100644 --- a/Kbuild +++ b/Kbuild @@ -5,4 +5,5 @@ 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 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_FFBEFFECTS.md b/docs/T500RS_FFBEFFECTS.md new file mode 100644 index 0000000..e012bdc --- /dev/null +++ b/docs/T500RS_FFBEFFECTS.md @@ -0,0 +1,619 @@ +# T500RS USB Force Feedback Protocol Analysis + +## Comprehensive Effect Implementation Reference +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. + +--- + +## 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. + +### Understanding Force Feedback Effects + +**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: + +| 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 + +** 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) +``` +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 (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 + 8 | 1 | reserved1 | 0x00 + 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 +``` + +**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 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) +- 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:** + +> **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) + - Control: 0x40 + - Duration: 0x01f4 = 500ms + - Delay: 0x0000 = 0ms + - Reserved1: 0x00 + - 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 + - Effect ID: 0x01 (hardware slot 1, logical 0) + - Effect type: 0x40 (conditional) + - Control: 0x40 + - Duration: 0x07d0 = 2000ms + - Delay: 0x0000 = 0ms + - Reserved1: 0x00 + - 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) +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x02 +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 +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 | Low byte of parameter_subtype from 0x01 packet (dynamic) +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 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. + +**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 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) + +**IMPORTANT:** Conditional effects (spring, damper, inertia, friction) require TWO 0x05 packets! + +**Packet Structure:** +``` +Offset | Size | Field | Description +-------|------|----------------|---------------------------------- +0 | 1 | packet_type | 0x05 +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) +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) +``` + +**Second Packet (Y-axis):** Same structure with second code from 0x01 packet. + +**NOTE:** T500RS is single-axis, so Y-axis packet typically contains zeros. + +**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 +- `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) +``` +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 + +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 + +**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` + +**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 + +**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` +- 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:** +- 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, 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 +- Direction: Applied during magnitude scaling (projection onto wheel axis) + +### 3. PERIODIC EFFECTS - TRIANGLE WAVE + +**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:** Waveform type determined by effect_type in main packet, not in periodic packet parameters. + +### 4. PERIODIC EFFECTS - SAWTOOTH UP + +**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 + +**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` + +**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 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 + +**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` + +**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 + +**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` + +**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 + +**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` + +**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 + +**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) + +--- + +## 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 +- **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 +- **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 degrees = 0x0000 + - 90 degrees = 0x2328 (9000 decimal) + - 180 degrees = 0x4650 (18000 decimal) + - 270 degrees = 0x6978 (27000 decimal) + +### Duration Encoding +- **Linux FFB Format:** Milliseconds +- **Device Format:** 16-bit little-endian in 0x01 packet +- **Conversion:** Direct copy (0xffff for infinite duration) +- **Examples:** + - 500ms = 0x01f4 + - 1000ms = 0x03e8 + - 2000ms = 0x07d0 + - 5000ms = 0x1388 + +### Force Level Encoding (Constant) +- **Linux FFB Format:** -32767 to +32767 (signed) +- **Device Format:** -127 to +127 (signed 8-bit) +- **Conversion:** `device_level = (os_ffb_level * 127LL) / 32767` +- **Examples:** + - 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) +- **Linux FFB Format:** 0-32767 (unsigned) +- **Device Format:** 0-127 (unsigned 8-bit) +- **Conversion:** `device_mag = (os_ffb_mag * 127LL) / 32767` +- **Examples:** + - Linux 0 -> Device 0 + - Linux 8000 -> Device 6 + - Linux 24000 -> Device 9 + - Linux 32767 -> Device 127 + +### Phase Encoding (Periodic) +- **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:** + - 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 +- **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 +- **Linux FFB Format:** 0-32767 (unsigned) +- **Device Format:** 0-255 (unsigned 8-bit) +- **Conversion:** `device_env = (os_ffb_env * 255LL) / 32767` +- **Examples:** + - 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/hid-tmff2.c b/src/hid-tmff2.c index 162a30a..b5ec5c7 100644 --- a/src/hid-tmff2.c +++ b/src/hid-tmff2.c @@ -242,9 +242,38 @@ static ssize_t gain_store(struct device *dev, } gain = value; - if (tmff2->set_gain) /* if we can, update gain immediately */ - tmff2->set_gain(tmff2->data, (GAIN_MAX * gain) / GAIN_MAX); + if (!tmff2->set_gain) + return count; + + /* Rationale: two-level gain model + * - 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 = 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/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 = 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; } @@ -258,6 +287,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 +297,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; + __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); } 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 +320,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; + __set_bit(FF_EFFECT_QUEUE_AUTOCENTER, &tmff2->pending_flags); + 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) @@ -297,10 +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 */ + spin_lock_irqsave(&tmff2->lock, lock_flags); + + 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; @@ -320,7 +388,6 @@ static void tmff2_work_handler(struct work_struct *w) (effect_delay + effect_length) * state->count) { __clear_bit(FF_EFFECT_PLAYING, &state->flags); __clear_bit(FF_EFFECT_QUEUE_UPDATE, &state->flags); - state->count = 0; } } @@ -694,6 +761,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 +878,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 +902,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 6ad1d82..39217f3 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) @@ -64,6 +66,11 @@ struct tmff2_device_entry { spinlock_t lock; + /* Pending control changes to be applied from workqueue context */ + uint16_t pending_gain; + uint16_t pending_autocenter; + unsigned long pending_flags; + int allow_scheduling; /* fields relevant to each actual device (T300, T248...) */ @@ -92,6 +99,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 +108,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 +118,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 0000000..997fd56 --- /dev/null +++ b/src/tmt500rs/hid-tmt500rs.c @@ -0,0 +1,1879 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for Thrustmaster T500RS wheel base that provides Force feedback + * + * Protocol documentation: docs/T500RS_FFBEFFECTS.md + * + * Copyright (c) 2025 Casimir Bonnet + */ + +#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, +}; + +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 long long arithmetic to prevent overflow */ + return (u8)((magnitude * 127LL) / 32767); +} + +/* + * Map logical effect ID to hardware effect ID. + * hw_id = logical_id + 1 + * + * 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) +{ + /* 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 */ +#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; +}; + +/* + * 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.) + * + * 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); +} + +/* + * 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) + * - 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 + * + * 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 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, + 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; +} + +/* + * 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 0..32767) + * - offset: signed DC offset (scaled from -32768..32767 to device range) + * - phase: 0..255 (256 steps for 360 degrees, scaled from 0..35999) + * - period_ms: period in MILLISECONDS (no Hz*100 conversion!) + * - reserved: always 0 + * + * Scaling formulas (from protocol doc): + * 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, + 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 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 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) + * 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_with_direction(int os_ffb_mag, + 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; + + if (projected < 0) { + /* Wheel handles positive magnitudes only */ + projected = -projected; + + /* 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; + } + + /* 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 Linux FFB subsystem format to device format. + * 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); +} + +/* + * 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 os_ffb_offset) +{ + return (s8)(os_ffb_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: encodes ramp direction (0x7f = up, 0x00 = down) + * - 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; + u8 phase; + + 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); + + /* + * 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 = phase; /* b5: direction (0x7f=up, 0x00=down) */ + 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 + +/** + * 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. + * + * 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 + * + * Parameters: + * - code: From 0x01 packet bytes 9-10 (first packet) or 11-12 (second packet) + * - 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, 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; +} + +/* + * 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. + * + * Per the T500RS USB protocol documentation: + * - Linux FFB level: 0-65535 (unsigned) + * - Device level: -127 to +127 (signed 8-bit) + * - 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) + */ +static inline s8 t500rs_scale_constant_level(u16 os_ffb_level) +{ + s32 tmp = ((s32)os_ffb_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 Linux FFB subsystem format to device format. + * Linux FFB : 0-32767 + * 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); +} + +/* + * 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 Linux FFB 0-32767) + * - fade_len: fade duration in milliseconds + * - 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, + 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. + * + * 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 { + /* + * 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"); + } +} + +/* Supported parameters */ +static unsigned long t500rs_params = PARAM_SPRING_LEVEL | PARAM_DAMPER_LEVEL | + 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 }; + +/* + * 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: { + ret = t500rs_send_envelope_packet(t500rs, buf, + (u8)env_sub, effect); + break; + } + + case T500RS_SEQ_CONSTANT: { + 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) { + ret = t500rs_send_ramp_packet(t500rs, buf, + (u8)param_sub, + &effect->u.ramp, + effect->replay.length); + } else { + ret = t500rs_send_periodic_packet(t500rs, buf, + (u8)param_sub, + &effect->u.periodic, + effect->direction); + } + break; + } + + case T500RS_SEQ_CONDITION_X: { + const struct ff_condition_effect *cond = + &effect->u.condition[0]; + ret = t500rs_send_condition_packet(t500rs, buf, + (u8)param_sub, cond); + 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]; + ret = t500rs_send_condition_packet(t500rs, buf, + (u8)env_sub, cond); + 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; + + if (!t500rs->send_buffer) { + hid_err(t500rs->hdev, "t500rs_set_gain: NULL send buffer\n"); + return -ENOMEM; + } + + buf = t500rs->send_buffer; + + /* 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); + + 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, 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; +} + +/* + * 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) +{ + 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)); +} + +/* 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; +} + +/* + * Upload spring/damper/friction/inertia effect. + * + * Per Windows captures (T500RS_FFBEFFECTS.md): + * - 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 + */ +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; + /* + * 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; +} + +/* + * 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 + * - 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; + + /* + * 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"; + 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; +} + +/* + * Upload ramp effect. + * + * 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 + * - 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; +} + +/* 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). + * 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; +} + +/* + * 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; +} + +/* + * 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 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); +} + +/* + * 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) +{ + 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: { + if (effect->u.constant.level == old->u.constant.level && + effect->direction == old->direction) + return 0; + + u16 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + return t500rs_send_constant_packet(t500rs, buf, (u8)param_sub, + effect->u.constant.level, + effect->direction); + } + + 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; + + u16 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + return t500rs_send_periodic_packet(t500rs, buf, (u8)param_sub, + &effect->u.periodic, + effect->direction); + } + + 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 param_sub, env_sub; + t500rs_index_to_subtypes(hw_id, ¶m_sub, &env_sub); + return t500rs_send_ramp_packet(t500rs, buf, (u8)param_sub, + &effect->u.ramp, + effect->replay.length); + } + + 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); + return t500rs_send_condition_packet(t500rs, buf, + (u8)param_sub, cond); + } + + 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; + + 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. 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 *strength = + (struct t500rs_pkt_r40_config *)buf; + 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; + + /* 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; +} + +/* 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) + * 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. + */ + { + 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); + + 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; + } + + 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/hid-tmt500rs.h b/src/tmt500rs/hid-tmt500rs.h new file mode 100644 index 0000000..f649cc3 --- /dev/null +++ b/src/tmt500rs/hid-tmt500rs.h @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * T500RS Force Feedback Protocol Constants and Structures for + * Thrustmaster T500RS wheel base. + * + * 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 __HID_TMT500RS_H +#define __HID_TMT500RS_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_SQUARE 0x20 +#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 + +/* 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 + * 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 */ + +/* 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, +}; + +/* Supported effects */ +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 (1-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 (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; + +/* + * 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: + * - b0: packet type (0x04) + * - b1: subtype code (from 0x01 packet_code_1, typically 0x2a) + * - b2: reserved (0x00) + * - 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 + * 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 Effect Packet (11 bytes) + * + * Packet format: + * - b0: packet type (0x05) + * - 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) + * + * 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 */ + 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 */ +} __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; + +/* 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 */ diff --git a/udev/71-thrustmaster-steamdeck.rules b/udev/71-thrustmaster-steamdeck.rules index 3f3ea1e..1d1e42b 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 e96cb02..25b8b59 100644 --- a/udev/99-thrustmaster.rules +++ b/udev/99-thrustmaster.rules @@ -12,6 +12,10 @@ SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b66f", RUN+="/us SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b66d", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b66d", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" +# T500RS +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" +SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b65e", RUN+="/usr/bin/jscal -s 6,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n" + # T248 + T128 SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b696", RUN+="/usr/bin/evdev-joystick --evdev %E{DEVNAME} --deadzone 0" SUBSYSTEM=="input", ATTRS{idVendor}=="044f", ATTRS{idProduct}=="b696", RUN+="/usr/bin/jscal -s 11,1,1,32767,32767,16384,16384,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,3,448,574,1394469,1394469,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912,1,0,0,0,536870912,536870912 /dev/input/js%n"