Easy On The Hand…

So a couple months ago, I beat the heck out of my right hand two days in a row. First, spending several hours driving an Formula 1 car in a racing sim, where you’re up shifting with the paddle shifter almost as fast as you can hit it coming out of every turn, and followed it up with hours on end working on a CAD drawing using a mouse the next day.

Day after that, I woke up and I could hardly do anything with my right hand. Couldn’t even pick up a cup of coffee. Now, this is not something with which I am unfamiliar. I’d done this to myself before back in the late ’90s after too many 12+ hour days on the computers at work without taking any breaks. RSI, repetitive strain injury, for which there is little help but to let it heal. Slowly.

Since I really don’t like using a mouse left-handed (which I did for several months last time) I decided to improve the ergonomics of my workstation a bit. First improvement was a vertical mouse, which lets you keep your wrist in a natural position, rather than twisting it flat, though it still hurt to press the buttons, and the scroll wheel, which gets used more than anything else, was even worse.

These vertical misc have gotten quite affordable.

So, I decided to use both hands. Moving the mouse didn’t strain anything that was injured, only manipulating the controls, so, enter the left hand scroll wheel / button box.

Phase One, rotary encoder and a coupe of buttons on the breadboard.

An Arduino Pro Micro, a couple of switches, and a KY-040 rotary encoder. The proof of concept version was functional, but only the push button in the KY-040 was easy to use.

Phase Two, not pictured, added a couple of cheap buttons to the plastic case with the rotary encoder. This kind of worked, but it wasn’t great having to grab and twist the encoder with two fingers.

Phase Three, a nice big plastic knob for the rotary encoder so you can spin it with one finger.

Phase Three added a nice large knob for the rotary encoder allowing it to be spun with one finger, but of course, I’d positioned the buttons too close and it blocks access to them. Not such a big deal, as this isn’t anything but a prototype, and there are nicer buttons on order. I’ve been holding off on the layout for the final version until I have the hardware.

FYI, that Hall Effect switch on the right front corner has nothing to do with the wheel / button box. Just added it as a button to play with it.

The software has gone through a multiple iterations as I’ve added things. It’s quite the hack, but it works.

Originally, it just used the encoder as a mouse wheel, but after spending several hours on a video editing project, I decided it would be nice to be able to switch it to transmit the left-arrow right-arrow keyboard keys the editing software uses to move on the timeline, so modes were added.

While holding down the left and right buttons, press the encoder, and the green LED starts flashing to display the current mode. Turning the the encoder either direction scrolls through the modes (currently only two), and pressing the encoder one more time exits mode selection and returns to normal operation.

I’ve included the code below, warts and all. It’s not pretty, but it works. Please excuse the mess – I have no need to clean it up for my own use. You’ll see there is provision for additional buttons, but the standard mouse library doesn’t seem to support more than five buttons. Still a work in progress.

/*
	Modified to support multiple modes of operation
		- Buttons + Mouse wheel
		- Buttons + Left & Right arrow keys (useful for video editing)
*/

#include <Keyboard.h>
#include <Mouse.h>

// #define	DEBUGGING
// #define DEBUG_MODES
//#define DEBUG_ENCODER
//#define DEBUG_BUTTONS
// #define DEBUG_BLINK

#define ENCODER_CLOCK		2		// Rotary encoder clock
#define	HE_SWITCH			3		// hall effect switch
#define ENCODER_DATA		A0		// Rotary encoder data 
#define ENCODER_SWITCH 		A1		// rotary encoder switch
#define LEFT_SWITCH			5		// left mouse button
#define	RIGHT_SWITCH		6		// right mouse button
#define SIX_SWITCH			7		// pin for mouse button 5
#define	SEVEN_SWITCH		8		// pin for mouse button 6

#define EXTERN_LED			16		// external LED

#define	MOUSE_SIX			(1<<5)	// six button
#define	MOUSE_SEVEN			(1<<6)	// seven button

#define	LED 				13		// power led pin

// #define DEBOUNCE_TIME		100		// delay 100 milliseconds for switch transitions
#define DEBOUNCE_TIME		20		// delay 20 milliseconds for switch transitions

