Introduction: Chording Keyboard BLE and USB
Skip to step two if you already know who I am and what a chording keyboard is. They are more interesting than me.
First things first, this is a computer keyboard and it types every letter you can find on a standard keyboard.
A chording keyboard is a device which relies on pressing multiple keys at once, similar to playing a chord on a guitar. Since combinations are possible chording keyboards have fewer keys than a standard keyboard. Some people even argue that because you don't have to move your fingers for each keystroke you can type faster. My argument is that fewer keys make this highly portable. The ergonomics of this device have been abandoned in favor of a highly portable keyboard. This device, the size of a computer mouse, is a full keyboard AND mouse. Not only that but it's Bluetooth 4.0 so it pairs up nicely with a mobile phone. BLE sound expensive? The Bluetooth module can be left out and it will work on USB with no changes to the code. That way you can try the keyboard with your regular computer to see if the Bluetooth is valuable to you. Heck, just use the programming and build your own keys, I'd love to see your desktop version of this keyboard.
Much of the credit for this project go to Greg Priest-Dorman who did it all first. His lessons on typing with the chording keyboard are better than I could write so I recommend them. I wrote the program for this project in Arduino's IDE but the charts I used for the key combinations were made by Greg. He developed it, I copied it, make yourself a copy from our work.
This project involves pretty ordinary materials but a few things, like liquid tape and a 3D printer might not be in your garage. Schematics are included so if you don't have a 3D printer it is possible to build your own, in fact, Greg's site has a few ideas on how to make a comfortable keyboard.
This project started very differently than it ended. The original design was going to be solely wrist-mounted, complex, lots of moving parts, servo motors, and a cabling system. That was dumb so I didn't do it. Despite my best efforts at being dumb. Check out my blog if you want to see the day-by-day trials and mishaps of this project. It took me three months to arrive at this design. And a lot of spent printer filament.
This keyboard is part of a cyberpunk costume I'm assembling. I wanted to have a futuristic/cinematic feel to the costume parts but I also want to have functional props. I could just hold some buttons glued to block and say it's a keyboard but I'd rather be able to type text messages with it.
Stay tuned for more costume parts including a lock pick holder that straps to your forearm for easy access. You'll have to source your own lock picks though.
Step 1: Materials and Parts
I've created a neat little package with all the 3D printer files needed for this project as well as a part list with links to anything you can't find at a hardware store. In addition to the printer files I've included the source files for anyone who wants to change something with OpenSCAD.
Alternatively the files can be downloaded right from this step.
The parts is in the package with links but here is the list to give you an idea if you want to start the project.
- 1@ Arduino Micro
- 1@ Adafruit EZ-Key Bluefruit
- 1@ ADXL335 breakout board
- 1@ Protoboard or strip board
- 7@ 12mm tactile switches
- 1@ 23A battery holder and battery
- 1@ USB micro breakout board
- 1@ USB micro male end
- 4@ #6 black screws
Step 2: The Enclosure
If you're printing the included files, an enclosure and a lid, there's nothing more to say. Go to the next step.
If you want to make your own enclosure I'd like to share some thoughts before you start. Each of your fingers has a single switch and the thumb has three switches. Arranging seven switches isn't difficult but making it ergonomic is tricky. If, like me, you want portability over comfort just arrange the switches so that you can operate them without it falling out of your hand. Maybe put them across from one another so you pinch it as you type. Stick your switches into an eraser for prototyping, that's what I did.
Don't confine yourself to square corners, it would be feasible to put all the electronics inside a tennis ball or a blue racket ball and have a spherical keyboard. In the future I hope to build one of these. This is a good time to think outside the box. See what I did there?
If you're not building a portable keyboard there are even more options. The switches I scoped out aren't important so pick ones you like. Cherry makes awesome switches but beware of breaking the leads on the back of them. Mechanical limit switches can be turned into keyboard keys. Heck, you could use infrared proximity switches and have a keyboard with no moving parts.
Hopefully some of these ideas get wheels turning, experiment and don't be afraid to take your electronics out of a failed keyboard and put them into a design you like better. My desk is filled with failed attempts in pursuit of a design I liked.
Step 3: Circuitry
This is the hardest step. Even if you are competent at soldering this will be the most difficult step. Unless you're hand forging an enclosure from iron ore this step will be where most of the work is done. Despite being the most difficult this is straight forward. The schematics included in the package detail which pins to attach to the switches. The rest of the components are optional. If they are not used there is no need for reprogramming. If you don't attach the Bluetooth adapter you simply won't have Bluetooth capability. If you don't attach the accelerometer you simply won't have mouse movement. If you don't attach the battery holder you will need to run off USB power. If you don't attach an external USB port you will have to rely on battery and Bluetooth. It won't hurt my feelings if you don't build this with all the features.
The most important thing to remember is that the switches attach to the outside of the enclosure so I recommend wiring each switch with two wires then feeding those wires through the enclosure holes before soldering them to the Arduino Micro. Keep your wires tidy since the keyboard enclosure was meant to be snug. Excessive wire length or sloppy wiring will make this more difficult than necessary.
Step 4: Programming
Arduino Micros are super easy to program. If you're familiar with Arduino IDE then you already know what to do. If not, please explore Instructables for programming an Arduino Micro. Fortunately, all you need to do is upload the program I've written, no changes are necessary. Although, if you want to make changes you certainly may.
If you are familiar with Codebender.cc then you can upload the code right from here! Codebender is a site which allows people like us to host Arduino code for free and share it with others. It is even possible to send your code to and Arduino right from a browser. I highly recommend them and support them with my donations.
The program for the Arduino can be downloaded from this step, taken from the bundled package, found directly on Codebender or copied right out of the text in this step. Formatting gets removed when pasted into Instructables but it should run just fine.
/*
Programmed by Brian McEvoy. 24 Hour Engineer 24hourengineer.com Program is distributable for personal use. *
/ Inputs. Buttons may be addressed by name but the program expects all buttons after the pinky // to be numbered sequentially. int pinkyButton = 4; int ringButton = 5; int middleButton = 6; int indexButton = 7; int nearTButton = 8; int centerTButton = 9; int farTButton = 10;
// Program integers int prefixChord = 0; // 1 = shift (F). 2 = numlock (N). 3 = special (CN). 4 = function keys int chordValue = 0; int randomNumber01; int randomNumber02; int randomNumber03; int randomNumber04; int debounceDelay = 20; int mouseDelay = 40; int mouseValues[] = {0,0,0}; int startingMouseValues[] = {0,0,0}; int mouseDivisionValues[] = {-15,1,-1};
int sign1 = 0; int sign2 = 0;
// Booleans boolean buttons[7]; // Pinky is [0] and far thumb is [6] boolean latchingButtons[7]; boolean acquiringPresses = LOW; boolean calculateKey = LOW; boolean stickyCapsLock = LOW; boolean stickyNumlock = LOW; boolean stickySpecialLock = LOW;
void setup(){ Serial1.begin(9600); Serial.begin(9600); Serial.println("Up and runnning"); Keyboard.begin(); Mouse.begin(); randomSeed(analogRead(0)); pinMode(pinkyButton, INPUT_PULLUP); pinMode(ringButton, INPUT_PULLUP); pinMode(middleButton, INPUT_PULLUP); pinMode(indexButton, INPUT_PULLUP); pinMode(nearTButton, INPUT_PULLUP); pinMode(centerTButton, INPUT_PULLUP); pinMode(farTButton, INPUT_PULLUP); }
void loop(){ acquiringPresses = checkButtonArray(); if (acquiringPresses && onlyFarThumbPressed(farTButton)){ doMouseSTUFF(); } if (acquiringPresses){ delay(debounceDelay); // Instead of a true software debounce this will wait a moment until the first button press has settled. typingChord(); // Wait and see which keys are touched. When they are all released print the correct letter. updateShiftKeys(); // Change the prefixChord value if any of the 'locks' are set. Example, Num Lock or Caps Lock. sendKeyPress(); // Using the buttons pressed during the typingChord function determine how to handle the chord. delay(debounceDelay); // The other half of the software "debounce" for (int i = 0; i < 7; i++){ // Once a keypress has been sent the booleans should be reset. latchingButtons[i] = LOW; } chordValue = 0; } }
void doMouseSTUFF(){ for (int i = 0; i < 3; i++){ startingMouseValues[i] = analogRead(i); } delay(debounceDelay); while (onlyFarThumbPressed(farTButton)){ delay(mouseDelay); for (int i = 0; i < 3; i++){ int reading = analogRead(i); mouseValues[i] = reading - startingMouseValues[i]; } for (int i = 0; i < 3; i++){ mouseValues[i] = mouseValues[i] / mouseDivisionValues[i]; } Mouse.move(mouseValues[1], mouseValues[2], mouseValues[0]); Serial1.write(0xFD); Serial1.write((byte)0x00); Serial1.write((byte)0x03); Serial1.write((byte)0x00); // Buttons Serial1.write((byte)mouseValues[1]); // X axis Serial1.write((byte)-mouseValues[2]); // Y axis Serial1.write((byte)0x00); Serial1.write((byte)0x00); Serial1.write((byte)0x00); } }
boolean onlyFarThumbPressed(int functionMaxButton){ for (int i = functionMaxButton - 1; i > (functionMaxButton - 7); i--){ if(!digitalRead(i)){ return LOW; } } if (!digitalRead(functionMaxButton)){ return HIGH; }else{ return LOW; } }
void updateShiftKeys(){ if (stickyCapsLock){ prefixChord = 1; } if (stickyNumlock){ prefixChord = 2; } if (stickySpecialLock){ prefixChord = 3; } }
boolean checkButtonArray(){ // Update the buttons[] array with each scan. Set the acquiringPresses bit HIGH if any switch is pressed. for (int i = 0; i < 7; i++){ boolean buttonState = !digitalRead(pinkyButton + i); if (buttonState){ buttons[i] = HIGH; }else{ buttons[i] = LOW; } } for (int i = 0; i < 7; i++){ if (buttons[i]){ return HIGH; } } return LOW; }
void typingChord(){ while (acquiringPresses){ for (int i = 0; i < 7; i++){ if (buttons[i] == HIGH){ latchingButtons[i] = HIGH; } } acquiringPresses = checkButtonArray(); } }
void sendKeyPress(){ for (int i = 0; i < 7; i++){ if (latchingButtons[i] == HIGH){ chordValue = chordValue + customPower(2, i); } } Serial1.write(keySwitch(chordValue)); }
int customPower(int functionBase, int functionExponent){ int functionResult = 1; for (int i = 0; i < functionExponent; i++){ functionResult = functionResult * functionBase; } return functionResult; }
int keySwitch(int functionChordValue){ switch (functionChordValue){ case 0: prefixChord = 0; Keyboard.releaseAll(); stickyNumlock = LOW; stickyCapsLock = LOW; stickySpecialLock = LOW; return 0; // error case 1: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('w'); return 119; // 119 is equivalent to the letter 'w' case 1: prefixChord = 0; prefixChord = 0; Keyboard.print('W'); return 87; case 2: prefixChord = 0; prefixChord = 0; Keyboard.print('5'); return 53; case 3: prefixChord = 0; prefixChord = 0; Keyboard.print('%'); return 37; case 4: prefixChord = 0; Keyboard.write(198); return 198; } case 2: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('y'); return 121; // 121 is equivalent to the letter 'y' case 1: prefixChord = 0; Keyboard.print('Y'); return 89; case 2: prefixChord = 0; Keyboard.print('4'); return 52; case 3: prefixChord = 0; Keyboard.print('$'); return 36; case 4: prefixChord = 0; Keyboard.write(197); return 197; } case 3: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('u'); return 117; // 117 is equivalent to the letter 'u' case 1: prefixChord = 0; Keyboard.print('U'); return 85; case 2: prefixChord = 0; Keyboard.print('"'); Keyboard.print('"'); Keyboard.press(KEY_LEFT_ARROW); Keyboard.releaseAll(); Serial1.print('"'); Serial1.print('"'); return 0x0B; case 3: prefixChord = 0; Keyboard.print('"'); Keyboard.print('"'); Keyboard.press(KEY_LEFT_ARROW); Keyboard.releaseAll(); Serial1.print('"'); Serial1.print('"'); return 0x0B; case 4: prefixChord = 0; return 0; } case 4: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('r'); return 114; // 114 → r case 1: prefixChord = 0; Keyboard.print('R'); return 82; case 2: prefixChord = 0; Keyboard.print('3'); return 45; case 3: prefixChord = 0; Keyboard.print('#'); return 35; case 4: prefixChord = 0; Keyboard.write(196); return 196; } case 5: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print("24Eng"); Serial1.print("24Eng"); return 0; case 1: prefixChord = 0; Keyboard.print("24hourengineer.com"); Serial1.print("24hourengineer.com"); return 0; case 2: prefixChord = 0; Keyboard.print("Brian '24HourEngineer' McEvoy"); Serial1.print("Brian '24HourEngineer' McEvoy"); return 0; case 3: prefixChord = 0; Keyboard.print("Brian McEvoy"); Serial1.print("Brian McEvoy"); return 0; case 4: prefixChord = 0; Keyboard.print("Easter Egg"); Serial1.print("Easter Egg"); return 0; } case 6: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('h'); return 104; // 104 → h case 1: prefixChord = 0; Keyboard.print('H'); return 72; case 2: prefixChord = 0; Keyboard.print('0'); Keyboard.print('0'); Serial.print('0'); return 48; case 3: prefixChord = 0; Keyboard.print('0'); Keyboard.print('0'); Serial.print('0'); return 48; case 4: prefixChord = 0; return 0; } case 7: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('s'); return 115; // 115 → s case 1: prefixChord = 0; Keyboard.print('S'); return 83; case 2: prefixChord = 0; Keyboard.print('-'); return 45; case 3: prefixChord = 0; Keyboard.print('_'); return 95; case 4: prefixChord = 0; return 0; } case 8: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('i'); return 105; // 105 → i case 1: prefixChord = 0; Keyboard.print('I'); return 73; case 2: prefixChord = 0; Keyboard.print('2'); return 50; case 3: prefixChord = 0; Keyboard.print('@'); return 64; case 4: prefixChord = 0; Keyboard.write(195); return 195; } case 9: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('b'); return 98; // 98 → b case 1: prefixChord = 0; Keyboard.print('B'); return 66; case 2: prefixChord = 0; Keyboard.write(92); return 92; case 3: prefixChord = 0; Keyboard.print('|'); return 124; case 4: prefixChord = 0; Keyboard.write(205); return 205; } case 10: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('k'); return 107; // 107 → k case 1: prefixChord = 0; Keyboard.print('K'); return 75; case 2: prefixChord = 0; Keyboard.print('$'); return 36; case 3: prefixChord = 0; Keyboard.print('$'); return 36; case 4: prefixChord = 0; return 0; } case 11: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('z'); return 122; // 122 → z case 1: prefixChord = 0; Keyboard.print('Z'); return 90; case 2: prefixChord = 0; Keyboard.print('`'); return 96; case 3: prefixChord = 0; Keyboard.print('~'); return 126; case 4: prefixChord = 0; return 0; } case 12: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('d'); return 100; // 100 → d case 1: prefixChord = 0; Keyboard.print('D'); return 68; case 2: prefixChord = 0; Keyboard.print('/'); return 47; case 3: prefixChord = 0; Keyboard.print('?'); return 63; case 4: prefixChord = 0; return 0; } case 13: // This chord is open randomNumber01 = random(0, 255); randomNumber02 = random(0, 1000); randomNumber03 = random(0, 4000); switch (prefixChord){ case 0: Mouse.click(MOUSE_LEFT); delay(2); prefixChord = 0; sign1 = random(0,4); sign2 = random(0,4); Keyboard.print(randomNumber01); Serial1.print(randomNumber01); if (sign1 == 0){ Keyboard.print('/'); Serial1.print('/'); } if (sign1 == 1){ Keyboard.print('*'); Serial1.print('*'); } if (sign1 == 2){ Keyboard.print('-'); Serial1.print('-'); } if (sign1 == 3){ Keyboard.print('+'); Serial1.print('+'); } Keyboard.print(randomNumber02); Serial1.print(randomNumber02); if (sign2 == 0){ Keyboard.print('/'); Serial1.print('/'); } if (sign2 == 1){ Keyboard.print('*'); Serial1.print('*'); } if (sign2 == 2){ Keyboard.print('-'); Serial1.print('-'); } if (sign2 == 3){ Keyboard.print('+'); Serial1.print('+'); } Keyboard.print(randomNumber03); Serial1.print(randomNumber03); delay(5); Keyboard.print('\n'); return 10; case 1: prefixChord = 0; Keyboard.print("0."); Serial1.print("0."); randomNumber04 = random(0, 10); Keyboard.print(randomNumber04); Serial1.print(randomNumber04); randomNumber04 = random(0, 10); Keyboard.print(randomNumber04); Serial1.print(randomNumber04); randomNumber04 = random(0, 10); Keyboard.print(randomNumber04); Serial1.print(randomNumber04); randomNumber04 = random(0, 10); Keyboard.print(randomNumber04); Serial1.print(randomNumber04); return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; Keyboard.print(randomNumber01); Serial1.print(randomNumber01); return 0; } case 14: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('e'); return 101; // 101 → e case 1: prefixChord = 0; Keyboard.print('E'); return 69; case 2: prefixChord = 0; Keyboard.print('='); return 61; case 3: prefixChord = 0; Keyboard.print('+'); return 43; case 4: prefixChord = 0; return 0; } case 15: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('t'); return 116; // 116 → t case 1: prefixChord = 0; Keyboard.print('T'); return 84; case 2: prefixChord = 0; Keyboard.print('0'); Keyboard.print('0'); Keyboard.print('0'); Serial1.print('0'); Serial1.print('0'); return 48; case 3: prefixChord = 0; Keyboard.print('0'); Keyboard.print('0'); Keyboard.print('0'); Serial1.print('0'); Serial1.print('0'); return 48; case 4: prefixChord = 0; return 0; } case 16: stickyCapsLock = LOW; stickyNumlock = LOW; switch (prefixChord){ case 0: prefixChord = 2; return 0; // Set the 'numlock' when only the near thumb key is pressed. case 1: prefixChord = 2; return 0; case 2: stickyNumlock = !stickyNumlock; // When this is pressed a second time toggle it. prefixChord = 0; return 0; case 3: prefixChord = 2; return 0; case 4: prefixChord = 0; return 0; } case 17: stickyCapsLock = LOW; stickyNumlock = LOW; stickySpecialLock = LOW; switch (prefixChord){ case 0: prefixChord = 4; return 0; case 1: prefixChord = 4; return 0; case 2: prefixChord = 4; return 0; case 3: prefixChord = 4; return 0; case 4: prefixChord = 0; return 0; } case 18: stickyCapsLock = LOW; stickyNumlock = LOW; stickySpecialLock = LOW; switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(27); return 0x1B; case 1: prefixChord = 0; Keyboard.write(27); return 0x1B; case 2: prefixChord = 0; Keyboard.write(27); return 0x1B; case 3: prefixChord = 0; Keyboard.write(27); return 0x1B; case 4: prefixChord = 0; Keyboard.write(27); return 0x1B; } case 19: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print(';'); return 59; case 1: prefixChord = 0; Keyboard.print(':'); return 58; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 20: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print(','); return 44; case 1: prefixChord = 0; Keyboard.print('<'); return 60; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 21: switch (prefixChord){ case 0: prefixChord = 0; return 0; case 1: prefixChord = 0; return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 22: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('.'); return 46; case 1: prefixChord = 0; Keyboard.print('>'); return 62; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 23: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; case 1: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 24: switch (prefixChord){ case 0: prefixChord = 0; return 0; case 1: prefixChord = 0; return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 25: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_INSERT); return 0x01; case 1: Keyboard.write(KEY_INSERT); return 0x01; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 26: switch (prefixChord){ case 0: prefixChord = 0; return 0; case 1: prefixChord = 0; return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 27: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_LEFT_CTRL); return 0xE0; case 1: prefixChord = 0; Keyboard.write(KEY_LEFT_CTRL); return 0xE0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 28: switch (prefixChord){ case 0: prefixChord = 0; return 0; case 1: prefixChord = 0; return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 29: switch (prefixChord){ case 0: prefixChord = 0; return 0; case 1: prefixChord = 0; return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 30: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(39); return 39; case 1: prefixChord = 0; Keyboard.write(34); return 34; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 31: switch (prefixChord){ case 0: prefixChord = 0; return 0; case 1: prefixChord = 0; return 0; case 2: prefixChord = 0; return 0; case 3: prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 32: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print(' '); return 32; // 32 → 'space' case 1: prefixChord = 0; Keyboard.print(' '); return 32; case 2: prefixChord = 0; Keyboard.print('1'); return 49; case 3: prefixChord = 0; Keyboard.print('!'); return 33; case 4: prefixChord = 0; Keyboard.write(194); return 194; } case 33: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('f'); return 102; // 102 → f case 1: prefixChord = 0; Keyboard.print('F'); return 70; case 2: prefixChord = 0; Keyboard.print('9'); return 57; case 3: prefixChord = 0; Keyboard.print('('); return 40; case 4: prefixChord = 0; Keyboard.write(202); return 202; } case 34: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('g'); return 103; // 103 → g case 1: prefixChord = 0; Keyboard.print('G'); return 71; case 2: prefixChord = 0; Keyboard.print('8'); return 56; case 3: prefixChord = 0; Keyboard.print('*'); return 42; case 4: prefixChord = 0; Keyboard.write(201); return 201; } case 35: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('v'); return 118; // 118 → v case 1: prefixChord = 0; Keyboard.print('V'); return 86; case 2: prefixChord = 0; Keyboard.print(']'); return 91; case 3: prefixChord = 0; Keyboard.print('}'); return 125; case 4: prefixChord = 0; return 0; } case 36: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('c'); return 99; // 99 → c case 1: prefixChord = 0; Keyboard.print('C'); return 67; case 2: prefixChord = 0; Keyboard.print('7'); return 55; case 3: prefixChord = 0; Keyboard.print('&'); return 38; case 4: prefixChord = 0; Keyboard.write(200); return 200; } case 37: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print(']'); return 93; // 93 → ] case 1: prefixChord = 0; Keyboard.print('}'); return 125; case 2: prefixChord = 0; Keyboard.print(']'); return 93; case 3: prefixChord = 0; Keyboard.print('}'); return 125; case 4: prefixChord = 0; return 0; } case 38: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('p'); return 112; // 112 → p case 1: prefixChord = 0; Keyboard.print('P'); return 80; case 2: prefixChord = 0; Keyboard.print('%'); return 37; case 3: prefixChord = 0; Keyboard.print('%'); return 37; case 4: prefixChord = 0; return 0; } case 39: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('n'); return 110; // 110 → n case 1: prefixChord = 0; Keyboard.print('N'); return 78; case 2: prefixChord = 0; Keyboard.print('['); return 91; case 3: prefixChord = 0; Keyboard.print('{'); return 123; case 4: prefixChord = 0; return 0; } case 40: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('l'); return 108; // 108 → l (lowercase L) case 1: prefixChord = 0; Keyboard.print('L'); return 76; case 2: prefixChord = 0; Keyboard.print('6'); return 54; case 3: prefixChord = 0; Keyboard.print('^'); return 94; case 4: prefixChord = 0; Keyboard.write(199); return 199; } case 41: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('x'); return 120; // 120 → x case 1: prefixChord = 0; Keyboard.print('X'); return 88; case 2: prefixChord = 0; Keyboard.print('&'); return 38; case 3: prefixChord = 0; Keyboard.print('&'); return 38; case 4: prefixChord = 0; return 0; } case 42: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('j'); return 106; // 106 → j case 1: prefixChord = 0; Keyboard.print('J'); return 74; case 2: prefixChord = 0; Keyboard.print('('); Keyboard.print(')'); Keyboard.press(KEY_LEFT_ARROW); Keyboard.releaseAll(); Serial1.print('('); Serial1.print(')'); return 0x0B; case 3: prefixChord = 0; Keyboard.print('('); Keyboard.print(')'); Keyboard.press(KEY_LEFT_ARROW); Keyboard.releaseAll(); Serial1.print('('); Serial1.print(')'); return 0x0B; case 4: prefixChord = 0; return 0; } case 43: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('q'); return 113; // 113 → q case 1: prefixChord = 0; Keyboard.print('Q'); return 81; case 2: prefixChord = 0; Keyboard.print('?'); return 63; case 3: prefixChord = 0; Keyboard.print('?'); return 63; case 4: prefixChord = 0; return 0; } case 44: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('m'); return 109; // 109 → m case 1: prefixChord = 0; Keyboard.print('M'); return 77; case 2: prefixChord = 0; Keyboard.print('*'); return 42; case 3: prefixChord = 0; Keyboard.print('*'); return 42; case 4: prefixChord = 0; return 0; } case 45: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('['); return 91; // 91 → [ case 1: prefixChord = 0; Keyboard.print('{'); return 123; case 2: prefixChord = 0; Keyboard.print('['); return 91; case 3: prefixChord = 0; Keyboard.print('{'); return 123; case 4: prefixChord = 0; return 0; } case 46: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('a'); return 97; // 97 → a case 1: prefixChord = 0; Keyboard.print('A'); return 65; case 2: prefixChord = 0; Keyboard.print('+'); return 43; case 3: prefixChord = 0; Keyboard.print('+'); return 43; case 4: prefixChord = 0; Keyboard.write(204); return 204; } case 47: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('o'); return 111; // 111 → o case 1: prefixChord = 0; Keyboard.print('O'); return 79; case 2: prefixChord = 0; Keyboard.print('0'); return 48; case 3: prefixChord = 0; Keyboard.print(')'); return 29; case 4: prefixChord = 0; Keyboard.write(203); return 203; } case 48: stickyCapsLock = LOW; stickyNumlock = LOW; switch (prefixChord){ case 0: prefixChord = 3; return 0; // Set the sticky 'special characters' when only the near thumb key is pressed. case 1: prefixChord = 3; return 0; case 2: prefixChord = 3; return 0; case 3: stickySpecialLock = !stickySpecialLock; prefixChord = 0; return 0; case 4: prefixChord = 0; return 0; } case 64: stickyNumlock = LOW; stickySpecialLock = LOW; switch (prefixChord){ case 0: prefixChord = 1; return 0; // Set the sticky 'shift' when only the near thumb key is pressed. case 1: stickyCapsLock = !stickyCapsLock; prefixChord = 0; return 0; case 2: prefixChord = 1; return 0; case 3: prefixChord = 1; return 0; case 4: prefixChord = 1; return 0; } case 65: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.print('\n'); return 10; // 10 → enter case 1: prefixChord = 0; Keyboard.print('\n'); return 10; // 10 → enter case 2: prefixChord = 0; Keyboard.print('\n'); return 10; // 10 → enter case 3: prefixChord = 0; Keyboard.print('\n'); return 10; // 10 → enter case 4: prefixChord = 0; Keyboard.print('\n'); return 10; // 10 → enter } case 66: switch (prefixChord){ case 0: prefixChord = 0; Mouse.click(MOUSE_RIGHT); return 0; case 1: prefixChord = 0; Mouse.click(MOUSE_RIGHT); delay(2); Mouse.click(MOUSE_RIGHT); return 0; case 2: prefixChord = 0; Mouse.click(MOUSE_RIGHT); return 0; case 3: prefixChord = 0; Mouse.click(MOUSE_RIGHT); return 0; case 4: prefixChord = 0; Mouse.click(MOUSE_RIGHT); return 0; } case 67: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_RIGHT_ARROW); return 0x07; case 1: prefixChord = 0; Keyboard.write(KEY_RIGHT_ARROW); return 0x07; case 2: prefixChord = 0; Keyboard.write(KEY_RIGHT_ARROW); return 0x07; case 3: prefixChord = 0; Keyboard.write(KEY_RIGHT_ARROW); return 0x07; case 4: prefixChord = 0; Keyboard.write(KEY_RIGHT_ARROW); return 0x07; } case 68: switch (prefixChord){ case 0: prefixChord = 0; Mouse.click(MOUSE_MIDDLE); return 0; case 1: prefixChord = 0; Mouse.click(MOUSE_MIDDLE); delay(2); Mouse.click(MOUSE_MIDDLE); return 0; case 2: prefixChord = 0; Mouse.click(MOUSE_MIDDLE); return 0; case 3: prefixChord = 0; Mouse.click(MOUSE_MIDDLE); return 0; case 4: prefixChord = 0; Mouse.click(MOUSE_MIDDLE); return 0; } case 69: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_UP_ARROW); return 0x0E; case 1: prefixChord = 0; Keyboard.write(KEY_UP_ARROW); return 0x0E; case 2: prefixChord = 0; Keyboard.write(KEY_UP_ARROW); return 0x0E; case 3: prefixChord = 0; Keyboard.write(KEY_UP_ARROW); return 0x0E; case 4: prefixChord = 0; Keyboard.write(KEY_UP_ARROW); return 0x0E; } case 70: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_DELETE); return 0x04; case 1: prefixChord = 0; Keyboard.write(KEY_DELETE); return 0x04; case 2: prefixChord = 0; Keyboard.write(KEY_DELETE); return 0x04; case 3: prefixChord = 0; Keyboard.write(KEY_DELETE); return 0x04; case 4: prefixChord = 0; Keyboard.write(KEY_DELETE); return 0x04; } case 71: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_BACKSPACE); return 0x08; case 1: prefixChord = 0; Keyboard.write(KEY_BACKSPACE); return 0x08; case 2: prefixChord = 0; Keyboard.write(KEY_BACKSPACE); return 0x08; case 3: prefixChord = 0; Keyboard.write(KEY_BACKSPACE); return 0x08; case 4: prefixChord = 0; Keyboard.write(KEY_BACKSPACE); return 0x08; } case 72: switch (prefixChord){ case 0: prefixChord = 0; Mouse.click(MOUSE_LEFT); return 0; case 1: prefixChord = 0; Mouse.click(MOUSE_LEFT); delay(2); Mouse.click(MOUSE_LEFT); return 0; case 2: prefixChord = 0; Mouse.click(MOUSE_LEFT); return 0; case 3: prefixChord = 0; Mouse.click(MOUSE_LEFT); return 0; case 4: prefixChord = 0; Mouse.click(MOUSE_LEFT); return 0; } case 73: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_LEFT_ARROW); return 0x0B; case 1: prefixChord = 0; Keyboard.write(KEY_LEFT_ARROW); return 0x0B; case 2: prefixChord = 0; Keyboard.write(KEY_LEFT_ARROW); return 0x0B; case 3: prefixChord = 0; Keyboard.write(KEY_LEFT_ARROW); return 0x0B; case 4: prefixChord = 0; Keyboard.write(KEY_LEFT_ARROW); return 0x0B; } case 74: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; case 1: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; case 2: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; case 3: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; case 4: prefixChord = 0; Keyboard.write(KEY_LEFT_ALT); return 0xE2; } case 75: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_PAGE_DOWN); return 0x06; case 1: prefixChord = 0; Keyboard.write(KEY_PAGE_DOWN); return 0x06; case 2: prefixChord = 0; Keyboard.write(KEY_PAGE_DOWN); return 0x06; case 3: prefixChord = 0; Keyboard.write(KEY_PAGE_DOWN); return 0x06; case 4: prefixChord = 0; Keyboard.write(KEY_PAGE_DOWN); return 0x06; } case 76: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_PAGE_UP); return 0x03; case 1: prefixChord = 0; Keyboard.write(KEY_PAGE_UP); return 0x03; case 2: prefixChord = 0; Keyboard.write(KEY_PAGE_UP); return 0x03; case 3: prefixChord = 0; Keyboard.write(KEY_PAGE_UP); return 0x03; case 4: prefixChord = 0; Keyboard.write(KEY_PAGE_UP); return 0x03; } case 77: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_DOWN_ARROW); return 0x0C; case 1: prefixChord = 0; Keyboard.write(KEY_DOWN_ARROW); return 0x0C; case 2: prefixChord = 0; Keyboard.write(KEY_DOWN_ARROW); return 0x0C; case 3: prefixChord = 0; Keyboard.write(KEY_DOWN_ARROW); return 0x0C; case 4: prefixChord = 0; Keyboard.write(KEY_DOWN_ARROW); return 0x0C; } case 78: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_END); return 0x05; case 1: prefixChord = 0; Keyboard.write(KEY_END); return 0x05; case 2: prefixChord = 0; Keyboard.write(KEY_END); return 0x05; case 3: prefixChord = 0; Keyboard.write(KEY_END); return 0x05; case 4: prefixChord = 0; Keyboard.write(KEY_END); return 0x05; } case 79: switch (prefixChord){ case 0: prefixChord = 0; Keyboard.write(KEY_HOME); return 0x02; case 1: prefixChord = 0; Keyboard.write(KEY_HOME); return 0x02; case 2: prefixChord = 0; Keyboard.write(KEY_HOME); return 0x02; case 3: prefixChord = 0; Keyboard.write(KEY_HOME); return 0x02; case 4: prefixChord = 0; Keyboard.write(KEY_HOME); return 0x02; } default: prefixChord = 0; Keyboard.releaseAll(); stickyNumlock = LOW; stickyCapsLock = LOW; stickySpecialLock = LOW; return 0; // error } }
Attachments
Step 5: Assembly
The rest is easy. Use some plastic glue, or just super glue, and glue the buttons in place. Grab some #6 screws 1" long and attach the lid. Once your glue dries you should be up and running. The Bluetooth will connect with your phone the way you would connect any Bluetooth keyboard but you can always read the tutorial from Adafruit, the makers of Bluefruit. If you're only using USB then you're already golden.
Step 6:
Thank you for reading all the way to the end. Maybe you have built a keyboard and you are scrolling down by pressing [far thumb], [index], [middle] and [pinkie] or maybe you've got an idea of how you would do this differently. I hope you'll tell me either way.
The design and programming for this device took me about three months. It was a lot of hard work, exhausting at times but ultimately rewarding. At my blog you can read this project from day one and see some mistakes I made while designing. After that check out some other projects, not all of which have been made into Instructables. My cyberpunk costume is coming together and I'll be making more parts for it so come and what I'm building.
15 Comments
2 years ago
I ran in to this doing research on chording keyboards, and I love the concept...
Unfortunately the EZ-KEY HID is no longer available so a significant piece of the code will need to be rewritten to use newer Bluetooth implementations...
7 years ago
I'm also doing a feather-based BLE HID keyboard and wonder what is an appropriate battery size. I've ordered the smallest lipo Adafruit offers (150 mah) but that might not be enough. Anyone have experience along this line?
Reply 7 years ago
I've ordered a Feather but I wasn't sure what size battery to get either. I picked up the smallest battery and the next size up. If you get results before I do please post!
7 years ago
Cool project, I would say for someone starting from scratch the Bluefruit Feather (probably came out after this project build) would make the most sense for this project. It is actually Bluetooth LE, includes the "Arduino" (32u4 chip) on the same board and has charge circuitry for a lipo battery on board too. Reduces the cost and complexity quite a bit. The other change I would make to reduce the complexity is to butt the usb edge of the Feather to an open port on the side of the 3D print to avoid the usb break out. Then it's just a matter of wiring buttons! ( I'm sure there will be a 9 dof wing in the future, so leave the clearance! )
Reply 7 years ago
That would have simplified the hardware. Honestly, I didn't know about that board and I I will likely get one. The built-in battery charger is simply brilliant.
For this project I wouldn't have picked that one because I wanted people to have modularity. If someone wanted to build a wired model they could buy an Arduino Micro and leave out the Bluefruit and accelerometer. In fact, I've done that myself.
http://www.24hourengineer.com/2015/12/2015-12-01-t...
As far as exposing the USB port I couldn't agree more. Time was running out and I stopped redesigning the case therefore the Micro inside never got an exposed USB port. It wouldn't be too much trouble to add one or just print the thing and use a Dremel to cut the hole.
If you build one of these and modify my code for the Feather be sure to post a link for us!
Reply 7 years ago
For the 7 Dollar difference it makes one question how important modularity is, because a wired one is still possible with the BLE feather. Personally I've learned that prototyping wired is the way to go. It's definitely good to have access to both!
I have a chorder on my desk that I have used quite a bit and has made me question the validity of our design choices (layout systems). So I'm not planning another with a char to char layout.
https://hackaday.io/project/1386-neotype-haptic-co...
However I am planning a handheld in the future. Right now I'm learning stenography and I hope to incorporate features like steno order and shorthand techniques into the device in an attempt to create a portable device that can keep up with higher typing speeds. The type of devices we have built seem to max out in the 50wpm range for a fast mover. (I"m not a fast mover and do about half that with Tenkey)
I'm more than happy to help with code though. The current code you have should be almost one to one minus the bluetooth bits to port your code to the BLE feather. Actually looking closer at your code it seems you have hard coded usb and BT signals into the same giant switch case. Key actuation should have been abstracted out into a separate function. No big deal, it just means a lot more editing to change things over. ( I have conversion and abstraction code for the Bluefruit HID kicking around somewhere if you're interested)
Also debouncing with a delay caused loads of issues for me. Take a look at the button.ino tab of 10 key Neotype if it's causing issues for you. All the code would work just as well with 7 keys.
Reply 7 years ago
I figured most people would buy a cheap Arduino Micro on eBay for $6 rather than a genuine Arduino. Even if they add the Ez-Key it's still less than $30. Granted, the size difference is valuable.
What do you mean about abstracting the key actuation to a separate function? The value of the keys isn't passed to the switch function until all keys are released.
My plans for another keyboard are to embed all the electronics inside a racket ball. Switches would be under the flexible skin so it would stay a perfect sphere. Not the perfect typing surface but easy to handle and you can literally toss it around. Plus, they make belt clips for tennis balls so you could slap your keyboard to your belt and keep it as a wearable. For something like that a rechargeable battery and tiny processor would be ideal. An inductive charger wouldn't be a bad idea either.
How big of an overhaul would be necessary to make the code work on the Feather?
Reply 7 years ago
Honestly I have a hard time recommending to buy from anyone other than companies supporting the ecosystem like Sparkfun, Adafruit and Arduino.cc. Maybe it's a bit more expensive and maybe it's too noble, but what goes around comes around.
By 'abstracting actuation' what I mean is having a function like the following.
void sendKeystroke ( byte keystroke ) {
Keyboard.press ( keystroke );
Serial1.write( keystroke );
}
Of course that is simplified. The switch case would figure the keystroke and pass it to sendKeystroke each case. This might seem asinine until you decide you want to use serial0 or some other interface to send keystrokes. Then it's just as easy a commenting out the old interface and substituting the new. It's really not that much of an overhaul as it is a change of organization.
Now if I touch the code I would just replace it with code I spent allot of time on, know works and have debugged. The code in Tenkey was derived from 8 and 5 key keyers that used USB HID and Bluefruit HID. Because I changed my mind so much on the set-up I wrote the code in such a way that the layout and interface could be switched by changing a layout array and number of keys changed by another array that defines the pins. 16 keys are possible, 32 if some data types are changed. https://github.com/PaulBeaudet/tenkey
The ball concept sounds like a neat idea reminds me of one of the original mech typewriters. I think inductive is going to be challenging for that type of set-up though. The coils need to be fairly close and aligned for efficient power transfer.
Reply 7 years ago
I see where you're coming from with hardware. My mentality of the inexpensive hardware was making it as accessible as possible. Plus, screwing up is cheaper and I can keep a handful of spared on-hand rather than reordering. In the future I will give more important jobs to the better hardware. I LOVE shopping the new arrivals at Adafruit.
I haven't created functions like you're showing. I only have a few years of experience with Arduino and a little bit more with structured text coding. My background is in industrial automation. I see the advantage of your method. If I were going to change my code it would be a find-replace ordeal.
I don't think the coil alignment would have to be a difficult deal. The BB-8 rc robots have a sphere and inductive coils. A small magnet in the base of the ball could activate the charging coil, which usually has a light, so that could serve as the alignment tool and cut power when the charger isn't being used. Green and easy!
Reply 7 years ago
With the inductive charging, I'm mainly thinking of the hardware I have. On the pad the coil is glued flat to a ceramic tile. If the coil could be formed around the curvature of the ball I think you would be all set. Think whether this is difficult or not has to do with the diameter of the coil. With BB8 it looks like the coil is only as wide as a shallow part of the ball. With a racket ball you might need a smaller coils.
Reply 7 years ago
http://www.adafruit.com/products/1407
I got this not too long ago from Adafruit. The base coil wouldn't have to be under a flat surface and that's what I was planning. I could also wind my own coils but that would probably be a waste of time. Plus I don't have lacquered wire that large sitting around my apartment.
Reply 7 years ago
I hadn't see that set yet. Looks like it would fit into a racket ball fine. I'm interested to see how this unfolds, it's going to be quite something to pack all that stuff in a little ball.
Reply 7 years ago
On top of that the BLE Feather isn't in stock right now. At this point the switches may take more room than anything else. I plan to 3D print the housing for everything. Or I could cut/drill a wooden sphere to make cavities for the components.
7 years ago
I don't know if you intended this, but it looks like the code you have posted in step 4 is for a servo arm whereas I might have been expecting keyer code. The links to the keyer code are correct.
Reply 7 years ago
Good call! I corrected that. Thank you.