package lejos.addon.keyboard;
import java.io.*;
import javax.bluetooth.*;
import javax.microedition.io.*;
import lejos.util.Delay;
/*
* Developer Notes:
* TODO: The preferred paradigm is to allow multiple text fields, multiple apps in MIDP
* and the one with focus is the one that gets the key events. see javax.microedition.lcdui.ItemStateListener?
*
* TODO: Would be slick if it would connect to leJOS as soon as it is turned on, like Windows XP.
*
* Programming keyboard input is not very simple or straightforward.
* This class basically converts the keyboard scan code into a VK constant (virtual keyboard)
* located in KeyEvent. The method that does this is getJavaConstant().
* Then this class then looks at the modifier keys (like shift) and converts the VK
* constant into the proper ASCII character.
*
* Freedom Universal is an AT keyboard, not XT. It uses Scan Code Set 2:
* http://www.computer-engineering.org/ps2keyboard/
* You will not find characters such as ? or lower case because those are Virtual Keys (VK).
* Here is a nice keyboard emulator which shows the scancode values to expect:
* http://www.barcodeman.com/altek/mule/kbemulator/
*/
/**
* <p>This class will only work with SPP keyboards, not standard HID
* keyboards. If it doesn't say it supports Bluetooth SPP then it
* will not work. There are currently only two known SPP models (also available
* on eBay or Amazon):
* <li>Freedom Universal Bluetooth keyboard<br>
* http://www.freedominput.com
* <li>iTech Virtual Keyboard (SPP only)<br>
* http://www.virtual-laser-keyboard.com/
* </p>
* <p>The SPP keyboards transmit keystrokes as one byte, not two like normal HID keyboards.
* For this reason, this class won't work properly with a regular HID keyboard as written.
* With modifications it could be made to work with both SPP and HID keyboards since
* the key tables are mostly the same.</p>
* <p>Note: This class is currently only tested with Freedom Universal. If you have problems with the iTech Virtual Keyboard, write to bbagnall@mts.net
* and I will try my best to adapt this class.</p>
* @author BB
*/
public class Keyboard extends Thread { // TODO: Use internal Thread so it isn't exposed (especially run()).
// Typematic Key Repeat:
/**
* Typematic delay - 0.25 seconds to 1.00 second (500ms default)
*/
private int typematicDelay = 500;
/**
* Typematic rate - 2.0 cps (characters per second) to 30.0 cps (10.9 default)
*/
private double typematicRate = 10.9;
private int ratePause = (int)(1000.0/typematicRate);
private char lastKeyPress = NOT_ASCII_CHAR;
private char oldChar = NOT_ASCII_CHAR;
/**
* Represents a key press that is not an ASCII character (e.g. Caps lock, Shift, etc...)
*/
private static char NOT_ASCII_CHAR = 0xFFFF;
/**
* Command used by keyboards. Full list of commands can be found here:
* http://www.computer-engineering.org/ps2keyboard/
*/
private static int ECHO = 0xEE;
/**
* Indicates if keyboard has caps lock on.
*/
private boolean capsLock = false;
/**
* Time between echo command sent to keyboard to prevent it
* from going to sleep. Universal Keyboard sleeps after 5000 ms
*/
private static int KEEP_ALIVE_DELAY = 4000; // milliseconds
private InputStream in = null;
private OutputStream out = null;
private int modifiers; // keeps track of modifier keys held down (shift, alt, etc)
private KeyListener keyListener = null;
private Keyboard kb = this; // Used by typematicThread
/**
* Thread to handle typematic key repeating.
* Typematic data is not buffered within the keyboard. In the case
* where more than one key is held down, only the last key pressed
* becomes typematic. Typematic repeat then stops when that key is
* released, even though other keys may be held down.
* "Set Typematic Rate/Delay" (0xF3) command
*/
private Thread typematicThread = new Thread() {
public void run() {
while(true) {
// Watch for key press that is char. (yield) If so, continue until released
// or new key pressed (doesn't matter which key, all interrupt typematic).
while(lastKeyPress == NOT_ASCII_CHAR) {Thread.yield();}
oldChar = lastKeyPress;
Delay.msDelay(typematicDelay);
while((lastKeyPress != NOT_ASCII_CHAR)&(oldChar == lastKeyPress)) {
KeyEvent ke = new KeyEvent(kb, KeyEvent.KEY_TYPED, System.currentTimeMillis(), modifiers, 0, lastKeyPress, 0);
notifyListeners(ke);
Delay.msDelay(ratePause);
}
}
}
};
/**
* Table converts the scan code into a Java VK constant (see KeyEvent constants).
* Index is the scancode, value is the Java constant
* 64 unique keys on Freedom Universal, many of these keys are not present but mapped for future compatibility
* The Fn key is not standard, so it is mapped to VK_META.
* TODO: Maybe Fn should not register at all as keypress? Instead, it outputs Esc, Pg_up, etc...
* as though they are key presses? Overrides actual key?
*/
/* DEVNOTES:
* http://www.computer-engineering.org/ps2keyboard/scancodes2.html
*
* F7 has a scancode of 0x83 which pushed this array much larger, KeyEvent.VK_ALT_GRAPH is an int.
* TODO: Use if-then later for high index values? (lot of zeros near end) like F7, SCROLL, PAGE UP, KP *.
* Didn't know how to handle multiple scan codes for print screen, pause. Also, keypad values interfere with multiple scancodes for page up, etc...
* note: VK_ALT is at 0x10 and 0x11 because Universal uses 0x10 for some reason.
*/
private static byte [] scanCodes = {
0,KeyEvent.VK_F9,KeyEvent.VK_META,KeyEvent.VK_F5,KeyEvent.VK_F3,KeyEvent.VK_F1,KeyEvent.VK_F2, KeyEvent.VK_WINDOWS, // 0x00 - 0x07
0,KeyEvent.VK_F10,KeyEvent.VK_F8,KeyEvent.VK_F6,KeyEvent.VK_F4,KeyEvent.VK_TAB,KeyEvent.VK_BACK_QUOTE,0, // 0x08 - 0F
KeyEvent.VK_ALT,KeyEvent.VK_ALT,KeyEvent.VK_SHIFT,KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_CONTROL,KeyEvent.VK_Q,KeyEvent.VK_1,0, // 0x10 - 0x17
0,0,KeyEvent.VK_Z,KeyEvent.VK_S,KeyEvent.VK_A,KeyEvent.VK_W,KeyEvent.VK_2, 0, // 0x18 - 0x1F
0,KeyEvent.VK_C,KeyEvent.VK_X,KeyEvent.VK_D,KeyEvent.VK_E,KeyEvent.VK_4,KeyEvent.VK_3,0, // 0x20 - 0x27
KeyEvent.VK_UP,KeyEvent.VK_SPACE,KeyEvent.VK_V,KeyEvent.VK_F,KeyEvent.VK_T,KeyEvent.VK_R,KeyEvent.VK_5,KeyEvent.VK_RIGHT, // 0x28 - 0x2F
0,KeyEvent.VK_N,KeyEvent.VK_B,KeyEvent.VK_H,KeyEvent.VK_G,KeyEvent.VK_Y,KeyEvent.VK_6,0, // 0x30 - 0x37
0,0,KeyEvent.VK_M,KeyEvent.VK_J,KeyEvent.VK_U,KeyEvent.VK_7,KeyEvent.VK_8,0, // 0x38 - 0x3F
KeyEvent.VK_UP, KeyEvent.VK_COMMA,KeyEvent.VK_K,KeyEvent.VK_I,KeyEvent.VK_O,KeyEvent.VK_0,KeyEvent.VK_9,0, // 0x40 - 0x47
0,KeyEvent.VK_PERIOD,KeyEvent.VK_SLASH,KeyEvent.VK_L,KeyEvent.VK_SEMICOLON,KeyEvent.VK_P,KeyEvent.VK_MINUS,0, // 0x48 - 0x4F
0,0,KeyEvent.VK_QUOTE,0,KeyEvent.VK_OPEN_BRACKET,KeyEvent.VK_EQUALS,0,0, // 0x50 - 0x57
KeyEvent.VK_CAPS_LOCK,KeyEvent.VK_SHIFT,KeyEvent.VK_ENTER,KeyEvent.VK_CLOSE_BRACKET,KeyEvent.VK_SPACE,KeyEvent.VK_BACK_SLASH,KeyEvent.VK_LEFT,0, // 0x58 - 0x5F
KeyEvent.VK_DOWN,0,0,0,0,0,KeyEvent.VK_BACK_SPACE,0, // 0x60 - 0x67
0,KeyEvent.VK_END,0,KeyEvent.VK_LEFT,KeyEvent.VK_HOME,0,0,0, // 0x68 - 0x6F
KeyEvent.VK_INSERT,KeyEvent.VK_DELETE,KeyEvent.VK_DOWN,0,KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_ESCAPE, KeyEvent.VK_NUM_LOCK, // 0x70 - 0x77
KeyEvent.VK_F11, KeyEvent.VK_PLUS, KeyEvent.VK_PAGE_DOWN, KeyEvent.VK_SUBTRACT, KeyEvent.VK_MULTIPLY // 0x78 - 0x7C
};
private DiscoveryAgent da;
private RemoteDevice btDevice = null;
private boolean doneInq = false;
// I think this indicates the BT device is a SPP device
private static final int SPP_DEVICE = 0x1F00;
/**
* Creates a new Keyboard instance using streams from the keyboard. Doesn't matter what the source is (Bluetooth, I2C, etc...)
* @param in
* @param out
*/
public Keyboard(InputStream in, OutputStream out) {
setup(in, out);
}
/**
* Helper method used by both constructors.
* @param in
* @param out
*/
private void setup(InputStream in, OutputStream out) {
this.in = in;
this.out = out;
this.setDaemon(true);
this.start();
// TODO: Perhaps check if keyboard is a Freedom Universal (buggy) first before switching these?
// Freedom Universal has a bug that switches DELETE and BACKSPACE values.
// Switching back in scanCodes array (above):
scanCodes[0x71] = KeyEvent.VK_BACK_SPACE;
scanCodes[0x66] = KeyEvent.VK_DELETE;
// Start typematic thread here:
typematicThread.setDaemon(true);
typematicThread.start();
}
/**
* Makes Bluetooth connection to SPP keyboard. The keyboard must have been paired already at
* the main menu. Will connect to the first paired SPP keyboard device that is turned on.
* NOTE: It can't distinguish between a GPS or Keyboard so make sure only the keyboard is on.
* @throws BluetoothStateException If it doesn't find an SPP device to connect to.
*/
// TODO: This constructor should throw exceptions, not catch them below!
public Keyboard() throws BluetoothStateException {
/* Developer Notes:
* The code in here is copied from BTLocationProvider. If that gets updated, should do same here and vice versa.
*/
/** Inner class DiscoveryListener:
*
*/
DiscoveryListener dl = new DiscoveryListener() {
/* DiscoveryListener methods: */
public void deviceDiscovered(RemoteDevice btdev, DeviceClass cod) {
//System.err.println(btdev.getFriendlyName(false) + " discovered.");
if((cod.getMajorDeviceClass() & SPP_DEVICE) == SPP_DEVICE) {
if(btdev.isAuthenticated()) { // Check if paired.
btDevice = btdev;
da.cancelInquiry(this);
}
}
}
public void inquiryCompleted(int discType) {
doneInq = true;
}
};
da = LocalDevice.getLocalDevice().getDiscoveryAgent();
da.startInquiry(DiscoveryAgent.GIAC, dl);
while(!doneInq) {Thread.yield();}
// TODO: What is the procedure if it fails to connect? Return? Throw BT exception?
if(btDevice == null) throw new BluetoothStateException("Nothing found.");
String address = btDevice.getBluetoothAddress();
String btaddy = "btspp://" + address;
try {
StreamConnectionNotifier scn = (StreamConnectionNotifier)Connector.open(btaddy);
if(scn == null) throw new BluetoothStateException("Failed to connect.");
StreamConnection c = scn.acceptAndOpen();
// This problem below occurred one time for my Holux GPS. The solution was to
// remove the device from the Bluetooth menu, then find and pair again.
// Need to throw exception with message.
if(c == null) throw new BluetoothStateException("Failed. Try pairing your device again.");
InputStream in = c.openInputStream();
OutputStream out = c.openOutputStream();
setup(in, out);
// c.close(); // TODO: Clean up when done. HOW TO HANDLE IN LOCATION?
} catch(IOException e) {
throw new BluetoothStateException("Failed to retrieve data streams.");
}
}
/**
* Typematic delay is the time after a key is held down that characters start repeating.
*
* @param delay 250 ms to 1000 ms (500ms default)
*/
public void setTypematicDelay(int delay) {
this.typematicDelay = delay;
}
/**
* Typematic delay is the time after a key is held down that characters start repeating.
* @return delay in milliseconds
*/
public int getTypematicDelay() {
return this.typematicDelay;
}
/**
* Typematic rate is the rate characters repeat when a key is held down.
* @param rate 2.0 cps (characters per second) to 30.0 cps (10.9 default)
*/
public void setTypematicRate(int rate) {
this.typematicRate = rate;
this.ratePause = (int)(1000.0/typematicRate);
}
/**
* Typematic rate is the rate characters repeat when a key is held down.
* @return Rate in characters per second (cps)
*/
public double getTypematicRate() {
return this.typematicRate;
}
/**
* Starts a KeyListener listening for events from the keyboard. Only one KeyListener is allowed.
*
* @param kl
*/
// TODO: If we expand our javax.microedition.lcdui functionality we should add ability for more listeners.
public void addKeyListener(KeyListener kl) {
this.keyListener = kl;
}
/**
* Removes the specified KeyListener from the Keyboard so it will no longer notify the listener of new events.
* @param kl
*/
public void removeKeylistener(KeyListener kl) {
if(this.keyListener == kl)
this.keyListener = null;
}
/**
* Notify listener if present.
* @param e The key event to send out
*/
private void notifyListeners(KeyEvent e) {
if(keyListener == null) return;
if(e.getID() == KeyEvent.KEY_PRESSED)
keyListener.keyPressed(e);
else if(e.getID() == KeyEvent.KEY_RELEASED)
keyListener.keyReleased(e);
else if(e.getID() == KeyEvent.KEY_TYPED)
keyListener.keyTyped(e);
}
public void run() {
long previousEcho = System.currentTimeMillis();
// TODO: Perhaps While connected is better. Then if disconnects, it reconnects and starts thread again?
while(true) {
// Keep-alive code:
long now = System.currentTimeMillis();
if(now - previousEcho >= KEEP_ALIVE_DELAY) {
try {
out.write(ECHO);
out.flush();
} catch(IOException e) {
// TODO: Thread could also reconnect if disconnected. Try powering off and on.
System.err.println("COMMAND EXCEPTION");
}
previousEcho = now;
}
// Notifier code:
try {
if(in.available() > 0) { // Check if byte available.
int bval = in.read();
// System.err.println("scancode: " + bval);
KeyEvent e = getKeyEvent(bval, System.currentTimeMillis());
if(e.getID() == KeyEvent.KEY_PRESSED) {
oldChar = NOT_ASCII_CHAR; // Prevents repetition if key pressed again.
lastKeyPress = e.getKeyChar();
} else if(e.getID() == KeyEvent.KEY_RELEASED) {
lastKeyPress = NOT_ASCII_CHAR;
}
notifyListeners(e);
// Generate KEY_TYPED event:
if((e.getID() == KeyEvent.KEY_PRESSED) & (e.getKeyChar() != NOT_ASCII_CHAR)) {
KeyEvent ke = new KeyEvent(this, KeyEvent.KEY_TYPED, e.getWhen(), modifiers, 0, e.getKeyChar(), 0);
notifyListeners(ke);
}
}
} catch(IOException e) {/*Debug.out("EXCEPTION");*/}
Thread.yield();
}
}
/**
* Helper method to generate keyEvent from scan code from keyboard
* @return
*/
private KeyEvent getKeyEvent(int scanCode, long timeStamp) {
short id = KeyEvent.KEY_PRESSED;
byte normalizedScanCode = (byte)scanCode;
if((byte)scanCode < 0) { // if 8th bit on (i.e. key release)
normalizedScanCode = (byte)(scanCode - 128); // remove 8th bit
id = KeyEvent.KEY_RELEASED;
}
int code = getJavaConstant(normalizedScanCode);
// Recalculate modifier
recalculateModifier(code, id);
// Handle Caps Lock pressed: Possibly beep here?
if((code == KeyEvent.VK_CAPS_LOCK) & (id == KeyEvent.KEY_PRESSED)) capsLock = !capsLock;
// Calculate location.
int location = getLocation(normalizedScanCode);
// Get ASCII character
char curChar = getAsciiChar(code);
return new KeyEvent(this, id, timeStamp, modifiers, code, curChar, location);
}
private int getLocation(int scanCode) {
int location = KeyEvent.KEY_LOCATION_STANDARD;
switch(scanCode) {
case 0x12: // L shift
case 0x14: // L ctrl
case 0x1F: // L gui (2 codes in real keyboard)
case 0x11: // L alt (0x11 on Standard keyboard)
case 0x10: // L alt (0x10 on Universal)
location = KeyEvent.KEY_LOCATION_LEFT;
break;
case 0x59: // R shift
case 0x13: // R alt gr (UK keyboards, incl. Freedom Universal)
//case 0x14: // R ctrl (2 codes on real keyboard)
//case 0x27: // R gui (2 codes on real keyboard)
//case 0x11: // R alt (2 codes on real keyboard)
location = KeyEvent.KEY_LOCATION_RIGHT;
break;
}
// TODO: Didn't implement KeyPad location. Not present on Freedom Universal keyboard.
return location;
}
/**
* Indicates whether or not caps lock is enabled for the keyboard. The only way to change caps lock is to presss the <code>Caps Lock</code> key.
* @return true if caps lock is on, false if it is off.
*/
public boolean isCapsLock() {
return capsLock;
}
/**
* Converts a Java constant into the proper ASCII character. For the most part, this method attempts to save memory by
* using the Java constant (code) to calculate the proper character. Sometimes it is a direct conversion by casting into
* a char, other times it needs a direct conversion to a different ASCII code.
* @param code
* @return
*/
private char getAsciiChar(int code) {
int ascii = code;
boolean shifted = (modifiers & KeyEvent.SHIFT_MASK) == KeyEvent.SHIFT_MASK;
// Letter keys:
if(code >= KeyEvent.VK_A & code <= KeyEvent.VK_Z) {
boolean capitalize = isCapsLock() ^ shifted;
if(!capitalize) ascii += 32; // convert to lower case
return (char)ascii;
}
// Number keys:
else if(code >= KeyEvent.VK_0 & code <= KeyEvent.VK_9) { // TODO: Redundant if you are using switch below?
// Shift
if(shifted) {
switch (code) {
case KeyEvent.VK_1:
case KeyEvent.VK_3:
case KeyEvent.VK_4:
case KeyEvent.VK_5:
ascii -= 16;
break;
case KeyEvent.VK_7:
case KeyEvent.VK_9:
ascii -= 17;
break;
case KeyEvent.VK_2:
ascii += 14;
break;
case KeyEvent.VK_6:
ascii += 40;
break;
case KeyEvent.VK_8:
ascii -= 14;
break;
case KeyEvent.VK_0:
ascii -= 7;
break;
}
}
return (char)ascii;
}
// Various characters:
// TODO: I'm not sure if all this branching logical code saves over a simple table array.
// Direct ASCII translation: ; - = [ ] \ ; , . / NOT: ` '
int shiftModifier = 0;
switch (code) {
case KeyEvent.VK_SPACE:
return (char)ascii;
case KeyEvent.VK_MINUS:
shiftModifier = (shifted?50:0);
return(char)(ascii + shiftModifier);
case KeyEvent.VK_EQUALS:
shiftModifier = (shifted?-18:0);
return(char)(ascii + shiftModifier);
case KeyEvent.VK_OPEN_BRACKET:
case KeyEvent.VK_CLOSE_BRACKET:
case KeyEvent.VK_BACK_SLASH:
shiftModifier = (shifted?32:0);
return(char)(ascii + shiftModifier);
case KeyEvent.VK_SEMICOLON:
shiftModifier = (shifted?-1:0);
return(char)(ascii + shiftModifier);
case KeyEvent.VK_QUOTE:
ascii = 39; // Correct ascii value for '
shiftModifier = (shifted?-5:0);
return(char)(ascii + shiftModifier);
case KeyEvent.VK_COMMA:
case KeyEvent.VK_PERIOD:
case KeyEvent.VK_SLASH:
shiftModifier = (shifted?16:0);
return(char)(ascii + shiftModifier);
case KeyEvent.VK_BACK_QUOTE: // TODO: This char is suspicious on leJOS. ` or ~ don't show up right. Test raw values for char.
ascii = 96; // Correct ascii value for `
shiftModifier = (shifted?30:0);
return(char)(ascii + shiftModifier);
}
// Handle Enter, Tab, Backspace
switch (code) {
case KeyEvent.VK_ENTER:
case KeyEvent.VK_TAB:
case KeyEvent.VK_BACK_SPACE:
return(char)ascii;
}
// The TYPED char should only appear if valid. Ctrl, Shift etc.. do NOT produce TYPED events.
return NOT_ASCII_CHAR;
}
private int getJavaConstant(byte scanCode) {
// Code to check if Fn held down, and if Esc, Pg Up, Pg Down, Home, End pressed.
if((modifiers & KeyEvent.META_MASK) == KeyEvent.META_MASK) {
switch(scanCodes[scanCode]) {
case KeyEvent.VK_BACK_QUOTE:
return KeyEvent.VK_ESCAPE;
case KeyEvent.VK_UP:
return KeyEvent.VK_PAGE_UP;
case KeyEvent.VK_DOWN:
return KeyEvent.VK_PAGE_DOWN;
case KeyEvent.VK_LEFT:
return KeyEvent.VK_HOME;
case KeyEvent.VK_RIGHT:
return KeyEvent.VK_END;
}
}
return scanCodes[scanCode];
}
/**
* Determines which modifier keys are held down.
* @param code
* @param id
*/
private void recalculateModifier(int code, short id) {
int val = 0;
switch (code) {
case KeyEvent.VK_SHIFT:
val = KeyEvent.SHIFT_MASK;
break;
case KeyEvent.VK_CONTROL:
val = KeyEvent.CTRL_MASK;
break;
case KeyEvent.VK_META:
val = KeyEvent.META_MASK;
break;
case KeyEvent.VK_ALT:
val = KeyEvent.ALT_MASK;
break;
case KeyEvent.VK_ALT_GRAPH:
val = KeyEvent.ALT_GRAPH_MASK;
break;
}
if(id == KeyEvent.KEY_PRESSED) {
// ADD new modifier
modifiers += val;
} else if(id == KeyEvent.KEY_RELEASED) {
// SUBTRACT new modifier
modifiers -= val;
}
}
}