/* Copyright (C) 2013 Isak Eriksson, Linus Lindgren This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package bluetooth; import host.Configuration; import host.Joystick; import java.awt.Robot; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import lib.Constants; import lib.Protocol; import util.IdHandler; /** * * The BluetoothClient is a {@link Thread} that represents a client to the * {@link BluetoothServer} and contains a {@link DataInputStream} from the * client's bluetooth device. The thread interprets the incoming data from the * client and uses a {@link Robot} to give key presses on the keyboard. * * @author Linus Lindgren(linlind@student.chalmers.com) & Isak * Eriksson(isak.eriksson@mail.com) * */ public class BluetoothClient extends Thread { private BufferedInputStream bis; private BufferedOutputStream bos; private int clientId; private String clientName; private Robot robot; private boolean running; private long lastPoll; private HashMap<Integer, Joystick> joyStick; private static final int SLEEP_BETWEEN_READINGS = 10; /** * Constructor that tries to get a client ID. If the server is full the * constructor will throw an exception and close the {@link DataInputStream} * . * * @param dis * The data input stream from the client. * @throws Exception * Is thrown when the server is full. */ public BluetoothClient(BufferedInputStream bis, BufferedOutputStream bos) throws Exception { this.bis = bis; this.bos = bos; setClientId(IdHandler.getInstance(Configuration.getInstance().getNumberOfClients()).getUnoccupiedId()); if (getClientId() == -1) { serverFull(); throw new Exception("Server is full!"); } lastPoll = System.currentTimeMillis(); robot = new Robot(); joyStick = new HashMap<Integer, Joystick>(); clientName = ""; } /** * While the thread is running, it will constantly collect data from the * input stream and interpret the data by the standard described in * {@link Protocol}. */ @Override public void run() { running = true; ArrayList<Byte> data = new ArrayList<Byte>(); boolean escape = false; boolean arrayStopped = true; while (!interrupted() && running) { try { byte[] byteArray = new byte[Constants.MESSAGE_MAX_SIZE]; int len = 0; if (bis.available() > 0) len = bis.read(byteArray); for (int i = 0; i < len; i++) { byte b = byteArray[i]; if (arrayStopped) { if (b == Protocol.START) { if (escape) { escape = false; data.add(b); } else { arrayStopped = false; } } } else { if (!(b == Protocol.STOP || b == Protocol.ESCAPE)) { data.add(b); if (escape) { escape = false; } } else if (b == Protocol.ESCAPE) { if (escape) { data.add(b); escape = false; } else { escape = true; } } else if (b == Protocol.STOP) { if (escape) { escape = false; data.add(b); } else { try { interpretByteArray(data); } catch (IndexOutOfBoundsException e) { System.out.println("WRONG BYTE ARRAY: " + e.getMessage()); } data = new ArrayList<Byte>(); arrayStopped = true; } } } } } catch (IOException e) { System.out.println("Unable to read data from client"); disconnect(); } if (System.currentTimeMillis() - lastPoll >= lib.Constants.CLIENT_TIMEOUT) { System.out.println("Client with ID " + getClientId() + " timed out!"); disconnect(); } try { Thread.sleep(SLEEP_BETWEEN_READINGS); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * Disconnects the client by releasing all pressed keys, stopping the * {@link Thread}, stopping the {@link Joystick}, closing the * {@link DataInputStream} and removing the client from the * {@link BluetoothServer}. */ public void disconnect() { releaseKeys(); BluetoothServer.getInstance().removeClient(this); try { this.bis.close(); this.bos.close(); } catch (IOException e) { System.out.println(e.getMessage()); } running = false; System.out.println((this.clientName.isEmpty() ? "Client" + this.clientId : this.clientName) + " was disconnected"); } /** * Sends an indication to the client that the connection was accepted * */ public void connectionAccepted() { try { bos.write(Protocol.MESSAGE_TYPE_CONNECTION_ACCEPTED); bos.flush(); } catch (IOException e1) { e1.printStackTrace(); } } private void serverFull() { try { bos.write(Protocol.MESSAGE_TYPE_SERVER_FULL); Thread.sleep(Constants.SLEEP_BETWEEN_NOTIFY_AND_CLOSE); bos.close(); bis.close(); } catch (IOException e) { System.out.println(e.getMessage()); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } public void kick() { try { bos.write(Protocol.MESSAGE_TYPE_CLOSE); Thread.sleep(Constants.SLEEP_BETWEEN_NOTIFY_AND_CLOSE); } catch (IOException e) { System.out.println(e.getMessage()); } catch (InterruptedException e) { System.out.println(e.getMessage()); } disconnect(); } private void releaseKeys() { for (Integer keyCode : Configuration.getInstance().getClientKeyCodes(clientId)) { try { this.robot.keyRelease(keyCode); } catch (IllegalArgumentException e) { // Ignore keycodes that are invalid } } for (Joystick j : joyStick.values()) { j.setStopped(); } } public int getClientId() { return clientId; } public void setClientId(int id) { this.clientId = id; } public String getClientName() { return clientName; } public void setClientName(String clientName) { this.clientName = clientName; } private void handleButtonEvent(ArrayList<Byte> data) { if (BluetoothServer.isAllowClientInput()) { try { int i = Configuration.getInstance().getKeyCode(clientId, data.get(1)); if (data.get(2) == 0x01) { if (i != 0) robot.keyPress(i); } else { if (i != 0) robot.keyRelease(i); } } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } } private void handleJoystickEvent(ArrayList<Byte> data) { if (BluetoothServer.isAllowClientInput()) { byte[] floatByte = { data.get(2), data.get(3), data.get(4), data.get(5) }; float position = java.nio.ByteBuffer.wrap(floatByte).asFloatBuffer().get(); int joyStickId = data.get(1); if (!joyStick.containsKey(joyStickId)) { this.joyStick.put(joyStickId, new Joystick(joyStickId, clientId)); } joyStick.get(joyStickId).setNewValue(position); } } private void handleCloseEvent(ArrayList<Byte> data) { byte[] stringBytes = new byte[data.size() - 1]; for (int i = 1; i < data.size(); i++) { stringBytes[i - 1] = data.get(i); } String message = new String(stringBytes); System.out.println("Client closes connection: " + message); disconnect(); } private void handleNameEvent(ArrayList<Byte> data) { byte[] stringBytes2 = new byte[data.size() - 1]; for (int i = 1; i < data.size(); i++) { stringBytes2[i - 1] = data.get(i); } String name = new String(stringBytes2); System.out.println("Client name: " + name); setClientName(name); } private void interpretByteArray(ArrayList<Byte> data) throws IndexOutOfBoundsException { int id = data.get(0); switch (id) { case Protocol.MESSAGE_TYPE_BUTTON: handleButtonEvent(data); break; case Protocol.MESSAGE_TYPE_JOYSTICK: handleJoystickEvent(data); break; case Protocol.MESSAGE_TYPE_CLOSE: handleCloseEvent(data); break; case Protocol.MESSAGE_TYPE_NAME: handleNameEvent(data); break; case Protocol.MESSAGE_TYPE_POLL: lastPoll = System.currentTimeMillis(); break; } } }