nirenjan/libx52

possible fixing kernel module for mouse?

Closed this issue · 15 comments

MNS26 commented

on kernel 6.4.15-100 when using the kernel module to fix the mouse cursor it breaks the slider on the throttle

(when loaded)
S is mapped to mouse vertical
Rs is mapped to mouse horizontal
Slider now sends random buttons like the mouse vertical axis before

(when not loaded)
S is mapped to Slider
Rs is mapped to mouse vertical axis
mouse horizontal axis is broken and pushes buttons

2023-09-27.11-08-19.mp4

this is a slightly modified version that fixes the button layout to how it is originally (

/*
 * HID driver for Saitek X52 HOTAS
 *
 * Supported devices:
 *  - Saitek X52
 *  - Saitek X52 Pro
 *
 * Copyright (c) 2020 Nirenjan Krishnan
 *
 * SPDX-License-Identifier: GPL-2.0-only
 */

#include <linux/module.h>
#include <linux/hid.h>
#include <linux/bits.h>

#define VENDOR_SAITEK 0x06a3
#define DEV_X52_1 0x0255
#define DEV_X52_2 0x075c
#define DEV_X52_PRO 0x0762


static void _parse_axis_report(struct input_dev *input_dev,
                               int is_pro, u8 *data, int len)
{
    static const s32 hat_to_axis[16][2] = {
        {0, 0},
        {0, -1},
        {1, -1},
        {1, 0},
        {1, 1},
        {0, 1},
        {-1, 1},
        {-1, 0},
        {-1, -1},
    };

    u8 hat = (data[len - 2]) >> 4;

    u32 axis = (data[3] << 24) |
               (data[2] << 16) |
               (data[1] <<  8) |
               data[0];

    if (is_pro) {
        input_report_abs(input_dev, ABS_X, (axis & 0x3ff));
        input_report_abs(input_dev, ABS_Y, ((axis >> 10) & 0x3ff));
    } else {
        input_report_abs(input_dev, ABS_X, (axis & 0x7ff));
        input_report_abs(input_dev, ABS_Y, ((axis >> 11) & 0x7ff));
    }

    input_report_abs(input_dev, ABS_RZ, ((axis >> 22) & 0x3ff));
    input_report_abs(input_dev, ABS_Z, data[4]);
    input_report_abs(input_dev, ABS_RX, data[5]);
    input_report_abs(input_dev, ABS_RY, data[6]);
    input_report_abs(input_dev, ABS_MISC, data[7]);

    /* Mouse stick is always the last byte of the report */
    input_report_abs(input_dev, ABS_TILT_X, data[len-1] & 0xf);
    input_report_abs(input_dev, ABS_TILT_Y, data[len-1] >> 4);

    /* Hat is always the upper nibble of the penultimate byte of the report */
    input_report_abs(input_dev, ABS_HAT0X, hat_to_axis[hat][0]);
    input_report_abs(input_dev, ABS_HAT0Y, hat_to_axis[hat][1]);
}

/**********************************************************************
 * Mapping buttons
 * ===============
 *
 * The X52 and X52 Pro report the buttons in different orders. In order
 * to let a userspace application handle them in a generic fashion, we
 * define a map for buttons to BTN_* defines, so that one button (eg. clutch)
 * will send the same ID on both X52 and X52 Pro
 *
 * The map is defined below.
 **********************************************************************
 */
#define X52_TRIGGER_1       BTN_TRIGGER_HAPPY1
#define X52_BTN_FIRE        BTN_TRIGGER_HAPPY2
#define X52_BTN_A           BTN_TRIGGER_HAPPY3
#define X52_BTN_B           BTN_TRIGGER_HAPPY4
#define X52_BTN_C           BTN_TRIGGER_HAPPY5
#define X52_BTN_PINKIE      BTN_TRIGGER_HAPPY6
#define X52_BTN_D           BTN_TRIGGER_HAPPY7
#define X52_BTN_E           BTN_TRIGGER_HAPPY8
#define X52_BTN_T1_UP       BTN_TRIGGER_HAPPY9
#define X52_BTN_T1_DN       BTN_TRIGGER_HAPPY10
#define X52_BTN_T2_UP       BTN_TRIGGER_HAPPY11
#define X52_BTN_T2_DN       BTN_TRIGGER_HAPPY12
#define X52_BTN_T3_UP       BTN_TRIGGER_HAPPY13
#define X52_BTN_T3_DN       BTN_TRIGGER_HAPPY14
#define X52_TRIGGER_2       BTN_TRIGGER_HAPPY15
#define X52_MOUSE_LEFT      BTN_TRIGGER_HAPPY16
#define X52_MOUSE_FORWARD   BTN_TRIGGER_HAPPY17
#define X52_MOUSE_BACKWARD  BTN_TRIGGER_HAPPY18
#define X52_MOUSE_RIGHT     BTN_TRIGGER_HAPPY19
#define X52_STICK_POV_N     BTN_TRIGGER_HAPPY20
#define X52_STICK_POV_E     BTN_TRIGGER_HAPPY21
#define X52_STICK_POV_S     BTN_TRIGGER_HAPPY22
#define X52_STICK_POV_W     BTN_TRIGGER_HAPPY23
#define X52_THROT_POV_N     BTN_TRIGGER_HAPPY24
#define X52_THROT_POV_E     BTN_TRIGGER_HAPPY25
#define X52_THROT_POV_S     BTN_TRIGGER_HAPPY26
#define X52_THROT_POV_W     BTN_TRIGGER_HAPPY27
#define X52_MODE_1          BTN_TRIGGER_HAPPY28
#define X52_MODE_2          BTN_TRIGGER_HAPPY29
#define X52_MODE_3          BTN_TRIGGER_HAPPY30
#define X52_BTN_CLUTCH      BTN_TRIGGER_HAPPY31
#define X52_BTN_FUNCTION    BTN_TRIGGER_HAPPY32
#define X52_BTN_START_STOP  BTN_TRIGGER_HAPPY33
#define X52_BTN_RESET       BTN_TRIGGER_HAPPY34
#define X52_BTN_PG_UP       BTN_TRIGGER_HAPPY35
#define X52_BTN_PG_DN       BTN_TRIGGER_HAPPY36
#define X52_BTN_UP          BTN_TRIGGER_HAPPY37
#define X52_BTN_DN          BTN_TRIGGER_HAPPY38
#define X52_BTN_MFD_SELECT  BTN_TRIGGER_HAPPY39

static void _parse_button_report(struct input_dev *input_dev,
                                 int is_pro, u8 *data, int num_buttons)
{
    int i;
    int idx;
    int btn;

    // Map X52 buttons from report to fixed button ID
    // This should be defined in the order provided in the report.
    static const int x52_buttons[] = {
        X52_TRIGGER_1,
        X52_BTN_FIRE,
        X52_BTN_A,
        X52_BTN_B,
        X52_BTN_C,
        X52_BTN_PINKIE,
        X52_BTN_D,
        X52_BTN_E,
        X52_BTN_T1_UP,
        X52_BTN_T1_DN,
        X52_BTN_T2_UP,
        X52_BTN_T2_DN,
        X52_BTN_T3_UP,
        X52_BTN_T3_DN,
        X52_TRIGGER_2,
        X52_STICK_POV_N,
        X52_STICK_POV_E,
        X52_STICK_POV_S,
        X52_STICK_POV_W,
        X52_THROT_POV_N,
        X52_THROT_POV_E,
        X52_THROT_POV_S,
        X52_THROT_POV_W,
        X52_MODE_1,
        X52_MODE_2,
        X52_MODE_3,
        X52_BTN_FUNCTION,
        X52_BTN_START_STOP,
        X52_BTN_RESET,
        X52_BTN_CLUTCH,
        X52_MOUSE_LEFT,
        X52_MOUSE_RIGHT,
        X52_MOUSE_FORWARD,
        X52_MOUSE_BACKWARD,
    };

    static const int pro_buttons[] = {
        X52_TRIGGER_1,
        X52_BTN_FIRE,
        X52_BTN_A,
        X52_BTN_B,
        X52_BTN_C,
        X52_BTN_PINKIE,
        X52_BTN_D,
        X52_BTN_E,
        X52_BTN_T1_UP,
        X52_BTN_T1_DN,
        X52_BTN_T2_UP,
        X52_BTN_T2_DN,
        X52_BTN_T3_UP,
        X52_BTN_T3_DN,
        X52_TRIGGER_2,
        X52_MOUSE_LEFT,
        X52_MOUSE_FORWARD,
        X52_MOUSE_BACKWARD,
        X52_MOUSE_RIGHT,
        X52_STICK_POV_N,
        X52_STICK_POV_E,
        X52_STICK_POV_S,
        X52_STICK_POV_W,
        X52_THROT_POV_N,
        X52_THROT_POV_E,
        X52_THROT_POV_S,
        X52_THROT_POV_W,
        X52_MODE_1,
        X52_MODE_2,
        X52_MODE_3,
        X52_BTN_CLUTCH,
        X52_BTN_FUNCTION,
        X52_BTN_START_STOP,
        X52_BTN_RESET,
        X52_BTN_PG_UP,
        X52_BTN_PG_DN,
        X52_BTN_UP,
        X52_BTN_DN,
        X52_BTN_MFD_SELECT,
    };

    const int *btn_map = is_pro ? pro_buttons : x52_buttons ;

    for (i = 0; i < num_buttons; i++) {
        idx = 8 + (i / BITS_PER_BYTE);
        btn = !!(data[idx] & (1 << (i % BITS_PER_BYTE)));
        input_report_key(input_dev, btn_map[i], btn);
    }
}

static int _parse_x52_report(struct input_dev *input_dev,
                             u8 *data, int len)
{
    if (len != 14) {
        return -1;
    }

    _parse_axis_report(input_dev, 0, data, len);
    _parse_button_report(input_dev, 0, data, 34);
    return 0;
}

static int _parse_x52pro_report(struct input_dev *input_dev,
                             u8 *data, int len)
{
    if (len != 15) {
        return -1;
    }

    _parse_axis_report(input_dev, 1, data, len);
    _parse_button_report(input_dev, 1, data, 39);
    return 0;
}

static int x52_raw_event(struct hid_device *dev,
                         struct hid_report *report, u8 *data, int len)
{
    struct input_dev *input_dev = hid_get_drvdata(dev);
    int is_pro = (dev->product == DEV_X52_PRO);
    int ret;

    if (is_pro) {
        ret = _parse_x52pro_report(input_dev, data, len);
    } else {
        ret = _parse_x52_report(input_dev, data, len);
    }
    input_sync(input_dev);
    return ret;
}

static int x52_input_configured(struct hid_device *dev,
                                struct hid_input *input)
{
    struct input_dev *input_dev = input->input;
    int i;
    int max_btn;
    int is_pro = (dev->product == DEV_X52_PRO);
    int max_stick;

    hid_set_drvdata(dev, input_dev);

    set_bit(EV_KEY, input_dev->evbit);
    set_bit(EV_ABS, input_dev->evbit);

    /*
     * X52 has only 34 buttons, X52 Pro has 39. The first 34 buttons are common
     * although the button order differs between the two.
     */
    max_btn = is_pro ? 39 : 34 ;

    for (i = 0; i < max_btn; i++) {
        set_bit(BTN_TRIGGER_HAPPY1 + i, input_dev->keybit);
    }

    /* Both X52 and X52 Pro have the same number of axes, only the ranges vary */

    set_bit(ABS_X, input_dev->absbit);
    set_bit(ABS_Y, input_dev->absbit);
    set_bit(ABS_Z, input_dev->absbit);
    set_bit(ABS_RX, input_dev->absbit);
    set_bit(ABS_RY, input_dev->absbit);
    set_bit(ABS_RZ, input_dev->absbit);
    set_bit(ABS_RZ, input_dev->absbit);
    set_bit(ABS_HAT0X, input_dev->absbit);
    set_bit(ABS_HAT0Y, input_dev->absbit);
    set_bit(ABS_TILT_X, input_dev->absbit);
    set_bit(ABS_TILT_Y, input_dev->absbit);
    set_bit(ABS_MISC, input_dev->absbit);

    max_stick = is_pro ? 1023 : 2047;
    input_set_abs_params(input_dev, ABS_X, 0, max_stick, max_stick >> 8, max_stick >> 4);
    input_set_abs_params(input_dev, ABS_Y, 0, max_stick, max_stick >> 8, max_stick >> 4);
    input_set_abs_params(input_dev, ABS_RZ, 0, 1023, 3, 63);
    input_set_abs_params(input_dev, ABS_RX, 0, 255, 0, 15);
    input_set_abs_params(input_dev, ABS_RY, 0, 255, 0, 15);
    input_set_abs_params(input_dev, ABS_Z, 0, 255, 0, 15);
    input_set_abs_params(input_dev, ABS_MISC, 0, 255, 0, 15);
    input_set_abs_params(input_dev, ABS_HAT0X, -1, 1, 0, 0);
    input_set_abs_params(input_dev, ABS_HAT0Y, -1, 1, 0, 0);
    input_set_abs_params(input_dev, ABS_TILT_X, 0, 15, 0, 0);
    input_set_abs_params(input_dev, ABS_TILT_Y, 0, 15, 0, 0);

    return 0;
}

static int x52_input_mapping(struct hid_device *dev,
                             struct hid_input *input,
                             struct hid_field *field,
                             struct hid_usage *usage,
                             unsigned long **bit,
                             int *max)
{
    /*
     * We are reporting the events in x52_raw_event.
     * Skip the hid-input processing.
     */
    return -1;
}

static const struct hid_device_id x52_devices[] = {
    { HID_USB_DEVICE(VENDOR_SAITEK, DEV_X52_1) },
    { HID_USB_DEVICE(VENDOR_SAITEK, DEV_X52_2) },
    { HID_USB_DEVICE(VENDOR_SAITEK, DEV_X52_PRO) },
    {}
};

MODULE_DEVICE_TABLE(hid, x52_devices);

static struct hid_driver x52_driver = {
    .name = "saitek-x52",
    .id_table = x52_devices,
    .input_mapping = x52_input_mapping,
    .input_configured = x52_input_configured,
    .raw_event = x52_raw_event,
};

module_hid_driver(x52_driver);


MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Nirenjan Krishnan");
MODULE_DESCRIPTION("HID driver for Saitek X52 HOTAS devices");

You shouldn't need the kernel module anymore, the hid-quirks should already handle separating the thumb stick into two separate axes.

Didn't you use to play with the joystick firmware? My kernel driver makes some assumptions about the report format, so if that has changed, then it won't work as expected.

As usual, I need the output of lsusb -v and usbhid-dump, as I requested in #50

MNS26 commented

You shouldn't need the kernel module anymore, the hid-quirks should already handle separating the thumb stick into two separate axes.

in linux yes, in wine and all its versions still breaks

Didn't you use to play with the joystick firmware? My kernel driver makes some assumptions about the report format, so if that has changed, then it won't work as expected.

reprogrammed with original (dumped) firmware

❯ sudo lsusb -v -d 06a3:0762

Bus 001 Device 005: ID 06a3:0762 Saitek PLC Saitek X52 Pro Flight Control System
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0         8
  idVendor           0x06a3 Saitek PLC
  idProduct          0x0762 Saitek X52 Pro Flight Control System
  bcdDevice           20.00
  iManufacturer           1 Logitech
  iProduct                2 X52 Professional H.O.T.A.S.
  iSerial                 0 
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x0022
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0x80
      (Bus Powered)
    MaxPower              230mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 
      bInterfaceProtocol      0 
      iInterface              0 
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.11
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength     125
         Report Descriptors: 
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0010  1x 16 bytes
        bInterval              10
Device Status:     0x0000
  (Bus Powered)
❯ sudo  usbhid-dump
005:003:002:DESCRIPTOR         1695813294.734790
 06 00 FF 09 01 A1 01 85 10 95 06 75 08 15 00 26
 FF 00 09 01 81 00 09 01 91 00 C0 06 00 FF 09 02
 A1 01 85 11 95 13 75 08 15 00 26 FF 00 09 02 81
 00 09 02 91 00 C0

005:003:001:DESCRIPTOR         1695813294.737774
 05 01 09 02 A1 01 85 02 09 01 A1 00 95 10 75 01
 15 00 25 01 05 09 19 01 29 10 81 02 95 02 75 10
 16 01 80 26 FF 7F 05 01 09 30 09 31 81 06 95 01
 75 08 15 81 25 7F 09 38 81 06 95 01 05 0C 0A 38
 02 81 06 C0 C0 05 0C 09 01 A1 01 85 03 95 02 75
 10 15 01 26 FF 02 19 01 2A FF 02 81 00 C0 05 01
 09 80 A1 01 85 04 95 01 75 02 15 01 25 03 09 82
 09 81 09 83 81 00 75 01 15 00 25 01 09 9B 81 06
 75 05 81 03 C0

005:003:000:DESCRIPTOR         1695813294.740774
 05 01 09 06 A1 01 05 07 19 E0 29 E7 15 00 25 01
 75 01 95 08 81 02 95 05 05 08 19 01 29 05 91 02
 95 01 75 03 91 03 95 70 75 01 05 07 19 04 29 73
 81 02 95 05 19 87 29 8B 81 02 95 03 19 90 29 92
 81 02 C0

001:005:000:DESCRIPTOR         1695813294.757880
 05 01 09 04 A1 01 09 01 A1 00 09 30 09 31 15 00
 26 FF 03 75 0A 95 02 81 02 75 02 95 01 81 01 09
 35 15 00 26 FF 03 75 0A 95 01 81 02 09 32 09 33
 09 34 09 36 15 00 26 FF 00 75 08 95 04 81 02 05
 09 19 01 29 27 15 00 25 01 95 27 75 01 81 02 75
 05 95 01 81 01 05 01 09 39 15 01 25 08 35 00 46
 3B 01 66 14 00 75 04 95 01 81 42 05 05 09 24 09
 26 15 00 25 0F 75 04 95 02 81 02 C0 C0

001:004:002:DESCRIPTOR         1695813294.761869
 05 0C 09 01 A1 01 15 00 25 01 09 E9 09 EA 09 B5
 09 B6 75 01 95 04 81 02 09 E2 09 B7 09 CD 95 03
 81 06 05 0B 09 20 95 01 81 06 05 0C 26 FF 00 09
 00 75 08 95 03 81 02 09 00 95 04 91 02 C0

001:003:001:DESCRIPTOR         1695813294.771740
 05 01 09 06 A1 01 05 07 19 E0 29 E7 15 00 25 01
 95 08 75 01 81 02 81 03 95 05 05 08 19 01 29 05
 91 02 95 01 75 03 91 01 95 06 75 08 15 00 26 A4
 00 05 07 19 00 2A A4 00 81 00 C0

001:003:000:DESCRIPTOR         1695813294.792746
 05 01 09 02 A1 01 85 01 09 01 A1 00 05 09 19 01
 29 05 15 00 25 01 95 05 75 01 81 02 95 01 75 03
 81 01 05 01 09 30 09 31 15 81 25 7F 75 08 95 02
 81 06 09 38 15 81 25 7F 75 08 95 01 81 06 C0 C0
 05 0C 09 01 A1 01 85 03 75 10 95 02 15 01 26 8C
 02 19 01 2A 8C 02 81 00 C0 05 01 09 80 A1 01 85
 04 75 02 95 01 15 01 25 03 09 82 09 81 09 83 81
 60 75 06 81 03 C0 05 01 09 00 A1 01 85 05 06 00
 FF 09 01 15 81 25 7F 75 08 95 07 B1 02 C0

001:006:000:DESCRIPTOR         1695813294.797736
 05 01 09 00 A1 01 85 01 15 00 26 FF 00 19 01 29
 08 95 3F 75 08 81 02 19 01 29 08 91 02 85 02 15
 00 26 FF 00 19 01 29 08 95 3F 75 08 81 02 19 01
 29 08 91 02 85 D0 15 00 26 FF 00 19 01 29 08 95
 3F 75 08 81 02 19 01 29 08 91 02 85 FA 15 00 26
 FF 00 19 01 29 08 95 3F 75 08 81 02 19 01 29 08
 91 02 85 FC 15 00 26 FF 00 19 01 29 08 95 3F 75
 08 81 02 19 01 29 08 91 02 06 00 FF 09 01 A1 01
 85 52 09 01 15 00 26 FF 00 75 08 95 B8 B1 02 85
 53 09 01 15 00 26 FF 00 75 08 96 D4 02 B1 02 85
 80 09 01 15 00 26 FF 00 75 08 95 F1 B1 02 85 81
 09 01 15 00 26 FF 00 75 08 95 F1 B1 02 85 82 09
 01 15 00 26 FF 00 75 08 95 F1 B1 02 85 A0 09 01
 15 00 26 FF 00 75 08 96 F8 02 B1 02 85 A1 09 01
 15 00 26 FF 00 75 08 96 F8 02 B1 02 85 A2 09 01
 15 00 26 FF 00 75 08 96 F8 02 B1 02 C0 C0

(without module)

2023-09-27.12-46-31.mp4

I've updated the kernel module in the branch report-buttons-in-order (commit 19b76d). Can you give this a try and let me know if it works?

MNS26 commented

hmm ok so it loads and keeps all buttons and axis the same but still has the broken mouse axis

image

MNS26 commented

looking at how it presses the buttons it might be that it overflows into the button sequence...
since you can see it go in steps when you move it slowly

MNS26 commented

i wonder if the bit location is misaligned

2023-09-27.20-19-05.mp4

I think the issue is that Proton/Wine is not using the kernel driver, but attempting to parse the USB report directly. The clue here is the Unhandled type 00000005, which appears to correspond to this section in the descriptor.

MNS26 commented

oh yikes... i already made report on the wine side... ill add this to it

MNS26 commented

hmm this means the wine implementation might be flawed then

MNS26 commented

umm is it a option to for the time being block those 2 from reporting?

We can disable them from being reported in the kernel module, but I don't think that would help in your case, since Steam/Wine is handling the parsing internally. The virtual mouse is handled by x52d, which parses the raw report and sends the events through uinput

MNS26 commented

hmm it was working before though

MNS26 commented

i did report this in the wine bugzilla

MNS26 commented

well after a looooong silence its finally getting fixed 🥳
in the end it was wine... the 9th axis overflowed into the buttons and it caused false button presses

MNS26 commented

i will now close this issue since a fix is there