Skip to content

Instantly share code, notes, and snippets.

@xkr47
Last active August 3, 2020 22:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xkr47/98ffa6b44b197a7a38435ebf9c2fe688 to your computer and use it in GitHub Desktop.
Save xkr47/98ffa6b44b197a7a38435ebf9c2fe688 to your computer and use it in GitHub Desktop.
Logitech TrackMan Marble FX scroll wheel patch for the Linux kernel

Logitech TrackMan Marble FX scroll wheel patch for the Linux kernel

NOTE I now have a standalone driver available here which is easier to install: https://github.com/xkr47/marblefx

This patch alters the usbmouse kernel driver to support a "scroll wheel mode" using the fourth button (the red button) when connected through a "ID 04d9:1400 Holtek Semiconductor, Inc. PS/2 keyboard + mouse controller" usb-to-ps2 adapter. No idea how things work with other adapters.

It seems the fourth button generates events, but the state of the button is not represented in any bits (at least by said usb-to-ps2 adapter).

However since "no buttons pressed" is only ever reported when releasing a single button being pressed, detecting multiple "no buttons pressed" events can be used to identify usage of the fourth button.

Therefore, by clicking the fourth button we get two "no buttons pressed" events, and the code then toggles the "mode" between scroll and normal.

So instead of keeping the fourth button pressed while scrolling, you click it once (while not moving the mouse or having any buttons clicked), then scroll the wheel and finally click the fourth button again to go back to normal mode. I find this good enough to be usable/useful.

The module logs warning messages to syslog when the scroll state changes. Comment out the line dev_warn(...) if you don't want it.

Installation

To compile & install the module for your current kernel, run the install.sh script attached to this gist:

./install.sh

Typically mouse devices use the usbhid driver, but I patched the simpler usbmouse driver for my purpose. Both usbhid and usbmouse drivers thus compete over access to the mouse when both are loaded. My script updates /etc/default/grub to blacklist the specific usb-to-ps2 adapter from being associated with usbhid so usbmouse always gets control of it. You'll have to run update-grub once after first installation. After kernel is updated, just run install.sh again. Please note that the mouse might not work very well until you run it (axis swapped, buttons not working etc) because the default usbmouse driver is a rather naive implementation of the protocol. You might even want to run it in some startup script on bootup. If the module is already compiled for the current kernel it skips the unnecessary steps so it doesn't really slow things down unnecessarily.

Notes

Patch originally implemented against ubuntu 16.10 kernel 3.19.0-82 sources. Works at least up to 4.8.

As documented in the HID report descriptor (used trick documented here to extract it ), the mouse (or the usb-to-ps2 adaptor) also includes a report id in front of data otherwise returned, so I had to increment all data offsets by one.

