Logitech Performance MX in Linux

I’ll keep it short: After being inspired by this blog post I decided it was about time the “Performance MX” got support for setting its DPI value in Linux.

After some USB sniffing and poking at a few registers I was successful. Compile the following code with
“gcc -o performance_mx_dpi performance_mx_dpi.c -lusb-1.0 –std=c99”.

/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 *
 * Author: Alexander Hofbauer 
 *
 * Parts of this file were heavily inspired by the work of Julien Danjou
 * (http://julien.danjou.info).
 */

#include 

#include 
#include 
#include 
#include 
#include 

#define W_INDEX 	2
#define W_VALUE 	0x0210
#define W_LENGTH 	7

#define CTRL_LED 	"\x10\x01\x80\x51\x00\x00\x00"
#define STATIC 		2
#define FLASHING	3
#define UPPER 		4
#define MIDDLE 		0
#define LOWER 		12
#define RED 		8
#define ALL_OFF 	1 << 0 | 1 << 4 | 1 << 8 | 1 << 12

#define CTRL_SENS 	"\x10\x01\x80\x63\x00\x00\x00"
#define SENS_MIN 	'\x80'


// STATIC or FLASHING
#define LED_MODE 	FLASHING


void set_matrix(unsigned char* matrix, const char* ctrl, uint16_t bits) {
	for (int i = 0; i < W_LENGTH; i++) {
		matrix[i] = ctrl[i];
	}

	matrix[4] = (bits >> 8);
	matrix[5] = bits;
}

int set_sensitivity(libusb_device_handle *handle, uint16_t sens) {
	unsigned char matrix[7];
	set_matrix(matrix, CTRL_SENS, 0);
	matrix[4] = sens;

	return libusb_control_transfer(handle,
			LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
			HID_REQ_SET_REPORT, W_VALUE, W_INDEX, matrix,
			sizeof(matrix), 0);
}

int set_leds(libusb_device_handle *handle, uint16_t leds) {
	unsigned char matrix[7];
	set_matrix(matrix, CTRL_LED, leds);

	return libusb_control_transfer(handle,
			LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE,
			HID_REQ_SET_REPORT, W_VALUE, W_INDEX, matrix,
			sizeof(matrix), 0);
}


void set_sensitivity_value(libusb_device_handle *handle, uint8_t sensitivity) {
	set_sensitivity(handle, SENS_MIN | sensitivity);
	set_leds(handle, ALL_OFF);

	switch ((sensitivity - 1) / 3) {
		case 0:
			set_leds(handle, (LED_MODE << LOWER));
			break;
		case 1:
			set_leds(handle, (LED_MODE << MIDDLE));
			break;
		case 2:
			set_leds(handle, (LED_MODE << UPPER));
			break;
		case 3:
			set_leds(handle, (LED_MODE << LOWER) | (LED_MODE << MIDDLE));
			break;
		default:
			set_leds(handle, (LED_MODE << LOWER) | (LED_MODE << MIDDLE)
					| (LED_MODE << UPPER));
			break;
	}
}



int main(int argc, char* argv[]) {
	if (argc != 2) {
		fprintf(stderr, "Please specify a sensitivity value between 1 and 16\n");
		return EXIT_FAILURE;
	}

	int sensitivity = atoi(argv[1]);
	if (sensitivity < 1 || sensitivity > 16) {
		fprintf(stderr, "Possible sensitivity value from 1 to 16\n");
		return EXIT_FAILURE;
	}

	libusb_context *ctx;
	libusb_init(&ctx);
	libusb_set_debug(ctx, 3);

	libusb_device_handle *device_handle = libusb_open_device_with_vid_pid(
			ctx, 0x046d, 0xc52b);

	if (device_handle == NULL) {
		return EXIT_FAILURE;
	}

	libusb_device *device = libusb_get_device(device_handle);

	struct libusb_device_descriptor desc;
	const struct libusb_interface_descriptor *iface_desc = NULL;

	libusb_get_device_descriptor(device, &desc);

	for (uint8_t i = 0; i < desc.bNumConfigurations; i++) {
		struct libusb_config_descriptor *config;

		libusb_get_config_descriptor(device, i, &config);

		const struct libusb_interface *iface = &config->interface[W_INDEX];

		for (int j = 0; j < iface->num_altsetting; j++) {
			iface_desc = &iface->altsetting[j];

			if (iface_desc->bInterfaceClass == LIBUSB_CLASS_HID) {
				break;
			} else {
				iface_desc = NULL;
			}
		}

		if (iface_desc != NULL) {
			break;
		}
	}

	if (iface_desc == NULL) {
		fprintf(stdout, "Could not find valid descriptor\n");
		return EXIT_FAILURE;
	}

	libusb_detach_kernel_driver(device_handle, W_INDEX);
	libusb_claim_interface(device_handle, W_INDEX);

	fprintf(stdout, "Setting sensitivity to %d\n", sensitivity);
	set_sensitivity_value(device_handle, sensitivity);

	libusb_release_interface(device_handle, W_INDEX);
	libusb_attach_kernel_driver(device_handle, W_INDEX);

	libusb_close(device_handle);
	libusb_exit(ctx);

	return EXIT_SUCCESS;
}

13 thoughts on “Logitech Performance MX in Linux”

  1. First, let me say thanks! I’ve been waiting for someone to do this for MONTHS. I would’ve done it myself if I had the knowledge, which I’ll attempt to acquire.

    First, what software did you use to sniff USB? Secondly, does the mouse send any packet once it turns on, so that we may set the resolution to a chosen value at power-on?

    Yet again, thanks.

  2. Yes, that’s unfortunately something that makes me mad as well. Hope I’ll soon find the time to investigate this further. For the time I’m using xbindkeys to set the DPI value whenever I reactivate the mouse.

    As for the sniffing part: Everything was basically said in the blog post I linked above.

    The kernel module usbmon in combination with Wireshark and a virtual machine (VirtualBox) running SetPoint are all you need. Enable USB pass-through of the receiver and deactivate mouse pointer integration for the VM. You’ll need a second pointing device (or else a lot of patience).

    In Wireshark you’ll see a port generating lots of packages as soon as you move the mouse – that’s the port to listen to. When tracing you can narrow the results by filtering the packages coming from the Performance MX, e.g. “usb.device_address == 5”.

    1. Thanks for your reply! Maybe I’ll have ago at reading the battery status. I also have a plan to make a more generic utility to which we’ll be able to add support for different mice and keyboards, as well as pairing/unpairing devices to the unifying receiver (pairing code exists) . What do you think?

      1. Julien Danjou has already put together some test-patches for unifying receivers.

        We also poked around a bit and found out the Performance MX seems to report either ‘Good’, ‘Medium’ or ‘Bad’ as battery charge (and not percentage values). I’m still waiting for my mouse’s battery to go down enough to report a different value to confirm we are poking at the correct register.

        1. I’ve been poking around a bit with the HID++2.0 specification and hidraw, and I’ve managed to get a few (confusing) replies. I’d love to see how you’re doing. If I send a ping (10 01 00 10 00 00 AA hex), I get a (10 01 8F 00 10 01 00 hex) response, meaning it’s a HID++1.0 device according to the spec. Has the HID++1.0 spec ever been released?

  3. Thanks, works perfectly! Thought I should let you know that I have created a repository at GitHub that holds your code and an additional Makefile for convenience.

  4. This is nice, thanks. Is there a way to read current dpi value. It would be nice to be able to increase or decrease current value by one (and then bind that to zoom+wheel or similar…).

    1. Should be possible without any problems at all. I guess the Solaar devs will have acquired quite some knowledge of Logitech hardware.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.