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");