diff --git a/Kbuild b/Kbuild index 28a70a7..b46b92c 100644 --- a/Kbuild +++ b/Kbuild @@ -1,3 +1,3 @@ -obj-m := hid-moza-ff.o -hid-moza-ff-y := hid-moza.o hid-pidff.o +obj-m := hid-universal-pidff.o +hid-universal-pidff-y := hid-pidff-wrapper.o hid-pidff.o ccflags-y := -Idrivers/hid diff --git a/README.md b/README.md index e4e8074..55dbf06 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ -# Force feedback support for MOZA steering wheels +# Universal Force Feedback driver for Linux -Linux module driver for MOZA driving wheels. +Linux PIDFF driver with useful patches for initialization of FFB devices. Primarily targeting Direct Drive wheelbases. -MOZA wheels are basically DirectInput wheels. -In that repository - copy of pidff driver from 6.6 kernel with some changes around infinite length effects (like that https://github.com/berarma/ffbtools/issues/26) +## What's different between this and native pidff driver? +That driver allows most DirectDrive wheelbases to initialize and work. +Most of the DirectDrive wheelbases are basically DirectInput wheels, but with some caveats, which Windows allows, but pidff doesn't. +In that repository - pidff driver with some changes, which allows most of the DirectDrive wheelbases to work. + +1. Added quirks for better initialization rules for different wheelbases (MOZA, VRS, Cammus) +2. Fixes for infinite-length effects +3. Fixes for out-of-bounds values (no more spam in kernel logs) And that's basically it ## What devices are supported? ### Bases: -1. MOZA R3 -1. MOZA R5 -1. MOZA R9 -1. MOZA R12 -1. MOZA R16 -1. MOZA R21 - -### Wheel rims (others yet untested): -1. MOZA RS V2 +1. MOZA R3, R5, R9, R12, R16, R21 +2. ... ## What works? 1. FFB (all effects from device descriptor) @@ -25,68 +24,43 @@ And that's basically it ## What does not work? -1. Telemetry functions (Shift lights, etc), mostly because telemetry works only with proprietary soft, which can't get access to shared memory chunks from games. +1. Telemetry functions (Shift lights, displays, SimHub, etc), mostly because telemetry works only with proprietary soft, which can't get access to shared memory chunks from games. 2. `Firmware Update` function. Use Windows PC or Windows VM at the moment. -3. Setup through MOZA PitHouse even with [some tweaking](#how-to-set-up-a-base-parameters)) +3. Setup through proprietary software. May require [some tweaking](#how-to-set-up-a-base-parameters)) ## How to use this driver? -There's an [AUR packege](https://aur.archlinux.org/packages/moza-ff-dkms-git) for Arch Linux maintained by @Lawstorant +There's an [AUR packege](https://aur.archlinux.org/packages/universal-ff-dkms-git) for Arch Linux maintained by @Lawstorant Alternatively, you can install it through DKMS or manually. ### DKMS 1. Install `dkms` -2. Clone repository to `/usr/src/moza-ff` +2. Clone repository to `/usr/src/universal-pidff` 3. Install the module: -`sudo dkms install /usr/src/moza-ff` +`sudo dkms install /usr/src/universal-pidff` 4. Update initramfs: `sudo update-initramfs -u` 5. Reboot To remove module: -`sudo dkms remove moza-ff/ --all` +`sudo dkms remove universal-ff/ --all` ### Manually 1. Install `linux-headers-$(uname -r)` 2. Clone repository 3. `make` -4. `sudo insmod hid-moza-ff.ko` +4. `sudo insmod hid-universal-ff.ko` To unload module: -`sudo rmmod hid_moza_ff` +`sudo rmmod hid_universal_ff` ## How to set up a base parameters? +### MOZA +**[Boxflat](https://github.com/Lawstorant/boxflat)** is a Linux Pit House alternative made by [@Lawstorant](https://github.com/Lawstorant) -For now, please, use [Android App](https://play.google.com/store/apps/details?id=com.gudsen.mozapithouse) - - -### Non working method -You can try to setup MOZA PitHouse with Wine. You need to tweak Wine prefix for them. - -That soft uses hidraw to set up a base. You need to create `udev` rule for allow access to hidraw device: -``` -echo 'KERNEL=="hidraw*", ATTRS{idVendor}=="346e", MODE="0660", TAG+="uaccess"' | sudo tee /etc/udev/rules.d/11-moza.rules - -udevadm control --reload-rules && udevadm trigger -``` - -Then you need to force MOZA soft to use hidraw, not SDL, to find devices: -1. Create new Wine prefix for them: - - `mkdir ~/moza-wine` -2. Launch regedit in prefix: - - `WINEPREFIX=$HOME/moza-wine wine regedit` -3. Create two keys in - `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\winebus`: - - * `DisableInput` (DWORD) - set to `1`; - * `Enable SDL` (DWORD) - set to `0`; (yes, variable name contains space) -4. Now you can launch soft through that WINEPREFIX: - - `WINEPREFIX=$HOME/moza-wine wine MOZA\ Pit\ House.exe` - launch your PitHouse from installation directory. +**[Android App](https://play.google.com/store/apps/details?id=com.gudsen.mozapithouse)** ## Known issues with the driver -1. Firmware update does not work. Please use Windows machine or Windows VM for any firmware updates +- Buttons above 80 simply don't show up. This is a Linux limitation and we're trying to fix that in the upstream ## Known issues with the firmware You tell me please diff --git a/dkms.conf b/dkms.conf index 0b193b1..aebd3b5 100644 --- a/dkms.conf +++ b/dkms.conf @@ -1,8 +1,8 @@ -PACKAGE_NAME="moza-ff" -PACKAGE_VERSION="0.0.1" +PACKAGE_NAME="universal-pidff" +PACKAGE_VERSION="0.0.3" MAKE[0]="make KVERSION=$kernelver" CLEAN="make clean" -BUILT_MODULE_NAME[0]="hid-moza-ff" -DEST_MODULE_NAME[0]="hid-moza-ff" +BUILT_MODULE_NAME[0]="hid-universal-pidff" +DEST_MODULE_NAME[0]="hid-universal-pidff" DEST_MODULE_LOCATION[0]="/kernel/drivers/hid" -AUTOINSTALL="yes" \ No newline at end of file +AUTOINSTALL="yes" diff --git a/hid-ids.h b/hid-ids.h new file mode 100644 index 0000000..1115543 --- /dev/null +++ b/hid-ids.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef __HID_IDS_H +#define __HID_IDS_H + +#define USB_VENDOR_ID_MOZA 0x346e +#define USB_DEVICE_ID_MOZA_R3 0x0005 +#define USB_DEVICE_ID_MOZA_R5 0x0004 +#define USB_DEVICE_ID_MOZA_R9 0x0002 +#define USB_DEVICE_ID_MOZA_R12 0x0006 +#define USB_DEVICE_ID_MOZA_R16_R21 0x0000 + +#endif diff --git a/hid-moza.c b/hid-moza.c deleted file mode 100644 index 8e6e963..0000000 --- a/hid-moza.c +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * HID driver for Moza Steering Wheels - * - * Copyright (c) 2024 Makarenko Oleg - */ - -#include -#include -#include -#include "hid-moza.h" -#include "hid-pidff.h" - -static const struct hid_device_id moza_devices[] = { - { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R3) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R5) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R9) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R12) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R16) }, - { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R21) }, - { } -}; -MODULE_DEVICE_TABLE(hid, moza_devices); - -// Fix data type on PID Device Control -static u8 *moza_report_fixup(struct hid_device *hdev, __u8 *rdesc, - unsigned int *rsize) -{ - if (rdesc[1002] == 0x91 && rdesc[1003] == 0x02) { - rdesc[1003] = 0x00; // Fix header, it needs to be Array. - } - return rdesc; -} - - -static int moza_probe(struct hid_device *hdev, - const struct hid_device_id *id) -{ - int ret; - ret = hid_parse(hdev); - if (ret) { - hid_err(hdev, "parse failed\n"); - goto err; - } - - ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF); - if (ret) { - hid_err(hdev, "hw start failed\n"); - goto err; - } - - /* set PIDFF quirks for moza wheelbases */ - unsigned quirks = 0; - quirks |= PIDFF_QUIRK_FIX_0_INFINITE_LENGTH; - quirks |= PIDFF_QUIRK_FIX_WHEEL_DIRECTION; - - ret = hid_pidff_init_with_quirks(hdev, quirks); - if (ret) { - hid_warn(hdev, "Force feedback not supported\n"); - goto err; - } - - return 0; -err: - return ret; -} - -static int moza_input_configured(struct hid_device *hdev, - struct hid_input *hidinput) -{ - struct input_dev *input = hidinput->input; - input_set_abs_params(input, ABS_X, - input->absinfo[ABS_X].minimum, input->absinfo[ABS_X].maximum, 0, 0); - - return 0; -} - -static struct hid_driver moza_ff = { - .name = "moza-ff", - .id_table = moza_devices, - .probe = moza_probe, - .input_configured = moza_input_configured, - .report_fixup = moza_report_fixup -}; -module_hid_driver(moza_ff); - -MODULE_AUTHOR("Oleg Makarenko "); -MODULE_DESCRIPTION("MOZA HID FF Driver"); -MODULE_LICENSE("GPL"); diff --git a/hid-moza.h b/hid-moza.h deleted file mode 100644 index b3c4adb..0000000 --- a/hid-moza.h +++ /dev/null @@ -1,13 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -#ifndef __HID_MOZA_H -#define __HID_MOZA_H - -#define USB_VENDOR_ID_MOZA 0x346e -#define USB_DEVICE_ID_MOZA_R3 0x0005 -#define USB_DEVICE_ID_MOZA_R5 0x0004 -#define USB_DEVICE_ID_MOZA_R9 0x0002 -#define USB_DEVICE_ID_MOZA_R12 0x0006 -#define USB_DEVICE_ID_MOZA_R16 0x0000 -#define USB_DEVICE_ID_MOZA_R21 0x0000 - -#endif diff --git a/hid-pidff-wrapper.c b/hid-pidff-wrapper.c new file mode 100644 index 0000000..ab53744 --- /dev/null +++ b/hid-pidff-wrapper.c @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID PIDFF wrapper + * First of all targeting steering wheels and wheelbases + * + * Copyright (c) 2024 Makarenko Oleg + */ + +#include +#include +#include +#include "hid-ids.h" +#include "hid-pidff.h" + +static const struct hid_device_id pidff_wheel_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R3), + .driver_data = PIDFF_QUIRK_FIX_WHEEL_DIRECTION | PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE }, + { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R5), + .driver_data = PIDFF_QUIRK_FIX_WHEEL_DIRECTION | PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE }, + { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R9), + .driver_data = PIDFF_QUIRK_FIX_WHEEL_DIRECTION | PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE }, + { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R12), + .driver_data = PIDFF_QUIRK_FIX_WHEEL_DIRECTION | PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE }, + { HID_USB_DEVICE(USB_VENDOR_ID_MOZA, USB_DEVICE_ID_MOZA_R16_R21), + .driver_data = PIDFF_QUIRK_FIX_WHEEL_DIRECTION | PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE }, + { } +}; +MODULE_DEVICE_TABLE(hid, pidff_wheel_devices); + + +static u8 *moza_report_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *rsize) +{ + // Fix data type on PID Device Control + if (rdesc[1002] == 0x91 && rdesc[1003] == 0x02) { + rdesc[1003] = 0x00; // Fix header, it needs to be Array. + } + return rdesc; +} + + +static u8 *universal_pidff_report_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *rsize) +{ + if (hdev->vendor == USB_VENDOR_ID_MOZA) { + return moza_report_fixup(hdev, rdesc, rsize); + } + return rdesc; +} + + +/* + * Check if the device is PID and initialize it + * Add quirks after initialisation + */ +static int universal_pidff_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + int ret; + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "parse failed\n"); + goto err; + } + + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF); + if (ret) { + hid_err(hdev, "hw start failed\n"); + goto err; + } + + ret = hid_pidff_init_with_quirks(hdev, id); + if (ret) { + hid_warn(hdev, "Force feedback not supported\n"); + goto err; + } + + return 0; +err: + return ret; +} + +static int universal_pidff_input_configured(struct hid_device *hdev, + struct hid_input *hidinput) +{ + // Remove fuzz and deadzone + struct input_dev *input = hidinput->input; + input_set_abs_params(input, ABS_X, + input->absinfo[ABS_X].minimum, input->absinfo[ABS_X].maximum, 0, 0); + + return 0; +} + +static struct hid_driver universal_pidff = { + .name = "hid-universal-pidff", + .id_table = pidff_wheel_devices, + .probe = universal_pidff_probe, + .input_configured = universal_pidff_input_configured, + .report_fixup = universal_pidff_report_fixup +}; +module_hid_driver(universal_pidff); + +MODULE_AUTHOR("Oleg Makarenko "); +MODULE_DESCRIPTION("Universal HID PIDFF Driver"); +MODULE_LICENSE("GPL"); diff --git a/hid-pidff.c b/hid-pidff.c index 608c15e..6657731 100644 --- a/hid-pidff.c +++ b/hid-pidff.c @@ -25,6 +25,14 @@ #define PID_EFFECTS_MAX 64 +/* + * This is 16384 or 90 degrees in polar coordinates (up) + * Racing games exclusively use polar coordinates but sometimes + * SDL/Proton mess with this value as they try to do + * some conversions to fix FFB for flight sticks + */ +#define PIDFF_FIXED_DIRECTION 0x4000 + /* Report usage table used to put reports into an array */ #define PID_SET_EFFECT 0 @@ -65,6 +73,10 @@ static const u8 pidff_set_effect[] = { 0x22, 0x50, 0x52, 0x53, 0x54, 0x56, 0xa7 }; +static const u8 pidff_set_effect_without_delay[] = { + 0x22, 0x50, 0x52, 0x53, 0x54, 0x56 +}; + #define PID_ATTACK_LEVEL 1 #define PID_ATTACK_TIME 2 #define PID_FADE_LEVEL 3 @@ -209,6 +221,36 @@ static int pidff_rescale_signed(int i, struct hid_field *field) field->logical_minimum / -0x8000; } +/* + * Clamp minimum value for the given field + */ +static int pidff_clamp_min(int i, struct hid_field *field) +{ + int ret = i < field->logical_minimum ? field->logical_minimum : i; + pr_debug("clamped min value from %d to %d\n", i, ret); + return ret; +} + +/* + * Clamp maximum value for the given field + */ +static int pidff_clamp_max(int i, struct hid_field *field) +{ + int ret = i > field->logical_maximum ? field->logical_maximum : i; + pr_debug("clamped max value from %d to %d\n", i, ret); + return ret; +} + +/* + * Clamp value for the given field + */ +static int pidff_clamp(int i, struct hid_field *field) +{ + i = pidff_clamp_min(i, field); + i = pidff_clamp_max(i, field); + return i; +} + static void pidff_set(struct pidff_usage *usage, u16 value) { usage->value[0] = pidff_rescale(value, 0xffff, usage->field); @@ -251,7 +293,11 @@ static void pidff_set_envelope_report(struct pidff_device *pidff, pidff->set_envelope[PID_ATTACK_TIME].value[0] = envelope->attack_length; pidff->set_envelope[PID_FADE_TIME].value[0] = envelope->fade_length; - hid_dbg(pidff->hid, "attack %u => %d\n", + hid_dbg(pidff->hid, "attack level %u => %d\n", + envelope->attack_level, + pidff->set_envelope[PID_ATTACK_LEVEL].value[0]); + + hid_dbg(pidff->hid, "fade level %u => %d\n", envelope->attack_level, pidff->set_envelope[PID_ATTACK_LEVEL].value[0]); @@ -303,23 +349,16 @@ static void pidff_set_effect_report(struct pidff_device *pidff, { /* check for device quirks */ unsigned short direction = effect->direction; - unsigned short length = effect->replay.length; - if (pidff->quirks & PIDFF_QUIRK_FIX_0_INFINITE_LENGTH && length == 0) - length = 0xffff; - - if ((effect->type == FF_DAMPER || - effect->type == FF_FRICTION || - effect->type == FF_SPRING || - effect->type == FF_INERTIA) && - pidff->quirks & PIDFF_QUIRK_FIX_WHEEL_DIRECTION) - direction = 0x3FFF; + if (pidff->quirks & PIDFF_QUIRK_FIX_WHEEL_DIRECTION) + direction = PIDFF_FIXED_DIRECTION; pidff->set_effect[PID_EFFECT_BLOCK_INDEX].value[0] = pidff->block_load[PID_EFFECT_BLOCK_INDEX].value[0]; pidff->set_effect_type->value[0] = pidff->create_new_effect_type->value[0]; - pidff->set_effect[PID_DURATION].value[0] = length; + pidff->set_effect[PID_DURATION].value[0] = + effect->replay.length == 0 ? 0xffff : effect->replay.length; pidff->set_effect[PID_TRIGGER_BUTTON].value[0] = effect->trigger.button; pidff->set_effect[PID_TRIGGER_REPEAT_INT].value[0] = @@ -362,7 +401,12 @@ static void pidff_set_periodic_report(struct pidff_device *pidff, pidff_set_signed(&pidff->set_periodic[PID_OFFSET], effect->u.periodic.offset); pidff_set(&pidff->set_periodic[PID_PHASE], effect->u.periodic.phase); - pidff->set_periodic[PID_PERIOD].value[0] = effect->u.periodic.period; + // Actually we just can use clamp macro + // from include/linux/kernel.h#L59 + // But for the debug purposes we're leaving it as is + pidff->set_periodic[PID_PERIOD].value[0] = + pidff_clamp(effect->u.periodic.period, + pidff->set_periodic[PID_PERIOD].field); hid_hw_request(pidff->hid, pidff->reports[PID_SET_PERIODIC], HID_REQ_SET_REPORT); @@ -636,6 +680,17 @@ static int pidff_upload_effect(struct input_dev *dev, struct ff_effect *effect, pidff_set_effect_report(pidff, effect); if (!old || pidff_needs_set_periodic(effect, old)) pidff_set_periodic_report(pidff, effect); + + if (pidff->quirks & PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE) + { + effect->u.periodic.envelope.attack_level = + effect->u.periodic.envelope.attack_level == 0 + ? 0x7fff : effect->u.periodic.envelope.attack_level; + + effect->u.periodic.envelope.fade_level = + effect->u.periodic.envelope.fade_level == 0 + ? 0x7fff : effect->u.periodic.envelope.fade_level; + } if (!old || pidff_needs_set_envelope(&effect->u.periodic.envelope, &old->u.periodic.envelope)) @@ -1087,10 +1142,23 @@ static int pidff_init_fields(struct pidff_device *pidff, struct input_dev *dev) { int envelope_ok = 0; - if (PIDFF_FIND_FIELDS(set_effect, PID_SET_EFFECT, 1)) { - hid_err(pidff->hid, "unknown set_effect report layout\n"); - return -ENODEV; + if (pidff->quirks & PIDFF_QUIRK_NO_DELAY_EFFECT) { + hid_dbg(pidff->hid, "Find fields for set_effect without delay\n"); + if (pidff_find_fields(pidff->set_effect, + pidff_set_effect_without_delay, + pidff->reports[PID_SET_EFFECT], \ + sizeof(pidff_set_effect_without_delay), 1)) { + hid_err(pidff->hid, "unknown set_effect report layout\n"); + return -ENODEV; + } } + else { + if (PIDFF_FIND_FIELDS(set_effect, PID_SET_EFFECT, 1)) { + hid_err(pidff->hid, "unknown set_effect report layout\n"); + return -ENODEV; + } + } + PIDFF_FIND_FIELDS(block_load, PID_BLOCK_LOAD, 0); if (!pidff->block_load[PID_EFFECT_BLOCK_INDEX].value) { @@ -1272,7 +1340,6 @@ int hid_pidff_init(struct hid_device *hid) return -ENOMEM; pidff->hid = hid; - pidff->quirks = 0; hid_device_io_start(hid); @@ -1355,7 +1422,7 @@ int hid_pidff_init(struct hid_device *hid) * Check if the device is PID and initialize it * Add quirks after initialisation */ -int hid_pidff_init_with_quirks(struct hid_device *hid, unsigned quirks) +int hid_pidff_init_with_quirks(struct hid_device *hid, const struct hid_device_id *id) { int error = hid_pidff_init(hid); @@ -1369,7 +1436,7 @@ int hid_pidff_init_with_quirks(struct hid_device *hid, unsigned quirks) /* set quirks on the pidff device */ hid_device_io_start(hid); - pidff->quirks |= quirks; + pidff->quirks |= id->driver_data; hid_device_io_stop(hid); hid_dbg(dev, "Device quirks: %d\n", pidff->quirks); diff --git a/hid-pidff.h b/hid-pidff.h index 90eab45..586ee96 100644 --- a/hid-pidff.h +++ b/hid-pidff.h @@ -5,18 +5,22 @@ /* PIDFF Quirks to solve issues with certain devices */ /* - * Substitute 0 length with 0xffff to resolve issues with - * infinite effects coming from windows API + * Always set a value > 0 for PERIODIC envelope attack and fade level */ -#define PIDFF_QUIRK_FIX_0_INFINITE_LENGTH BIT(0) +#define PIDFF_QUIRK_FIX_PERIODIC_ENVELOPE BIT(0) /* - * Ignore direction for spring/damping/friction/inertia effects - * and always set 16384 + * Ignore direction and always set 16384 (0x4000) */ -#define PIDFF_QUIRK_FIX_WHEEL_DIRECTION BIT(1) +#define PIDFF_QUIRK_FIX_WHEEL_DIRECTION BIT(1) + +/* + * Skip initialization of 0xA7 descriptor (Delay effect) +*/ +#define PIDFF_QUIRK_NO_DELAY_EFFECT BIT(2) + int hid_pidff_init(struct hid_device *hid); -int hid_pidff_init_with_quirks(struct hid_device *hid, unsigned quirks); +int hid_pidff_init_with_quirks(struct hid_device *hid, const struct hid_device_id *id); #endif