Project Mouse Tailor

Making the world safe from Wayland!

Every linux distro seems to be pushing to be the first to eradicate X11 and only ship Wayland. As is always the case when they throw out the old obsolete code for the new improved shiny much much better code, there are vital features that completely vanish.

In the case of Wayland, one of those features is input handling for the mouse. No more button mapping. No more drag lock. All collateral damage from the improvements.

Oh, but the compositor should handle that, they say. Trouble is every GUI is likely to have a different compositor (whatever that is), and so different GUI interfaces may or may not handle it at all, or may handle it completely differently. At the moment, none of the GUIs under Wayland seem to handle it at all. (And this is after years of development).

So what to do?

Screw it. Fix it in hardware so it doesn't matter how pitiful the damn GUI support is, I'll still be able to tailor my mouse to work the way I want it to work.

I've now ordered a Teensy 4.1, an arduino-like board with both USB client and USB host interfaces. So I should be able to plug in the mouse and remap buttons and implement drag lock entirely on the teensy, and it can send the altered mouse commands to the computer running Wayland. I spit on your puny compositor!

I'll see if I can accomplish anything when the parts get here...

Phase 1 plan: Hard code the features I want.

Phase 2 plan: Allow configuration of many more mappings and functions from file on the sdcard so no reprogramming is required.

Hey! The post office actually delivered the teensy early, and I've been fooling with the USB host interface (after soldering in the headers I needed to connect the cable).

Here's the first code to just print everything that shows up from the mouse. Seems to work OK.

sketch1.cpp

// Just deal with mice...
// This seems to be just about the minimum code required to notice the
// mouse being connected and disconnected and process all the mouse
// events. Should be able to only enable the mouse output when a mouse
// input is connected to avoid trouble with uploading new code.

#include "USBHost_t36.h"

USBHost myusb;
USBHIDParser hid(myusb);
bool hid_active = false;
MouseController mouse(myusb);

uint32_t last_buttons=0;

void ProcessMouseData() {
  uint32_t cur_buttons;
  int cur_x;
  int cur_y;
  int cur_wheel;
  int cur_wheelh;

  if (mouse.available()) {
    cur_buttons = mouse.getButtons();
    if (cur_buttons != last_buttons) {
       Serial.printf("Buttons changed from 0x%x to 0x%x\n",
                     last_buttons,cur_buttons);
       last_buttons = cur_buttons;
    }
    cur_x = mouse.getMouseX();
    if (cur_x != 0) {
       Serial.printf("X moved %d\n",cur_x);
    }
    cur_y = mouse.getMouseY();
    if (cur_y != 0) {
       Serial.printf("Y moved %d\n",cur_y);
    }
    cur_wheel = mouse.getWheel();
    if (cur_wheel != 0) {
       Serial.printf("Wheel moved %d\n",cur_wheel);
    }
    cur_wheelh = mouse.getWheelH();
    if (cur_wheelh != 0) {
       Serial.printf("Wheelh moved %d\n",cur_wheelh);
    }
    mouse.mouseDataClear();
  }
}

void setup()
{
  Serial1.begin(2000000);
  while (!Serial && millis() < 3000) ; // wait for Arduino Serial Monitor
  Serial.println("\n\nUSB Host Testing");
  myusb.begin();
}

void loop()
{
  myusb.Task();

  if ((bool)hid != hid_active) {
     hid_active = ! hid_active;
     if (hid_active) {
        Serial.printf("Device %x:%x - connected\n", hid.idVendor(),
                       hid.idProduct());
        const uint8_t *psz = hid.manufacturer();
        if (psz && *psz) Serial.printf("  manufacturer: %s\n", psz);
        psz = hid.product();
        if (psz && *psz) Serial.printf("  product: %s\n", psz);
        psz = hid.serialNumber();
        if (psz && *psz) Serial.printf("  Serial: %s\n", psz);
     } else {
        Serial.println("Device disconnected.");
     }
  }
  ProcessMouseData();
}

To test passing all mouse data through to the computer, I added a helpful interface to the code in hardware/teensy/avr/cores/teensy4/usb_mouse.[ch].

patch.txt

