Logitech G400s hack

May 22, 2019 - Reading time: 8 minutes

No proper driver support on Linux for my Logitech G400s mouse, so I need to come up with something to have some kind of driver/support/configuration

I eventually found and used the following tools, made some config files and a script that I start or restart if needed (for example if I plug the mouse to another port, or if I boot to Windows it will change the mouse internal driver mode because of the installed Logitech drivers, so I will need to force the script again when rebooted on Linux).

evrouter (used to assign keyboard keys to some mouse buttons not recognized by system)
imwheel (used to have program specific mouse button configs)
g400s_hack (used to set driver mode and dpi in the mouse as fixed settings, without it I can't use the buttons around mouse wheel as they would change mouse dpi on the fly (because of wrong driver mode) and/or would not be recognized by the system at all)

First step is to compile g400s_hack (program that I found after reading some issues people had with a custom driver for the G400 mouse), pretty straight forward with the provided instruction, simply run in terminal: gcc -o g400s_hack g400s_hack.c $(pkg-config libusb-1.0 --cflags --libs)

//
// Change DPI and driver mode on Logitech "G400s Gaming Mouse"
// By Matti 'ccr' Hamalainen <ccr@tnsp.org> https://tnsp.org/~ccr/files/g400s_hack.c
//
// Based on https://bitbucket.org/extaliones/g400_hack/raw/b1a0f430dcb1c10991294447bf4f74a6acfff748/g400_hack.c
// by Przemek Aksamit <extaliones dot gmail.com>
//
// Compilation: gcc -o g400s_hack g400s_hack.c $(pkg-config libusb-1.0 --cflags --libs)
//
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <libusb-1.0/libusb.h>

#define G400_VENDOR_ID  0x046d
#define G400_PRODUCT_ID 0xc24c

void errmsg(const char *str, int ret)
{
  printf("%s [%d: %s / %s]\n",
    str, ret,
    libusb_error_name(ret),
    libusb_strerror(ret));
}

int main(int argc, char *argv[])
{
  libusb_context *usb_ctx = NULL;
  libusb_device_handle *usb_handle = NULL;
  int usb_detached = 0;
  int usb_claimed  = 0;
  int ret, dpi_idx, dmode = -1;
  char *arg;

  if (argc < 3)
  {
    printf(
      "Usage: %s <dpi> <mode>\n"
      "<dpi> is DPI sensitivity:\n"
      "   400|800|1800|3600\n"
      "\n"
      "<mode> is 'driver mode' value:\n"
      "   0 / off = 'driverless', special button events NOT sent to host\n"
      "   1 / on  = driver mode, special button events ARE sent to host\n"
      "\n", argv[0]);
    return 1;
  }

  // 0 => 400dpi, 1 => 800dpi, 2 => 1800dpi, 3 => 3600dpi
  arg = argv[1];
  switch (arg[0])
  {
    case '4': dpi_idx = 0; break;
    case '8': dpi_idx = 1; break;
    case '1': dpi_idx = 2; break;
    case '3': dpi_idx = 3; break;
    default:
      printf("Invalid DPI value '%s'.\n", arg);
      goto out;
  }

  arg = argv[2];
  if (strncasecmp(arg, "on", 2) == 0 ||
      strncasecmp(arg, "en", 2) == 0)
    dmode = 1;
  else
  if (strncasecmp(arg, "of", 2) == 0 ||
      strncasecmp(arg, "di", 2) == 0)
    dmode = 0;
  else
    dmode = atoi(arg);

  if (dmode != 0 && dmode != 1)
  {
    printf("Invalid driver mode value '%s', must be 0/off/disable OR 1/on/enable.\n",
      arg);
    goto out;
  }

  // Initialize libusb and find device
  if ((ret = libusb_init(&usb_ctx)) != 0)
  {
    errmsg("LibUSB initialization failed!", ret);
    goto out;
  }

  usb_handle = libusb_open_device_with_vid_pid(usb_ctx, G400_VENDOR_ID, G400_PRODUCT_ID);
  if (usb_handle == NULL)
  {
    printf("Logitech G400s not found! (Do you have permissions to the usb devices? Try with sudo?)\n");
    goto out;
  }

  // Check if there is a driver attached
  if (libusb_kernel_driver_active(usb_handle, 1) == 1)
  {
    // Attempt to detach it temporarily
    if ((ret = libusb_detach_kernel_driver(usb_handle, 1)) != 0)
    {
      errmsg("Can't detach kernel driver.", ret);
      goto out;
    }
    usb_detached = 1;
  }

  // Claim the interface for our purposes
  if ((ret = libusb_claim_interface(usb_handle, 1)) != 0)
  {
    errmsg("Failed to claim interface.", ret);
    goto out;
  }
  usb_claimed = 1;

  // Send the data
  printf("Setting DPI index to %d, driver mode is %s.\n",
    dpi_idx, dmode ? "ENABLED" : "DISABLED");

  if ((ret = libusb_control_transfer(usb_handle,
    0x40, 2, 0x008e,
    (0x03 + dpi_idx) |     // The two lowest bits seem to be needed, results in error otherwise
    (dmode ? 0x80 : 0x00), // The 8th bit seems to control the driver mode
    0, 0, 1000)) != 0)
  {
    errmsg("Error writing to USB device!", ret);
    goto out;
  }

  printf("Successfully finished.\n");

  // Shut down
out:
  if (usb_handle != NULL)
  {
    if (usb_claimed)
      libusb_release_interface(usb_handle, 1);

    if (usb_detached)
      libusb_attach_kernel_driver(usb_handle, 1);

    libusb_close(usb_handle);
  }

  if (usb_ctx != NULL)
    libusb_exit(usb_ctx);

  return 0;
}

Then I moved the compiled binary to /usr/bin/g400s_hack

I made a basic script that I run to enforce the driver mode and dpi (to have similar behavior I used to have on Windows), and reload evrouter and imwheel all at once.

#!/usr/bin/bash
#

# kill evrouter and clean its left over temp files to be able to restart it (it doesn't clean by itself)
sudo /usr/bin/evrouter -q
sudo /usr/bin/rm -f /tmp/.evrouter*

# use driver mode hack https://tnsp.org/~ccr/files/g400s_hack.c and set dpi to max
sudo /usr/bin/g400s_hack 3600 1

#restart evrouter
sudo /usr/bin/evrouter --config=/home/omano/.evrouterrc /dev/input/by-id/usb-Logitech_G400s_Optical_Gaming_Mouse-event-mouse

#restart imwheel
imwheel -k

Note that event* can (should) be replace with event6 in my case (where the mouse is actually) Note that I use /dev/input/by-id/ to perfectly match the mouse all the time now, since I did that, evrouterrc never crashes (it used to sometimes and I had to restart the script).

I was used to have a backspace button in front of the mouse wheel on Windows, and also had Enter and Escape key configured by default on the thumb buttons, I was doing that with Logitech drivers, but I can do that too on Linux, with the following configuration files doing what I wanted (enabling the non working button around mouse wheel and assigning them keyboard keys)

~/.evrouterrc

"Logitech G400s Optical Gaming Mouse" "/dev/input/by-id/usb-Logitech_G400s_Optical_Gaming_Mouse-event-mouse" any key/278 "XKey/BackSpace"
"Logitech G400s Optical Gaming Mouse" "/dev/input/by-id/usb-Logitech_G400s_Optical_Gaming_Mouse-event-mouse" any key/279 "XKey/Escape"
"Logitech G400s Optical Gaming Mouse" "/dev/input/by-id/usb-Logitech_G400s_Optical_Gaming_Mouse-event-mouse" any key/277 "XKey/Delete"

and having some specific mouse config for specific programs (here all it does is changing the thumb buttons for Enter and Escape keys for a few programs)

~/.imwheelrc

"^Firefox"
    None, Thumb1, Escape
    None, Thumb2, Return

"^Navigator"
    None, Thumb1, Escape
    None, Thumb2, Return

"^Mozilla"
    None, Thumb1, Escape
    None, Thumb2, Return

"^konsole"
    None, Thumb1, Escape
    None, Thumb2, Return

"^kate"
    None, Thumb1, Escape
    None, Thumb2, Return

"^Steam"
    None, Thumb1, Escape
    None, Thumb2, Return

"^Thunderbird"
    None, Thumb1, Escape
    None, Thumb2, Return

"^TeamSpeak"
    None, Thumb1, Escape
    None, Thumb2, Return

"^filezilla"
    None, Thumb1, Escape
    None, Thumb2, Return

"^ark"
    None, Thumb1, Escape
    None, Thumb2, Return

"^smplayer"
    None, Thumb1, Escape
    None, Thumb2, Return

"^meld"
    None, Thumb1, Escape
    None, Thumb2, Return

"^kompare"
    None, Thumb1, Escape
    None, Thumb2, Return
    

Not perfect, but does the job. I just can't do the macros I was able to do on Windows with the mouse drivers.. for now.. can live with that.

I just have to run the script to reload any change or to reset driver/driverless mode when needed (reboot on Windows with Logitech app reconfiguring mouse internals..)