diff --git a/.gitignore b/.gitignore index a9ba97a..9bea998 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Compiled object files *.ko *.o -*.mod.* +*.mod* # Generated objects (source, executables, tarballs, etc.) a.out diff --git a/kernel_module/Makefile b/kernel_module/Makefile index adf2fea..9e716e7 100644 --- a/kernel_module/Makefile +++ b/kernel_module/Makefile @@ -1,5 +1,5 @@ obj-m := saitek_x52.o -saitek_x52-objs := x52joy.o x52joy_commands.o x52joy_input.o +saitek_x52-objs := hid-saitek-x52.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) diff --git a/kernel_module/README.md b/kernel_module/README.md index 9827fba..9c1acce 100644 --- a/kernel_module/README.md +++ b/kernel_module/README.md @@ -1,9 +1,29 @@ -Kernel driver for Saitek X52 Pro -================================ +Kernel driver for Saitek X52 and X52 Pro +======================================== -This folder contains a loadable kernel module for the Saitek X52Pro HOTAS. -Note that the standard usbhid driver already supports this joystick as an -input device, and the additional functionality of the LEDs and MFD can be -handled by a userspace library. +This folder contains a loadable kernel module for the Saitek X52 and X52 Pro +HOTAS. This improves upon the standard `hid-generic` driver by reporting both +the left-right and up-down motion of the thumb stick. -**This module is only a proof-of-concept and not suitable for production** +However, it changes the buttons that are reported by the joystick, and thus, +may not be suitable for all applications. + +# Building + +This directory is deliberately not integrated with the top level Autotools +based build framework. + +Install the Linux headers for the currently running kernel. On Ubuntu, this +can be done by running `sudo apt-get install -y linux-headers-$(uname -r)`. + +Run `make`. This will build the module from source. + +# Installing the kernel module + +Once you have built the kernel module, run `sudo insmod saitek_x52.ko` from +the current directory. With a recent enough kernel, the driver should switch +automatically. Otherwise, simply disconnect and reconnect your X52. + +# Reporting issues + +Please report any issues seen as a [Github issue](https://github.com/nirenjan/x52pro-linux/issues). diff --git a/kernel_module/hid-saitek-x52.c b/kernel_module/hid-saitek-x52.c new file mode 100644 index 0000000..b138b6e --- /dev/null +++ b/kernel_module/hid-saitek-x52.c @@ -0,0 +1,238 @@ +/* + * 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 +#include +#include + +#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[5]); + input_report_abs(input_dev, ABS_MISC, data[6]); + + /* 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]); +} + +static void _parse_button_report(struct input_dev *input_dev, + u8 *data, int num_buttons) +{ + int i; + int idx; + int btn; + + 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_TRIGGER_HAPPY + 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, 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, 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 int x52_probe(struct hid_device *dev, const struct hid_device_id *id) +{ + int ret; + + hid_set_drvdata(dev, NULL); + ret = hid_parse(dev); + if (ret != 0) { + hid_err(dev, "parse failed\n"); + return ret; + } + + ret = hid_hw_start(dev, HID_CONNECT_DEFAULT); + if (ret != 0) { + hid_err(dev, "hw start failed\n"); + return ret; + } + + return 0; +} + +static void x52_remove(struct hid_device *dev) +{ + hid_hw_stop(dev); +} + +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, + .probe = x52_probe, + .remove = x52_remove, + .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");