--- orig/arduino-1.8.19/hardware/teensy/avr/cores/teensy4/usb_mouse.h	2022-01-03 16:07:38.265807198 -0500
+++ arduino-1.8.19/hardware/teensy/avr/cores/teensy4/usb_mouse.h	2022-01-01 00:37:46.550349988 -0500
@@ -44,6 +44,7 @@
 void usb_mouse_configure(void);
 int usb_mouse_buttons(uint8_t left, uint8_t middle, uint8_t right, uint8_t back, uint8_t forward);
 int usb_mouse_move(int8_t x, int8_t y, int8_t wheel, int8_t horiz);
+int usb_mouse_mask_and_move(uint8_t mask, int8_t x, int8_t y, int8_t wheel, int8_t horiz);
 int usb_mouse_position(uint16_t x, uint16_t y);
 void usb_mouse_screen_size(uint16_t width, uint16_t height, uint8_t mac);
 extern uint8_t usb_mouse_buttons_state;
@@ -98,6 +99,9 @@
 			usb_mouse_move(0, 0, 0, 0);
 		}
 	}
+	void mask_and_move(uint8_t b, int8_t x, int8_t y, int8_t wheel, int8_t horiz) {
+	        usb_mouse_mask_and_move(b,x,y,wheel,horiz);
+	}
         bool isPressed(uint8_t b = MOUSE_ALL) {
 		return ((usb_mouse_buttons_state & (b & MOUSE_ALL)) != 0);
 	}
--- orig/arduino-1.8.19/hardware/teensy/avr/cores/teensy4/usb_mouse.c	2022-01-03 16:07:38.262807207 -0500
+++ arduino-1.8.19/hardware/teensy/avr/cores/teensy4/usb_mouse.c	2022-01-01 00:34:57.020952842 -0500
@@ -190,6 +190,11 @@
 	return usb_mouse_transmit(buffer, 6);
 }
 