#!/bin/sh
main () {
set -ex
cd /usr/src
ver=$(uname -r)
dir=linux-${ver%%-*}
if [ -e /lib/modules/${ver}/usbmouse-ok ]; then
echo "Already built; loading.."
else
rm -rf /usr/src/${dir}
apt-get source linux-image-${ver}
apt-get build-dep linux-image-${ver}
cd ${dir}
cp -va /boot/config-${ver} .config
ln -vsnf ../linux-headers-${ver}/Module.symvers .
versuf=${ver#*.*.}
versublevel=${versuf%%-*}
verextra=${versuf#*-}
[ ! "$verextra" ] || verextra=-${verextra}
perl -i -pe 's!^(SUBLEVEL =).*!$1 '"$versublevel"'!;s!^(EXTRAVERSION =).*!$1 '"$verextra"'!;' Makefile
patch -p0 < $HOME/config/patches/linux-3.19.0-logitech-trackman-marble-fx.patch
make oldconfig
make prepare
make modules_prepare
make SUBDIRS=scripts/mod
make SUBDIRS=drivers/hid/usbhid modules
cp -va drivers/hid/usbhid/usbmouse.ko /lib/modules/${ver}/kernel/drivers/hid/usbhid/
if [ ! -e /etc/modules-load.d/modules.conf ]; then
echo "Unable to add 'usbmouse' to modules to be loaded"
exit 1
fi
if ! egrep -q '^usbmouse$' /etc/modules-load.d/modules.conf ; then
echo usbmouse >> /etc/modules-load.d/modules.conf
fi
if [ ! -e /etc/default/grub ]; then
echo "Unable to add quirks parameter for module usbhid to grub command line"
exit 1
fi
perl -i.usbmouse-bak -e '
while(<>) {
if (/^(GRUB_CMDLINE_LINUX_DEFAULT=".*)("\s*$)/) {
my ($x,$y)=($1,$2);
$found=1;
if ($x !~ m!usbhid.quirks!) {
$_ = $x." usbhid.quirks=0x4d9:0x1400:0x4".$y; # see below for clarification
}
}
print;
}
exit(1) unless($found);
' /etc/default/grub
perl -i.usbmouse-bak -pe 's!^blacklist usbmouse$!# using xkr47 version of usbmouse\n#blacklist usbmouse!' /etc/modprobe.d/*blacklist*
:> /lib/modules/${ver}/usbmouse-ok
fi
# reload with quirks parameter if needed
if ! grep -q 0x4d9:0x1400:0x4 /sys/module/usbhid/parameters/quirks ; then
modprobe -r usbhid ||:
modprobe usbhid quirks=0x4d9:0x1400:0x4 # 0x4d9:0x1400 = usb device id, 0x4 = action (blacklist)
fi
# reload our module
modprobe -r usbmouse ||:
modprobe usbmouse
cd /sys/bus/usb/drivers/usbhid/
drvs=(*/????:04D9:1400*/input/*/mouse*)
# reassign to our driver if already assigned to other driver
if [ ${#drvs[@]} = 1 -a -e "${drvs[0]}" ]; then
drv=${drvs[0]}
id=${drv%%/*}
echo $id > unbind
cd ../usbmouse
echo $id > bind
fi
# decrease scrolling speed (default 1 1 1)
devid=$(xinput |grep "TrackMan Marble FX"|egrep -o 'id=[0-9]+'|tr -d id=)
[ "$devid" ]
xinput set-prop $devid "Evdev Scrolling Distance" 8 8 1
}
main "$@" ; exit $?
Bus 003 Device 050: ID 04d9:1400 Holtek Semiconductor, Inc. PS/2 keyboard + mouse controller
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 0 (Defined at Interface level)
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x04d9 Holtek Semiconductor, Inc.
idProduct 0x1400 PS/2 keyboard + mouse controller
bcdDevice 1.43
iManufacturer 0
iProduct 0
iSerial 0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 59
bNumInterfaces 2
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xa0
(Bus Powered)
Remote Wakeup
MaxPower 100mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 1 Boot Interface Subclass
bInterfaceProtocol 1 Keyboard
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.10
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 65
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 0x0008 1x 8 bytes
bInterval 10
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 1 Boot Interface Subclass
bInterfaceProtocol 2 Mouse
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.10
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 157
Report Descriptor: (length is 157)
Item(Global): Usage Page, data= [ 0x01 ] 1
Generic Desktop Controls
Item(Local ): Usage, data= [ 0x02 ] 2
Mouse
Item(Main ): Collection, data= [ 0x01 ] 1
Application
Item(Global): Report ID, data= [ 0x01 ] 1
Item(Local ): Usage, data= [ 0x01 ] 1
Pointer
Item(Main ): Collection, data= [ 0x00 ] 0
Physical
Item(Global): Usage Page, data= [ 0x09 ] 9
Buttons
Item(Local ): Usage Minimum, data= [ 0x01 ] 1
Button 1 (Primary)
Item(Local ): Usage Maximum, data= [ 0x05 ] 5
Button 5
Item(Global): Logical Minimum, data= [ 0x00 ] 0
Item(Global): Logical Maximum, data= [ 0x01 ] 1
Item(Global): Report Count, data= [ 0x05 ] 5
Item(Global): Report Size, data= [ 0x01 ] 1
Item(Main ): Input, data= [ 0x02 ] 2
Data Variable Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Global): Report Count, data= [ 0x01 ] 1
Item(Global): Report Size, data= [ 0x03 ] 3
Item(Main ): Input, data= [ 0x01 ] 1
Constant Array Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Global): Usage Page, data= [ 0x01 ] 1
Generic Desktop Controls
Item(Local ): Usage, data= [ 0x30 ] 48
Direction-X
Item(Local ): Usage, data= [ 0x31 ] 49
Direction-Y
Item(Local ): Usage, data= [ 0x38 ] 56
Wheel
Item(Global): Logical Minimum, data= [ 0x81 ] 129
Item(Global): Logical Maximum, data= [ 0x7f ] 127
Item(Global): Report Size, data= [ 0x08 ] 8
Item(Global): Report Count, data= [ 0x03 ] 3
Item(Main ): Input, data= [ 0x06 ] 6
Data Variable Relative No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Main ): End Collection, data=none
Item(Main ): End Collection, data=none
Item(Global): Usage Page, data= [ 0x0c ] 12
Consumer
Item(Local ): Usage, data= [ 0x01 ] 1
Consumer Control
Item(Main ): Collection, data= [ 0x01 ] 1
Application
Item(Global): Report ID, data= [ 0x02 ] 2
Item(Global): Logical Minimum, data= [ 0x00 ] 0
Item(Global): Logical Maximum, data= [ 0x01 ] 1
Item(Local ): Usage, data= [ 0xe9 ] 233
Volume Increment
Item(Local ): Usage, data= [ 0xea ] 234
Volume Decrement
Item(Local ): Usage, data= [ 0xe2 ] 226
Mute
Item(Local ): Usage, data= [ 0xcd ] 205
Play/Pause
Item(Local ): Usage Minimum, data= [ 0xb5 ] 181
Scan Next Track
Item(Local ): Usage Maximum, data= [ 0xb8 ] 184
Eject
Item(Global): Report Size, data= [ 0x01 ] 1
Item(Global): Report Count, data= [ 0x08 ] 8
Item(Main ): Input, data= [ 0x02 ] 2
Data Variable Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Local ): Usage, data= [ 0x8a 0x01 ] 394
AL Email Reader
Item(Local ): Usage, data= [ 0x21 0x02 ] 545
AC Search
Item(Local ): Usage, data= [ 0x2a 0x02 ] 554
(null)
Item(Local ): Usage Minimum, data= [ 0x23 0x02 ] 547
AC Home
Item(Local ): Usage Maximum, data= [ 0x27 0x02 ] 551
AC Refresh
Item(Main ): Input, data= [ 0x02 ] 2
Data Variable Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Local ): Usage, data= [ 0x83 0x01 ] 387
AL Consumer Control Configuration
Item(Local ): Usage, data= [ 0x96 0x01 ] 406
AL Internet Browser
Item(Local ): Usage, data= [ 0x92 0x01 ] 402
AL Calculator
Item(Local ): Usage, data= [ 0x9e 0x01 ] 414
AL Terminal Local/Screensaver
Item(Local ): Usage, data= [ 0x94 0x01 ] 404
AL Local Machine Browser
Item(Local ): Usage, data= [ 0x06 0x02 ] 518
AC Minimize
Item(Local ): Usage, data= [ 0xb2 ] 178
Record
Item(Local ): Usage, data= [ 0xb4 ] 180
Rewind
Item(Main ): Input, data= [ 0x02 ] 2
Data Variable Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Main ): End Collection, data=none
Item(Global): Usage Page, data= [ 0x01 ] 1
Generic Desktop Controls
Item(Local ): Usage, data= [ 0x80 ] 128
System Control
Item(Main ): Collection, data= [ 0x01 ] 1
Application
Item(Global): Report ID, data= [ 0x03 ] 3
Item(Global): Logical Minimum, data= [ 0x00 ] 0
Item(Global): Logical Maximum, data= [ 0x01 ] 1
Item(Local ): Usage, data= [ 0x81 ] 129
System Power Down
Item(Local ): Usage, data= [ 0x82 ] 130
System Sleep
Item(Local ): Usage, data= [ 0x83 ] 131
System Wake Up
Item(Global): Report Count, data= [ 0x03 ] 3
Item(Global): Report Size, data= [ 0x01 ] 1
Item(Main ): Input, data= [ 0x02 ] 2
Data Variable Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Global): Report Count, data= [ 0x01 ] 1
Item(Global): Report Size, data= [ 0x05 ] 5
Item(Main ): Input, data= [ 0x01 ] 1
Constant Array Absolute No_Wrap Linear
Preferred_State No_Null_Position Non_Volatile Bitfield
Item(Main ): End Collection, data=none
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0008 1x 8 bytes
bInterval 10
Device Status: 0x0000
(Bus Powered)
--- drivers/hid/usbhid/usbmouse.c~ 2015-02-09 04:54:22.000000000 +0200
+++ drivers/hid/usbhid/usbmouse.c 2017-03-26 02:48:09.931574540 +0200
@@ -57,6 +57,8 @@
signed char *data;
dma_addr_t data_dma;
+ char num_zeros;
+ char mode;
};
static void usb_mouse_irq(struct urb *urb)
@@ -78,15 +80,26 @@
goto resubmit;
}
- input_report_key(dev, BTN_LEFT, data[0] & 0x01);
- input_report_key(dev, BTN_RIGHT, data[0] & 0x02);
- input_report_key(dev, BTN_MIDDLE, data[0] & 0x04);
- input_report_key(dev, BTN_SIDE, data[0] & 0x08);
- input_report_key(dev, BTN_EXTRA, data[0] & 0x10);
-
- input_report_rel(dev, REL_X, data[1]);
- input_report_rel(dev, REL_Y, data[2]);
- input_report_rel(dev, REL_WHEEL, data[3]);
+ if (!data[1] && !data[2] && !data[3]) {
+ mouse->num_zeros^=1;
+ if (!mouse->num_zeros) {
+ mouse->mode = !mouse->mode;
+ //dev_warn(&mouse->usbdev->dev, "mouse %s\n", mouse->mode ? "scroll" : "normal");
+ }
+ } else {
+ mouse->num_zeros = 0;
+ }
+
+ input_report_key(dev, BTN_LEFT, data[1+0] & 0x01);
+ input_report_key(dev, BTN_RIGHT, data[1+0] & 0x02);
+ input_report_key(dev, BTN_MIDDLE, data[1+0] & 0x04);
+ if (!mouse->mode) {
+ input_report_rel(dev, REL_X, data[1+1]);
+ input_report_rel(dev, REL_Y, data[1+2]);
+ } else {
+ input_report_rel(dev, REL_HWHEEL, data[1+1]);
+ input_report_rel(dev, REL_WHEEL, -data[1+2]);
+ }
input_sync(dev);
resubmit:
@@ -165,7 +178,7 @@
if (!strlen(mouse->name))
snprintf(mouse->name, sizeof(mouse->name),
- "USB HIDBP Mouse %04x:%04x",
+ "USB HIDBP Logitech TrackMan Marble FX Mouse @xkr47 %04x:%04x",
le16_to_cpu(dev->descriptor.idVendor),
le16_to_cpu(dev->descriptor.idProduct));
@@ -181,9 +194,7 @@
input_dev->keybit[BIT_WORD(BTN_MOUSE)] = BIT_MASK(BTN_LEFT) |
BIT_MASK(BTN_RIGHT) | BIT_MASK(BTN_MIDDLE);
input_dev->relbit[0] = BIT_MASK(REL_X) | BIT_MASK(REL_Y);
- input_dev->keybit[BIT_WORD(BTN_MOUSE)] |= BIT_MASK(BTN_SIDE) |
- BIT_MASK(BTN_EXTRA);
- input_dev->relbit[0] |= BIT_MASK(REL_WHEEL);
+ input_dev->relbit[0] |= BIT_MASK(REL_WHEEL) | BIT_MASK(REL_HWHEEL);
input_set_drvdata(input_dev, mouse);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment