Simply put, a chorded keyboard is a keyboard device that you use with one hand, where different combinations of buttons ("chords") represent different keys on a keyboard. All the designs I have seen involve some bulky device you must either carry around or attach to your arm. They also have many buttons to increase the number of available chord combinations. I wanted a device that was not intrusive, easy to learn to use, and did not obstruct the use of my hand. Enter the glove.
Hardware
I am building a glove where buttons are pressed by tapping them on a hard surface, such as a table. A conductive surface, such as conductive fabric, will be attached to the fingertips, and will act as capacitive switches. A small amount of fabric or foam will separate the fingertips from the plate, and the change in capacitance is measured (see Capacitive Sensing). As this is my first project in the "wearables" subgenre of DIY electronics, I had planed on using a LilyPad. However, I settled on the Teensy 2.0, which is much smaller, cheaper, and uses a ATmega32u4 instead, allowing native USB support (and can easily be made into a HID). The wearable community had a great suggestion of using crimp beads to attach smd devices to fabric. 4 LEDs will signal which mode the glove is in. There will also be a "soft-off" toggle switch (so I can pick up a pencil or open a door, for example).Software
Having only five fingers creates a problem for chorded keyboards. There are only 2^5 = 32 possible chord combinations with 5 buttons. If you subtract one (because the "null" chord, where no buttons are pressed, is unusable because that is when you aren't pressing anything!), we are down to 31 chord combinations. Considering there are 26 letters, we can see there certainly aren't going to be enough chords to emulate a full keyboard. The solution is the same as is used on phone keyboards (and traditional keyboards, for that matter): switchable modes.
The colored X's correspond to each possible chord combo. I am an avid Guitar Hero player, so I used the color codes from the game. The left column is the decimal form of each chord. Take, for example, chord 17: green and orange. 17 is 10001 in binary. As you can see, the first '1' means that green is pressed, the last '1' means orange is pressed, and the '0's mean the other respective keys are not pressed. Chords 3 and 17 I am using as mode switch chords. Pressing one of these two shifts the glove through each of the modes: standard, capital, arrows, and symbols.
I really need to point out that I am left handed (and planning to use this on my dominant hand), which explains the layout (PRMIT, for pinky, ring, middle, index, thumb; from left to right). If you are right-handed, and you want to build something like this, some software changes are probably necessary. I matched keys to chords, roughly such that more common letters have easier chords. I also tried to make them easy to remember. For example, 'm' is 11100, because the chord, green-red-yellow, looks like an upside-down 'm'. Similarly, 'w' is 01110 and 'n' is 01100. I then filled in symbols, numbers, and a few keyboard shortcuts in the remaining spots.
So we currently have 5 bits that correspond to the chord itself. If we tack on two more bits to represent the current mode (standard = 00, capital=01, arrows=10, symbols=11), we are up to 7. To make it an even byte, I tacked on a final bit where '1' refers to a "special case" (either a mode switch chord or a keyboard shortcut, such as Alt+Tab). I generated all the combinations, and made a table matching them to the ASCII keyboard code (and I converted both to HEX so it was easier to look at). Both columns are a single byte. We are dealing with less than 128 pairs of bytes. Really what we need then is a database that the Teensy can use to lookup which keyboard command to send with which chord.
Fortunately, the Teensy (and all Ardunios and compatibles, I think) have 1 KB of EEPROM. EEPROM is perfect for this task. It is for long term data storage which needs to persist after the device is powered off (devices with firmware, such as your motherboard's CMOS, use EEPROM or similar stuff). Also, it is pretty fast to do a lookup, which will lighten the load on the microprocessor, which will need to be able ot listen for button presses and do other strenuous calculations really fast. The chord byte we generated can be the address in the EEPROM, and the data stored there is the ASCII byte to send to the keyboard.
Here is the sketch that writes and checks the firmware (it is just the EEPROM.read example sketch with a bunch of write() statements during setup):
/* * EEPROM Read * * Reads the value of each byte of the EEPROM and prints it * to the computer. * This example code is in the public domain. */ #include <EEPROM.h> // start reading from the first byte (address 0) of the EEPROM int address = 0; byte value; void setup() { // initialize serial and wait for port to open: Serial.begin(9600); while (!Serial) { ; // wait for serial port to connect. Needed for Leonardo only }
EEPROM.write(0,0x0);EEPROM.write(1,0x0); EEPROM.write(3,0x0);EEPROM.write(5,0x0); EEPROM.write(7,0x0);EEPROM.write(8,0x40); EEPROM.write(10,0x20);EEPROM.write(12,0x0A); EEPROM.write(14,0x30);EEPROM.write(16,0x6F); EEPROM.write(18,0x4F);EEPROM.write(20,0xD7); EEPROM.write(22,0x31);EEPROM.write(25,0x0); EEPROM.write(27,0x0);EEPROM.write(29,0x0); EEPROM.write(31,0x0);EEPROM.write(32,0x61); EEPROM.write(34,0x41);EEPROM.write(36,0xD9); EEPROM.write(38,0x2E);EEPROM.write(40,0x6C); EEPROM.write(42,0x4C);EEPROM.write(44,0xD6); EEPROM.write(46,0x3F);EEPROM.write(48,0x73); EEPROM.write(50,0x53);EEPROM.write(52,0x29); EEPROM.write(54,0x32);EEPROM.write(56,0x75); EEPROM.write(58,0x55);EEPROM.write(60,0x7D); EEPROM.write(62,0x2A);EEPROM.write(64,0x74); EEPROM.write(66,0x54);EEPROM.write(68,0xDA); EEPROM.write(70,0x2C);EEPROM.write(72,0x75); EEPROM.write(74,0x55);EEPROM.write(76,0xD3); EEPROM.write(78,0x21);EEPROM.write(80,0x64); EEPROM.write(82,0x44);EEPROM.write(84,0x5D); EEPROM.write(86,0x22);EEPROM.write(88,0x7A); EEPROM.write(90,0x5A);EEPROM.write(92,0x84); EEPROM.write(94,0x2B);EEPROM.write(96,0x6E); EEPROM.write(98,0x4E);EEPROM.write(100,0x28); EEPROM.write(102,0x27);EEPROM.write(104,0x79); EEPROM.write(106,0x59);EEPROM.write(109,0x0); EEPROM.write(110,0x3D);EEPROM.write(112,0x77); EEPROM.write(114,0x57);EEPROM.write(116,0x2F); EEPROM.write(118,0x33);EEPROM.write(120,0x6A); EEPROM.write(122,0x4A);EEPROM.write(124,0xD2); EEPROM.write(126,0x39);EEPROM.write(128,0x65); EEPROM.write(130,0x45);EEPROM.write(132,0xD8); EEPROM.write(134,0x36);EEPROM.write(137,0x0); EEPROM.write(139,0x0);EEPROM.write(141,0x0); EEPROM.write(143,0x0);EEPROM.write(144,0x68); EEPROM.write(146,0x48);EEPROM.write(148,0x5F); EEPROM.write(150,0x3A);EEPROM.write(152,0x67); EEPROM.write(154,0x47);EEPROM.write(156,0xB1); EEPROM.write(158,0x23);EEPROM.write(160,0x72); EEPROM.write(162,0x52);EEPROM.write(164,0x5B); EEPROM.write(166,0x3B);EEPROM.write(168,0x70); EEPROM.write(170,0x50);EEPROM.write(173,0x0); EEPROM.write(174,0x5E);EEPROM.write(176,0x66); EEPROM.write(178,0x46);EEPROM.write(180,0xC6); EEPROM.write(182,0x40);EEPROM.write(184,0x6B); EEPROM.write(186,0x4B);EEPROM.write(188,0x3C); EEPROM.write(190,0x7E);EEPROM.write(192,0x69); EEPROM.write(194,0x49);EEPROM.write(196,0x2D); EEPROM.write(198,0x37);EEPROM.write(200,0x62); EEPROM.write(202,0x42);EEPROM.write(204,0x7B); EEPROM.write(206,0x25);EEPROM.write(208,0x63); EEPROM.write(210,0x43);EEPROM.write(212,0x87); EEPROM.write(214,0x24);EEPROM.write(216,0x78); EEPROM.write(218,0x58);EEPROM.write(220,0x60); EEPROM.write(222,0x7C);EEPROM.write(224,0x6D); EEPROM.write(226,0x4D);EEPROM.write(228,0x5C); EEPROM.write(230,0x38);EEPROM.write(232,0x71); EEPROM.write(234,0x51);EEPROM.write(236,0x3E); EEPROM.write(238,0x26);EEPROM.write(240,0x8); EEPROM.write(242,0x8);EEPROM.write(244,0xD5); EEPROM.write(246,0x34);EEPROM.write(248,0xB0); EEPROM.write(250,0xB0);EEPROM.write(252,0xB3); EEPROM.write(254,0x35);EEPROM.write(255,0x0); Serial.println("Done writing. Reading:"); } void loop() { // read a byte from the current address of the EEPROM value = EEPROM.read(address); //Serial.print(address); //Serial.print("\t"); Serial.print(value, HEX); //Serial.println(); // advance to the next address of the EEPROM address = address + 1; // there are only 512 bytes of EEPROM, from 0 to 511, so if we're // on address 512, wrap around to address 0 if (address == 512) address = 0; delay(500); } Then, a new sketch actually reads the button presses, formats the chord byte correctly, and does an EEPROM lookup. The code is nothing near complete. The special cases are still missing, mode switching still isn't implemented, and the program is still printing to Serial instead of emulating a keyboard (I don't have a Teensy yet, so I'm using a Duemilanove, which doesn't have native USB support short of serial via FTDI, to test the software). It is also littered with comments of future features and removed test lines, without any sort of explanation of the current lines in most places. I will update the code when it is more functional. It uses bitwise operations as often as possible to maximize speed and keep variables byte-size (no pun intended) for EEPROM.
#include <CapSense.h> #include <EEPROM.h> CapSense cs_4_2 = CapSense(3,2); CapSense cs_4_6 = CapSense(7,6); CapSense cs_4_8 = CapSense(9,8); CapSense cs_4_10 = CapSense(11,10); CapSense cs_4_12 = CapSense(13,12); byte mode = 0; void setup(){ Serial.begin(9600); //wait for driver } void loop(){ byte recd = 0; long start=millis(); while(millis()<start+300){ recd = recd|listener(); } //Serial.print(cs_4_2.capSense(30));Serial.print("\t"); //Serial.print(cs_4_6.capSense(30));Serial.print("\t"); //Serial.print(cs_4_8.capSense(30));Serial.print("\t"); //Serial.print(cs_4_10.capSense(30));Serial.print("\t"); //Serial.print(cs_4_12.capSense(30));Serial.print("\t"); if(recd>0){ recd=recd<<2 +mode; recd=recd<<1; if (recd==0x18 || recd==0x88 || recd==0x1A || recd==0x8A || recd==0x1C || recd==0x8C || recd==0x6C || recd==0xAC || recd==0x1E || recd==0x8E){ recd=recd+1; specialkey(recd); }else{ sendkey(recd); } Serial.print("\t"); Serial.println(mode); } // for miliseconds // recd=listener(recd) //if recd not null //recd=bitwise shift << x2, add mode, bitwise shift<< //if recd=(list of special cases), add 1 //if first bit is 1, specialkey(recd) else sendkey(recd) } byte listener(){ byte held=0; if (cs_4_2.capSense(30) > 10){ held=held+1; }held=held<<1; if (cs_4_6.capSense(30) > 10){ held=held+1; }held=held<<1; if (cs_4_8.capSense(30) > 10){ held=held+1; }held=held<<1; if (cs_4_10.capSense(30) > 10){ held=held+1; }held=held<<1; if (cs_4_12.capSense(30) > 10){ held=held+1; } return held; } void sendkey(byte srecd){ //read eeprom, send key Serial.write(EEPROM.read(srecd)); } void specialkey(byte srecd){ //mode forward: 19, 1B, 1D, 1F //mode backward: 89, 8B, 8D, 8F //alt-tab 6D //ctlaltdel AD Serial.print("SPECIAL:\t"); //if(srecd==0x6D){Serial.println("alt-tab");} //if(srecd==0xAD){Serial.println("ctlaltdel");} //if(srecd==0x19 //|| srecd==0x1B //|| srecd==0x1D){Serial.println("up mode"); mode++;} // if(srecd==0x8B //|| srecd==0x8D //|| srecd==0x8F){Serial.println("down mode"); mode--;} //if(srecd==0x1F){mode=0;} //if(srecd==0x89){mode=4;} }