#define LED_PULSETIME		250
#define DEADTIME			1000	// delay between LED pulse streams

/*
	Setup mode
		Toggle setup mode by holding down buttons 1 & 2, then pressing button 3
*/
// what mode are we in? allows functionality changes
bool setupMode = false;		// changing modes
// available modes
enum hidMode {
	mouse,			// encoder sends mouse wheel events
	arrows			// encoder sends left and right cursor key events (useful for video editing)
} mode = mouse;

void nextMode() {
	switch (mode) {
		case mouse:
			mode = arrows;
			#if defined(DEBUGGING) && defined(DEBUG_MODES)
			Serial.println("mode == ARROWS");
			#endif
			break;
		case arrows:
		default:		// something is broken, switch back to mouse
			mode = mouse;
			#if defined(DEBUGGING) && defined(DEBUG_MODES)
			Serial.println("mode == MOUSE");
			#endif
			break;
	}
}

void displayMode() {	// blink LED to display mode we're in
	switch (mode) {
		case mouse:
			blink(1, LED_PULSETIME);
			break;
		case arrows:
			blink(2, LED_PULSETIME);
			break;
		default:		// error!!!
			blink(10, LED_PULSETIME);
	}
}

class Button {
	private:
		int pin;
		String buttonName;
		int state;
		int clickVal;

	// constructor
	public:
		Button(int pinNum, String name, int click ) {
			pin = pinNum;
			state = HIGH;
			buttonName = name;
			clickVal = click;
			pinMode(pin, INPUT_PULLUP);
		}

		int State() {
			return state;
		}

		bool Update() {
			int buttonState = digitalRead(pin);
			if (buttonState != state) {			// state change?
				delay(DEBOUNCE_TIME);
				buttonState = digitalRead(pin);
				if (buttonState != state) {		// if really changed
					state = buttonState;
					#if defined(DEBUGGING) && defined(DEBUG_BUTTONS)
					Serial.print(buttonName);
					#endif
					if (!setupMode) {
						if (state == LOW) {
							#if defined(DEBUGGING) && defined(DEBUG_BUTTONS)
							Serial.println(" down");
							#endif
							Mouse.press(clickVal);
							// lowAction(clickVal);
						} else {
							#if defined(DEBUGGING) && defined(DEBUG_BUTTONS)
							Serial.println(" up");
							#endif
							Mouse.release(clickVal);
							// highAction(clickval);
						}
					}
				}
			}
		}
};

// define mouse buttons - this will need to be enhanced to support multiple modes
Button midButton(ENCODER_SWITCH, "middle button", MOUSE_MIDDLE);
Button leftButton(LEFT_SWITCH, "left button", MOUSE_LEFT);
Button rightButton(RIGHT_SWITCH, "right button", MOUSE_RIGHT);
Button sixButton(SIX_SWITCH, "six button", MOUSE_SIX);
Button sevenButton(SEVEN_SWITCH, "seven button", MOUSE_SEVEN);
Button hallEffectButton(HE_SWITCH, "hall effect", MOUSE_RIGHT);

//------------------------------------- encoder -------------------------------
int encoderCount = 0;
bool stateChanged = false;
bool clockWise = true;		// which way do we spin the mouse wheel

void encoderInterrupt() {
	stateChanged = true;
	if (digitalRead(ENCODER_DATA) == HIGH) {
		if (clockWise)
			encoderCount++;
		else
			encoderCount--;
	} else {
		if (clockWise)
			encoderCount--;
		else
			encoderCount++;
	}
}

void checkEncoder() {
	if (stateChanged) {
		#if defined(DEBUGGING) && defined(DEBUG_ENCODER)
		Serial.print("encoder count = "); Serial.println(encoderCount);
		#endif
		if (setupMode) {
			// switch to next mode
			nextMode();
		} else {
			switch (mode) {
				case mouse:
					Mouse.move(0, 0, encoderCount);
					break;
				case arrows:
					if (encoderCount > 0) {
						Keyboard.write(KEY_LEFT_ARROW);
						#if defined(DEBUGGING) && defined(DEBUG_ENCODER)
						Serial.println("LEFT ARROW");
						#endif
					} else {
						Keyboard.write(KEY_RIGHT_ARROW);
						#if defined(DEBUGGING) && defined(DEBUG_ENCODER)
						Serial.println("RIGHT ARROW");
						#endif
					}
					break;
			}
		}
		encoderCount = 0;
		stateChanged = false;
	}
}

