From 247e98c5dc602c39684a655483d026317840da6e Mon Sep 17 00:00:00 2001 From: nirenjan Date: Wed, 29 Sep 2021 12:59:34 -0700 Subject: [PATCH] Add kernel driver for Saitek X65F joystick This commit adds the kernel driver for the Saitek X65F joystick. This is necessary, since it has the same limitation with the thumbstick not getting recognized on older kernels. The quirks fix has been pushed to newer stable kernels, and therefore do not require this driver. --- kernel_module/Makefile | 3 +- kernel_module/README.md | 15 +++ kernel_module/hid-saitek-x65.c | 177 +++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 kernel_module/hid-saitek-x65.c diff --git a/kernel_module/Makefile b/kernel_module/Makefile index 9e716e7..c4dc96c 100644 --- a/kernel_module/Makefile +++ b/kernel_module/Makefile @@ -1,5 +1,6 @@ -obj-m := saitek_x52.o +obj-m := saitek_x52.o saitek_x65.o saitek_x52-objs := hid-saitek-x52.o +saitek_x65-objs := hid-saitek-x65.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) diff --git a/kernel_module/README.md b/kernel_module/README.md index 4515db4..e09bc4a 100644 --- a/kernel_module/README.md +++ b/kernel_module/README.md @@ -35,3 +35,18 @@ automatically. Otherwise, simply disconnect and reconnect your X52. # Reporting issues Please report any issues seen as a [Github issue](https://github.com/nirenjan/libx52/issues). + +# Notes + +This folder also includes a driver for the X65F HOTAS, but it is untested as of +this writing. The same build instructions apply, but you will need to run `sudo +insmod saitek_x65.ko` instead. + +As with the X52 driver, the following kernel versions have a fix for the thumb +stick, and do not require this custom driver. + +* 5.13+ (from -rc3 onwards) +* 5.12.12+ +* 5.10.45+ +* 5.4.127+ +* 4.9.196+ diff --git a/kernel_module/hid-saitek-x65.c b/kernel_module/hid-saitek-x65.c new file mode 100644 index 0000000..8d53978 --- /dev/null +++ b/kernel_module/hid-saitek-x65.c @@ -0,0 +1,177 @@ +/* + * HID driver for Saitek X65 HOTAS + * + * Copyright (c) 2021 Nirenjan Krishnan + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include +#include +#include + +#define VENDOR_SAITEK 0x06a3 +#define DEV_X65F 0x0b6a + +/* X65 Report format assuming the below is one long little-endian integer + * + * |-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-| + * Rz | Y | X | + * |-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-| + * Slider | Ry | Rx | Z | + * |-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-| + * Buttons 1-31 | + * |-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-| + * | |MouseY |MouseX | Hat | Buttons 32-50 + * |-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-|-+-+-+-+-+-+-+-| + */ +static void _parse_axis_report(struct input_dev *input_dev, u8 *data) +{ + 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[14] >> 3) & 0x0f; + + u32 axis = (data[2] << 16) | + (data[1] << 8) | + data[0]; + + input_report_abs(input_dev, ABS_X, (axis & 0xfff)); + input_report_abs(input_dev, ABS_Y, ((axis >> 12) & 0xfff)); + + input_report_abs(input_dev, ABS_RZ, ((data[4] & 0x1) << 8) | data[3]); + input_report_abs(input_dev, ABS_Z, ((data[5] & 0x1) << 8) | data[4]); + input_report_abs(input_dev, ABS_RX, ((data[6] & 0x1) << 8) | data[5]); + input_report_abs(input_dev, ABS_RY, ((data[7] & 0x1) << 8) | data[6]); + input_report_abs(input_dev, ABS_THROTTLE, ((data[8] & 0x1) << 8) | data[7]); + + input_report_abs(input_dev, ABS_MISC, ((data[15] & 0x7) << 1) | ((data[14] & 0x80) >> 7)); + input_report_abs(input_dev, ABS_MISC + 1, (data[15] >> 3) & 0xf); + + input_report_abs(input_dev, ABS_HAT0X, hat_to_axis[hat][0]); + input_report_abs(input_dev, ABS_HAT0Y, hat_to_axis[hat][1]); +} + +static void _parse_button_report(struct input_dev *input_dev, u8 *data) +{ + u64 buttons; + int report_button; + int i; + + buttons = ((u64)data[14] << 47) | + ((u64)data[13] << 39) | + ((u64)data[12] << 31) | + ((u64)data[11] << 23) | + ((u64)data[10] << 15) | + ((u64)data[9] << 7) | + ((u64)data[8] >> 1); + + for (i = 0; i < 50; i++) { + if (i <= 0xf) { + report_button = BTN_JOYSTICK + i; + } else { + report_button = BTN_TRIGGER_HAPPY + i - 0x10; + } + input_report_key(input_dev, report_button, !!(buttons & (1UL << i))); + } +} + +static int x65_raw_event(struct hid_device *dev, + struct hid_report *report, u8 *data, int len) +{ + struct input_dev *input_dev = hid_get_drvdata(dev); + + _parse_axis_report(input_dev, data); + _parse_button_report(input_dev, data); + input_sync(input_dev); + return 0; +} + +static int x65_input_configured(struct hid_device *dev, + struct hid_input *input) +{ + struct input_dev *input_dev = input->input; + int i; + + hid_set_drvdata(dev, input_dev); + + set_bit(EV_KEY, input_dev->evbit); + set_bit(EV_ABS, input_dev->evbit); + + for (i = 0; i < 0x10; i++) { + set_bit(BTN_JOYSTICK + i, input_dev->keybit); + } + for (i = 0x10; i < 50; i++) { + set_bit(BTN_TRIGGER_HAPPY + i - 0x10, input_dev->keybit); + } + + 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_THROTTLE, input_dev->absbit); + set_bit(ABS_HAT0X, input_dev->absbit); + set_bit(ABS_HAT0Y, input_dev->absbit); + set_bit(ABS_MISC, input_dev->absbit); + set_bit(ABS_MISC+1, input_dev->absbit); + + input_set_abs_params(input_dev, ABS_X, 0, 4095, 15, 255); + input_set_abs_params(input_dev, ABS_Y, 0, 4095, 15, 255); + input_set_abs_params(input_dev, ABS_RZ, 0, 511, 1, 31); + 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_THROTTLE, 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_MISC, 0, 15, 0, 0); + input_set_abs_params(input_dev, ABS_MISC+1, 0, 15, 0, 0); + + return 0; +} + +static int x65_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 x65_raw_event. + * Skip the hid-input processing. + */ + return -1; +} + +static const struct hid_device_id x65_devices[] = { + { HID_USB_DEVICE(VENDOR_SAITEK, DEV_X65F) }, + {} +}; + +MODULE_DEVICE_TABLE(hid, x65_devices); + +static struct hid_driver x65_driver = { + .name = "saitek-x65", + .id_table = x65_devices, + .input_mapping = x65_input_mapping, + .input_configured = x65_input_configured, + .raw_event = x65_raw_event, +}; + +module_hid_driver(x65_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Nirenjan Krishnan"); +MODULE_DESCRIPTION("HID driver for Saitek X65 HOTAS devices");