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, we’ve gotten to the first week of 2019, and I can finally lay to rest the American Home Shield service request that started last June.
Seems that any form of contact with AHS other than social media is useless. After getting two more bills telling me I owed them for an extra service call they never made, and getting no response from the person that had finally gotten the HVAC fixed, I went after them on Twitter and Facebook, and got responses on both within a couple of hours.
I was told the exact same thing, pretty much word for word (must have been reading the same script), about it being fixed and I’d see my account corrected online in a couple of days… but this time they actually did it!
So, fingers crossed, we’re actually done. System is working. And if I go back and look at the amount of time I’ve invested getting to this point, I would have come out way ahead had I worked those hours and paid for it myself.
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
}
So, after the hour on the phone with customer support, where they claimed to have fixed the problem with their billing, weeks later I get another email telling me my account is past due for the bogus service fee. Later the same day, I get another invoice in the mail telling me my account is past due.
How many weeks does it take for them to correct their billing error? Will it ever happen? Not holding my breath.
Rather than waste hours on the phone with customer service again,, I’ve reached out to the one person that seemed to be able to do things correctly at AHS. I’m hoping this will work.
The 4th of Dec. when I get the email telling me my account is about to be terminated for late payment on a service call. Hmm, only service call I had was when this AC nonsense started back in June.
Thirty five minutes listening to the same twenty second long clip of classical music, so horribly over modulated and distorted it was painful to hear (I may never listen to that particular piece for the rest of my life).
Finally get a human, back on hold while the “look up my account”. They must be using mid-eighties vintage IBM PC’s for it to take this long. Ok, ten or fifteen minutes later I’m told that when somebody reopened the incomplete service call they’d repeatedly closed, even though the work hadn’t been done, they put it in as a new service call, triggering the service charge.
About which I was never informed. So, they can add charges to your account, but the system never sends any notification to the customer so they can pay it? Never got a warning, no payment reminder. Not a word, until today when their system decided it was ready to terminate my account because I didn’t pay that service fee. Who wrote that system?
It only took another ten minutes listening to very annoying, but thankfully low volume, undistorted, generic, instrumental background music (thank you for not ruing any more classics for me) until the nice lady filled out the form telling requesting the charge be removed from my account.
I’m withholding final judgement until I see if it actually gets fixed.
How does one stay in business when providing such wonderful service?
So, the nice person at American Home Shield called back today, as promised. Turns out the parts Total Air Care told me were on order twelve days ago, on 9/7, were not ordered until today. I didn’t ask if they were ordered before or after AHS spoke to Total Air Care (I’m betting on after). Supposedly, it’ll take two to three days for the parts to arrive and then Total Air Care will call to schedule installation.
The person helping me at AHS is going contact me again next week to verify Total Air Care has actually scheduled installation.
Waited another ten days, then replied to the email claiming we had no coverage and got a prompt response:
YES SIR, THIS PART ORDERED, THOUGH, DUE TO THE WEATHER ,THE PART WILL MORE THEN LIKELY BE IN STOCK IN 2-3 DAYS IN WHICH TIME WE WILL CALL YOU AND GET YOU ON THE SCHEDULE AT OUR VERY EARLIEST AVAILABLE APPOINTMENT