//------------------------------------- support functions ---------------------
void updateButtons() {
	leftButton.Update();
	rightButton.Update();
	midButton.Update();
	sixButton.Update();
	sevenButton.Update();
	hallEffectButton.Update();
}

void toggleSetup() {
	if (!setupMode) {
		if (leftButton.State() == LOW && rightButton.State() == LOW && midButton.State() == LOW) {
			while (midButton.State() == LOW)	// wait for middle button to be released or we'll switch states again
				midButton.Update();
			setupMode = true;
			Mouse.release(MOUSE_LEFT);		// release the buttons so computer isn't locked up
			Mouse.release(MOUSE_RIGHT);
			Mouse.release(MOUSE_MIDDLE);
		}
	} else {
		if (midButton.State() == LOW) {
			setupMode = false;
		}
	}
}

//------------------------------------- start ---------------------------------
void setup() {
	// use falling edge because capacitor on input will cause a slow rising edge
	attachInterrupt(digitalPinToInterrupt(ENCODER_CLOCK), encoderInterrupt, FALLING);
	Mouse.begin();
	Keyboard.begin();
	Serial.begin(9600);

	// enable use of built-in LED
	pinMode(LED_BUILTIN, OUTPUT);

	// configure pin for external LED
	pinMode(EXTERN_LED, OUTPUT);
	// and turn it on (testing)
	externLed(LOW);
}

//-----------------------------------------------------------------------------
// blink LED without stopping processing
bool running = false;
unsigned long endTime;
int blinkCount = 0;
int blinkDuration = 0;
bool ledOn = false;

void blink(int count, int blinkTime) {
	// Serial.print("bink: count = "); Serial.print(count); Serial.print(" blinkTime = "); Serial.println(blinkTime);
	if (!running) {							// start new cycle, if already running, too bad, can't stack them up
		running = true;
		blinkCount = count + 1;					// remember pulses and their length
		blinkDuration = blinkTime;
		endTime = millis() + blinkDuration;	// set the timer
		// digitalWrite(LED_BUILTIN, HIGH);	// and turn on the LED
		externLed(HIGH);
		ledOn = true;
	}
}

void doBlink() {
	if (running) {
		if (endTime < millis()) {				// has delay elapsed?
			#if defined(DEBUGGING) && defined(DEBUG_BLINK)
			Serial.print("ledOn = "); Serial.println(ledOn);
			#endif
			if (ledOn) {						// toggle LED
				#if defined(DEBUGGING) && defined(DEBUG_BLINK)
				Serial.print("blinkCount = "); Serial.print(blinkCount); Serial.println(" turning off LED");
				#endif
				// digitalWrite(LED_BUILTIN, LOW);	// turn off
				externLed(HIGH);
				ledOn = false;
				blinkCount--;					// decrement count every time cycle completes
				if (blinkCount > 0) {			// if more to do
					endTime = millis() + blinkDuration;		// restart timer
				} else if (blinkCount == 0) {				// finished, delay a bit before allowing turn on again
					endTime = millis() + DEADTIME;	// long delay after last one cycle
					ledOn = true;				// say it's on so we hit the off code again
				} else {						// we're done
					running = false;
				}
			} else {
				#if defined(DEBUGGING) && defined(DEBUG_BLINK)
				Serial.print("blinkCount = "); Serial.print(blinkCount); Serial.println(" turning on LED");
				#endif
				// digitalWrite(LED_BUILTIN, HIGH);
				externLed(LOW);
				ledOn = true;
				endTime = millis() + blinkDuration;		// restart timer
			}
		}
	}
}

// external LED
void externLed(int state) {
	digitalWrite(EXTERN_LED, state);
}
//------------------------------------- main loop -----------------------------
void loop() {
	if (setupMode) {
		// display current mode (blink onboard LED)
		displayMode();
	}
	checkEncoder();
	updateButtons();
	toggleSetup();		// go to setup mode?
	doBlink();			// keep the LED running
}