for me to actually get around to building the foot stool I promised my wife when she started using a nasty old fold-up plastic stool to rest her feet under her desk.
Stole the design.
First time I’ve ever tried dovetails or doing much of anything with a chisel. If you don’t look close, the joints don’t look too bad.
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.
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.
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 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.
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
}