Things to do with a chipKIT™ Lenny and a QuickIO. No. 2: Virtual Mouse

Number two in my series of what to do with a Lenny and a QuickIO.

How about a mouse? Yeah, a mouse. Honest :)

Well, maybe not an actual mouse, but maybe make it control the mouse pointer in an "almost" usable way...?

The two potentiometers on the QuickIO could control the mouse position, and the buttons can be the mouse buttons for clicking, etc. That should work.

This is going to get a little more complex, though, since we're going to do something slightly more advances with the chipKIT USB system - we're going to define our own USB setup. So you'll first have to make sure that the USB configuration in your chosen IDE is set to "Custom" (you'll find it in one of the menus somewhere). The reason we want to do this is that we don't want to use a Mouse device. A Mouse works with relative coordinates. An X coordinate of 100 means "move the cursor 100 pixels to the right". We don't want that - we want the potentiometers to relate to absolute positions on the screen - otherwise it would just be crazy with the mouse pointer zooming all over the place. So instead we want to use the Tablet device. This is just like a mouse, except it operates on absolute coordinates. When we give an X coordinate of 100 it means go to X coordinate 100.

So to begin with we want to create ourselves a custom USB setup. For that we need to create a USB interface device and bind it to the internal USB manager. Then create a HID Tablet device, and later on attach that device to the USB manager.

Normally all this is done for you by the core, but there's no default configuration that includes a tablet device, so we have to do it manually.

Creating the required components is simple enough though, you just use:

USBFS usbDevice;
USBManager USB(usbDevice, 0x04d8, 0x0f5e);
HID_Tablet Tablet(1024, 1024);

That creates a Full Speed USB device (the Lenny only supports Full Speed mode - for High Speed mode you need a PIC32MZ based board). Then it creates a USB Manager instance bound to that device and gives it a VID (0x4d8 is Microchip) and a PID (0x0f5e is one of chipKIT's allocated PIDs). You can choose your own VID and PID if you like (it's common to use 0xF055 for the VID - Free and 0pen 5ource 5oftware) or get one allocated by pid.codes. You can also optionally give the manager a manufacturer name, product name, and serial number, should you so wish.

Then we create the HID Tablet device and give it a resolution of 1024x1024. This sets the number of pixels the tablet interface has. This pair of dimensions covers the entirety of your screen, so in an ideal world it would be the same as the resolution of your display. However the potentiometers return values from 0 to 1023, so for simplicity we map the tablet dimensions to the input range - that way 100% on a potentiometer yields 100% of the screen width or height.

There's many more USB interface classes you can mix and match in your projects - MIDI, keyboard, CDCACM (and yes, you can have multiple instances of those), etc. For now we'll just stick to the single Tablet device.

Now connecting the Tablet device to the USB Manager and starting the whole USB stack up is as simple as adding, in setup(), this:

USB.addDevice(Tablet);
USB.begin();

Now your Lenny is a tablet interface. From here on in it's just a matter of calling Tablet.move(x,y); to move the mouse pointer to any position on the screen.

One quirk with the USB Tablet protocol is that if you send the same coordinates twice in a row it ignores the second set completely. It thinks "The pointer is already there. I don't need to move it". However, if some other pointing device has moved the pointer (such as your real mouse) then that won't be true. So it can be good to send two sets of coordinates one after the other to ensure that there is a change every time. Just a tiny twitch is all it needs.

One thing we don't want to do is have the tablet interface constantly running and moving the pointer to a specific location, since we'd never be able to move our mouse in any other way - the Lenny would keep moving the mouse back. So we'd want to implement some kind of method for turning it on and off. One of the QuickIO's buttons should do it.

Assign a couple of buttons to be the left and right mouse buttons as well, and we're good to go.

Here's the program I have put together that does all this. I'll dissect it afterwards:

USBFS usbDevice;
USBManager USB(usbDevice, 0x04d8, 0x0f5e);
HID_Tablet Tablet(1024, 1024);