+int usb_mouse_mask_and_move(uint8_t mask, int8_t x, int8_t y, int8_t wheel, int8_t horiz) {
+        usb_mouse_buttons_state = mask;
+        return usb_mouse_move(x,y,wheel,horiz);
+}
+
 int usb_mouse_position(uint16_t x, uint16_t y)
 {
 	if (x >= usb_mouse_resolution_x) x = usb_mouse_resolution_x - 1;

Then I built this new sketch which does indeed pass all the mouse data through the teensy.

sketch2.cpp

// Just deal with mice...
// This version gets rid of the serial output and just passes all mouse
// events through from host to computer.

#include "USBHost_t36.h"

USBHost myusb;
USBHIDParser hid(myusb);
MouseController mouse(myusb);

void ProcessMouseData() {
  uint32_t cur_buttons;
  int cur_x;
  int cur_y;
  int cur_wheel;
  int cur_wheelh;

  if (mouse.available()) {
    cur_buttons = mouse.getButtons();
    cur_x = mouse.getMouseX();
    cur_y = mouse.getMouseY();
    cur_wheel = mouse.getWheel();
    cur_wheelh = mouse.getWheelH();
    Mouse.mask_and_move(cur_buttons,cur_x,cur_y,cur_wheel,cur_wheelh);
    mouse.mouseDataClear();
  }
}

void setup()
{
  myusb.begin();
}

void loop()
{
  myusb.Task();
  ProcessMouseData();
}

With that proof of concept, I can now proceed to try and modify the mouse buttons to implement button mapping and drag lock independent of linux.

But first! A case for the gadget. The first issue is that the USB host cable is really long. I cobbled up a shorter one from a cable I unscrewed from an old USB connector bracket for a PC:

Still doesn't make for a really elegant design, but I ought to be able to cobble something up and I don't want to wait for ordering any kind of more compact connectors or soldering wires directly to the teensy.

After a few test prints, I have the first half of the case printed:

Not compact or elegant, but should keep me from shorting something out or screwing up a connector. The base of the new cable also has a handy shape for capturing in the case design.

The other half has printed now and I've finally got the teensy protected:

Everything seems solid as a rock. The test code still works with the teensy in the case. Now I can see about finishing the code...

It works! This code emulates the exact same behavior as the X11 libinput settings I've been using:

draglock.cpp

// Quick and dirty code to implement drag lock independent of the X11
// libinput code I've been using to do this:
//
//    xinput --set-button-map "$nm" 1 8 3 4 5 6 7 2
//    xinput --set-prop --type=int --format=8 "$nm" \
//       'libinput Drag Lock Buttons' 2 1
//
// Since no Wayland compositor seems to have any support for such
// "complicated" mouse settings, I figure doing it in microcode
// before it gets to linux would free me up from dependence on
// pitiful "new and improved" software.
//
// This version is just a proof of concept. It doesn't bother checking to
// see that the real trackball was plugged in or provide any way to do
// anything other than just emulate exactly what I've always done with X11.
// I used this a while now, and it appears to behave identically to
// the X11 libinput tweaks above, only with no tweaks required.

#include "USBHost_t36.h"

class mouse_state {
public:
   mouse_state();

   mouse_state(MouseController & mouse);

   mouse_state &
   operator=(const mouse_state & cpy);

   bool
   same_state(const mouse_state & cmp) const;

   uint32_t
   get_buttons() const {
         return cur_buttons;
      }

   int
   get_x() const {
         return cur_x;
      }

   int
   get_y() const {
         return cur_y;
      }

   int
   get_wheel() const {
         return cur_wheel;
      }

   int
   get_wheelh() const {
         return cur_wheelh;
      }

   void
   set_buttons(uint32_t new_buttons) {
         cur_buttons = new_buttons;
      }

private:
  uint32_t cur_buttons;
  int cur_x;
  int cur_y;
  int cur_wheel;
  int cur_wheelh;
};

mouse_state::mouse_state()
 : cur_buttons(0),
   cur_x(0),
   cur_y(0),
   cur_wheel(0),
   cur_wheelh(0) {
}

mouse_state &
mouse_state::operator=(const mouse_state & cpy) {
   cur_buttons = cpy.cur_buttons;
   cur_x = cpy.cur_x;
   cur_y = cpy.cur_y;
   cur_wheel = cpy.cur_wheel;
   cur_wheelh = cpy.cur_wheelh;
   return *this;
}

mouse_state::mouse_state(MouseController & mouse) {
   cur_buttons = mouse.getButtons();
   cur_x = mouse.getMouseX();
   cur_y = mouse.getMouseY();
   cur_wheel = mouse.getWheel();
   cur_wheelh = mouse.getWheelH();
}

bool
mouse_state::same_state(const mouse_state & cmp) const {
   return (cur_buttons == cmp.cur_buttons) &&
          (cur_x == cmp.cur_x) &&
          (cur_y == cmp.cur_y) &&
          (cur_wheel == cmp.cur_wheel) &&
          (cur_wheelh == cmp.cur_wheelh);
}

USBHost myusb;
USBHIDParser hid(myusb);
MouseController mouse(myusb);
bool dragging = false;

void ProcessMouseData() {
  mouse_state prev_state;

  if (mouse.available()) {
    mouse_state cur_state(mouse);
    uint32_t mask = cur_state.get_buttons();
    if ((mask & 0x04) == 0x04) {
       dragging = ! dragging; // toggle dragging flag when A is pressed
       mask ^= 0x04;          // turn off button A in mask
    }
    if (dragging) {
       mask |= 0x01;          // while dragging report button 1 is pressed
    }
    if ((mask & 0x08) == 0x08) {
       mask ^= 0x08;          // always change button D to button A
       mask |= 0x04;
    }
    cur_state.set_buttons(mask);
    Mouse.mask_and_move(cur_state.get_buttons(),
                        cur_state.get_x(),
                        cur_state.get_y(),
                        cur_state.get_wheel(),
                        cur_state.get_wheelh());
    prev_state = cur_state;
    mouse.mouseDataClear();
  }
}

void setup()
{
  myusb.begin();
}

void loop()
{
  myusb.Task();
  ProcessMouseData();
}

I can think of lots of fancy enhancements now like allowing the drag to be released by clicking other buttons, and allowing drag of other buttons to be specified by clicking something other than button1 while holding down the drag button, but those are all enhancements for another day. I'll use this under wayland for a while to see if anything else makes wayland impossible to use...

I'm now running wayland on the computer I use to run jiggit with this gadget standing between me and wayland, and it works great! (I have also ordered a shorter cable to reduce clutter :-).

Page last modified Mon Jan 3 16:24:42 2022