void setup() {
    USB.addDevice(Tablet);
    USB.begin();
    pinMode(A0, INPUT_PULLUP);
    pinMode(A3, INPUT_PULLUP);
    pinMode(2, OUTPUT);
}

void loop() {
    static bool running = false;
    static bool activateState = HIGH;
    static bool leftClickState = HIGH;
    static bool rightClickState = HIGH;

    if (running) {
        int x = analogRead(A4);
        int y = 1023 - analogRead(A5);
//        Tablet.move(x - 1, y);
        Tablet.move(x, y);
    }

    bool leftClickValue = digitalRead(A0);
    if (leftClickValue != leftClickState) {
        leftClickState = leftClickValue;
        if (leftClickValue == LOW) {
            Tablet.press(MOUSE_LEFT);
        } else {
            Tablet.release(MOUSE_LEFT);
        }
    }

    bool rightClickValue = digitalRead(A1);
    if (rightClickValue != rightClickState) {
        rightClickState = rightClickValue;
        if (rightClickValue == LOW) {
            Tablet.press(MOUSE_RIGHT);
        } else {
            Tablet.release(MOUSE_RIGHT);
        }
    }

    bool activateValue = digitalRead(A3);
    if (activateValue != activateState) {
        activateState = activateValue;
        if (activateValue == LOW) {
            running = !running;
            digitalWrite(2, running);
        }
    }
}

I think everything in setup() is pretty much self-explanatory. So let's work through the loop. Let's start at the top:

static bool running = false;
static bool activateState = HIGH;
static bool leftClickState = HIGH;
static bool rightClickState = HIGH;

We need some variables to store the current state of play. The first one indicates if the tablet is "running" or not - that is, actively reading the potentiometers and sending the coordinates. The other three keep the state of each of the buttons we are using.

You may be wondering what this "static" thing is. Well, "static" means "keep your value between calls to the function". It works just like a global variable, but it's not global. The scope of the variable is within just the loop() function. My university lecturer drummed into us the rule that the scope of variables should be kept as small as possible. So that's what is being done here with "static".

    if (running) {
        int x = analogRead(A4);
        int y = 1023 - analogRead(A5);
//        Tablet.move(x - 1, y);
        Tablet.move(x, y);
    }

If we are running then read the values from the potentiometers and send the coordinates. The Y coordinate is being reversed, since 0 is at the top of the screen not the bottom, but a potentiometer gives 0 when turned fully left. It just makes sense that the "bottom" of a potentiometer should equate to the bottom of the screen.

The line that is commented out creates the "twitch" I mentioned earlier. Just a nudge to the left. It makes the pointer vibrate (more so than just the potentiometers do) and can be quite annoying, so it's up to you if you include it or not.

Now on to three blocks for handling the buttons. Two the same for the mouse buttons, and one slightly different to toggle the running variable. The basic anatomy of them is the same though. Let's look at one of the mouse button ones:

    bool leftClickValue = digitalRead(A0);
    if (leftClickValue != leftClickState) {
        leftClickState = leftClickValue;
        if (leftClickValue == LOW) {
            Tablet.press(MOUSE_LEFT);
        } else {
            Tablet.release(MOUSE_LEFT);
        }
    }

We grab the current value from the button. Then we compare that to what we had saved in the "static" variable earlier. If they differ then logically the button must either have been pressed or released since the last time through loop(). So we remember the new state by saving the "value" into the "state" variable.

Now we can look to see what actually happened. We know it was either pressed or released, so examining the current value should tell us what was done. If it's HIGH Then it must have been released. If it's LOW then it must have been pressed. In the case of a mouse button we want to press the mouse button when the QuickIO button is pressed, and release the mouse button when the QuickIO button is released. In the case of the running variable we just want to toggle the running state on and off every time the button is pressed - we don't care about when it's released.

And that's really all there is to it.


Hacking the SWA1 Smart WiFi Power Switch