package bugbeechat;
import com.buglabs.application.IServiceProvider;
import com.buglabs.application.MainApplicationThread;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import com.buglabs.bug.module.bugbee.pub.IBUGBeeControl;
import com.buglabs.bug.module.bugbee.pub.IBUGBeeLEDControl;
import com.buglabs.bug.module.bugbee.pub.IBUGBeePacket;
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
import com.buglabs.device.IButtonEventProvider;
/**
* BUGBee-to-BUGBee sample chatting application.
*
* To use, create /home/root/chatboss on one (and only one) of the BUGs.
*
* Once the two instances of the app are talking to each other, you can use the BUGbase
* hotkey 1 to toggle the display of the matchbox on-screen keyboard. This will let you
* type text into the top pane of the application's window. Anything you type into this
* pane will be sent to the other BUG when you press RET.
*
* Can also be used to communicate between a Digi XBee ZB device
* {@linkplain http://www.digi.com/products/wireless/zigbee-mesh/xbee-zb-module.jsp} and a BUGBee;
* if you want to do that please get the XBee ZB's full IEEE Address using the commands ATSH and STSL,
* then modify {@link NetworkHandler#stateMachineProc()} so that the
* {@link BUGBeeChatApplication#STATE_BIND_REQUEST} case statement uses that address rather
* than an all-zeros address (in my testing the XBee ZB didn't usually accept the bind request unless
* it was explicitly addressed to it). Note that ATSH and ATSL each make up 4 bytes of the 8 bytes of
* the XBee ZB's IEEE address so if the value either of them returns is only 3 bytes then you will need to
* prepend a zero byte.
*
* TODO: Add on-gui status of bind status (me->peer and peer->me)
*
* TODO: CONTROL: Unable to send data to our peer, status was 0xb9.
* <- This is "ZApsNoBoundDevice".
*
* TODO: CONTROL: Ignoring unrecognized command id of 0x8047
* <- This is not in the CC2480 Interface Specification doc
*
* TODO: CONTROL: Ignoring unrecognized command id of 0x8247
* <- This is not in the CC2480 Interface Specification doc
*
* TODO: Add connection-loss detection and recovery
*/
public class BUGBeeChatApplication extends MainApplicationThread implements DataReceiver {
/**
* The name of the file that is used to indicate which BUG is to
* act as the COORDinator of the ZigBee PAN.
* TODO: Make it so we don't need this - it's lame.
*/
private static final String COORD_FILE = "/home/root/chatboss";
/**
* The name of the file that is used to indicate that we
* should echo back messages that we receive (principally for
* testing).
*/
private static final String ECHO_FILE = "/home/root/chatecho";
/**
* The name of the file that is used to indicate that we
* should send out ping messages periodically (principally for
* testing).
*/
private static final String PING_FILE = "/home/root/chatping";
/**
* Delay in milliseconds between pings; only relevant if PING_FILE exists.
*/
private static final int PING_DELAY = 5000;
/**
* Debug flag to put log messages into text boxes as well as
* the 'last message' status fields.
*/
private static final boolean LOG_TO_SCREEN = false;
/**
* font to use for our plain labels
*/
private static final Font labelFont = new Font("serif", Font.BOLD, 10);
/**
* font to use for our status labels
*/
private static final Font statusFont = new Font("serif", Font.ITALIC, 10);
/**
* font to use for our text areas
*/
private static final Font textFont = new Font("serif", Font.PLAIN, 14);
/**
* Time, in milliseconds, to wait between polls of the data
* and control sockets.
* TODO: make blocking calls work properly, since that would be more efficient.
* As it is they don't get cleaned up properly in error cases so everything
* works better using MSG_DONTWAIT.
*/
private static final int THREAD_SLEEP_TIME = 300;
/**
* Maximum number of characters to keep in our "Remote -> Local" text
* area. Without a cap we'd eventually run out of memory.
*/
private static final int MAX_SCROLLBACK = 200;
/*
* bog-standard BUG app stuff
*/
private IServiceProvider serviceProv;
private boolean ran = false;
private IBUGBeeLEDControl led;
private IBUGBeeControl control;
private IBUGBeePacket packet;
private IModuleDisplay display;
private IButtonEventProvider button;
private Frame frame;
/**
* DateFormat used to display timestamps in the status labels.
*/
private final DateFormat dateFormat = new SimpleDateFormat("HH:mm");
/**
* Where we display text entered by our user, via the matchbox keyboard (or otherwise)
*/
private TextArea local2RemoteText;
/**
* State of the local->remote connection
*/
private Label local2RemoteStatus;
/**
* Where we display text received from our peer.
*/
private TextArea remote2LocalText;
/**
* State of the local->remote connection
*/
private Label remote2LocalStatus;
/**
* Class to manage the matchbox virtual keyboard
*/
private KeyboardHandler keyboardHandler;
private NetworkHandler network;
private final boolean runAsCoord;
private final boolean doEcho;
private final boolean doPing;
public BUGBeeChatApplication(IServiceProvider serviceProv) {
this.serviceProv = serviceProv;
// read out cheesy file-based "configuration"
runAsCoord = (new File(COORD_FILE)).exists();
doEcho = (new File(ECHO_FILE)).exists();
doPing = (new File(PING_FILE)).exists();
}
public boolean getRan() {
return ran;
}
private void ran() {
ran = true;
}
/**
* This method is invoked as a result of all services
* becoming available for the application. The list of services is
* obtained from the getServices() method.
*/
public void run() {
try {
log("RUN: BUGBeeChatApplication");
serviceSetup();
// do this before gui setup so gui can ask if we're coord
network = new NetworkHandler(control, led, packet, runAsCoord, this);
guiSetup();
keyboardSetup();
network.networkSetup();
log("RUN: Starting main loop");
// loop in our state machine until we're told to go away
int pingCount = 0;
long lastTimePinged = System.currentTimeMillis();
while (!tearDownRequested) {
network.poll();
if (network.isConnected() && doPing) {
final long now = System.currentTimeMillis();
if ((now - lastTimePinged) > PING_DELAY) {
final String msg;
if (runAsCoord) {
msg = "Parent ping " + pingCount;
} else {
msg = "Child ping " + pingCount;
}
network.sendDataMessage(msg);
++pingCount;
// don't use now as we want ping end-to-start to be PING_DELAY,
// rather than ping start-to-start
lastTimePinged = System.currentTimeMillis();
}
}
try {
Thread.sleep(THREAD_SLEEP_TIME);
} catch (InterruptedException e) {
}
}
log("RUN: BUGBeeChatApplication shutting down");
network.networkTearDown();
guiTearDown();
serviceTearDown();
keyboardTearDown();
log("RUN: BUGBeeChatApplication stopped");
} catch (IOException e) {
e.printStackTrace();
}
log("RUN: Done!");
ran();
}
private void serviceSetup() {
// hook up to all of our services
led = (IBUGBeeLEDControl) serviceProv.getService(IBUGBeeLEDControl.class);
control = (IBUGBeeControl) serviceProv.getService(IBUGBeeControl.class);
packet = (IBUGBeePacket) serviceProv.getService(IBUGBeePacket.class);
display = (IModuleDisplay) serviceProv.getService(IModuleDisplay.class);
button = (IButtonEventProvider) serviceProv.getService(IButtonEventProvider.class);
}
private void serviceTearDown() {
led = null;
control = null;
packet = null;
display = null;
button = null;
}
private void keyboardSetup() {
keyboardHandler = new KeyboardHandler();
button.addListener(keyboardHandler);
}
private void keyboardTearDown() {
if (keyboardHandler != null) {
if (button != null) {
button.removeListener(keyboardHandler);
}
// make sure the virtual keyboard is gone
keyboardHandler.stopKeyboard();
keyboardHandler = null;
}
}
private void guiSetup() {
// bring up our GUI
createUI();
frame.show();
}
private void guiTearDown() {
frame.dispose();
frame = null;
}
/**
* Create our fabulous user interface.
* TODO: figure out how to hide the Remote->Local text box when the
* virtual keyboard is on-screen; I cannot get this to work, and with
* them both on-screen the Local->Remote box is so squished you can
* hardly see what you're "typing".
*/
private void createUI() {
frame = display.getFrame();
frame.setTitle("BUGBeeChat");
// set up our Local -> Remote gui stuff
final Panel local2RemoteLabelPanel = new Panel(new GridLayout(1, 2));
final Label local2RemoteLabel;
if (runAsCoord) {
local2RemoteLabel = new Label("Local -> Remote [Parent]");
} else {
local2RemoteLabel = new Label("Local -> Remote [Child]");
}
local2RemoteLabel.setFont(labelFont);
local2RemoteLabelPanel.add(local2RemoteLabel);
local2RemoteStatus = new Label("not connected");
local2RemoteStatus.setFont(statusFont);
local2RemoteLabelPanel.add(local2RemoteStatus);
local2RemoteText = new TextArea();
local2RemoteText.setEditable(true);
local2RemoteText.setFont(textFont);
local2RemoteText.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent arg0) {
if (arg0.getKeyCode() == KeyEvent.VK_ENTER) {
sendLocalText();
}
}
public void keyReleased(KeyEvent arg0) {}
public void keyTyped(KeyEvent arg0) {}});
final Panel local2RemotePanel = new Panel(new BorderLayout());
local2RemotePanel.add(local2RemoteLabelPanel, BorderLayout.NORTH);
local2RemotePanel.add(local2RemoteText, BorderLayout.CENTER);
// set up our Remote -> Local gui stuff
final Panel remote2LocalLabelPanel = new Panel(new GridLayout(1, 2));
final Label remote2LocalLabel = new Label("Remote -> Local");
remote2LocalLabel.setFont(labelFont);
remote2LocalLabelPanel.add(remote2LocalLabel);
remote2LocalStatus = new Label("not connected");
remote2LocalStatus.setFont(statusFont);
remote2LocalLabelPanel.add(remote2LocalStatus);
remote2LocalText = new TextArea();
remote2LocalText.setEditable(true);
remote2LocalText.setFont(textFont);
// listen for the user pressing Enter in the Local->Remote
// text box, so we can send the data to Remote.
remote2LocalText.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent arg0) {
if (arg0.getKeyCode() == KeyEvent.VK_ENTER) {
sendLocalText();
}
}
public void keyReleased(KeyEvent arg0) {}
public void keyTyped(KeyEvent arg0) {}});
final Panel remote2LocalPanel = new Panel(new BorderLayout());
remote2LocalPanel.add(remote2LocalLabelPanel, BorderLayout.NORTH);
remote2LocalPanel.add(remote2LocalText, BorderLayout.CENTER);
// put it actually on the screen
frame.setLayout(new GridLayout(2, 1));
frame.add(local2RemotePanel);
frame.add(remote2LocalPanel);
frame.show();
local2RemoteText.requestFocus();
}
/**
* Send the text the user has entered over to our peer.
*/
private void sendLocalText() {
try {
network.sendDataMessage(local2RemoteText.getText());
local2RemoteText.setText("");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Provides a list of service names that this application depends on.
*
*/
public List getServices() {
final List services = new ArrayList();
services.add(IBUGBeeLEDControl.class.getName());
services.add(IBUGBeeControl.class.getName());
services.add(IBUGBeePacket.class.getName());
services.add(IModuleDisplay.class.getName());
services.add(IButtonEventProvider.class.getName());
return services;
}
private String timestampify(final String msg) {
return dateFormat.format(new Date()) + ": " + msg;
}
private void log(final String msg) {
System.out.println(msg);
if (LOG_TO_SCREEN && (local2RemoteText != null)) {
remote2LocalText.append("[" + msg + "]\n");
}
}
public void receiveData(final String data) {
remote2LocalText.append(data);
trimScrollback();
if (doEcho) {
try {
network.sendDataMessage(data);
} catch (IOException e) {
log("Failed to echo back msg: '" + data + "', error: " + e.getMessage());
}
}
}
private void trimScrollback() {
int len = remote2LocalText.getText().length();
if (len > MAX_SCROLLBACK) {
remote2LocalText.replaceRange("", 0, len - MAX_SCROLLBACK);
len = remote2LocalText.getText().length();
remote2LocalText.select(len, len);
}
}
public void updateLocal2RemoteStatus(final String msg) {
local2RemoteStatus.setText(timestampify(msg));
if (LOG_TO_SCREEN) {
// don't put this in local2Remote or we'll try and send it...
remote2LocalText.append("[" + timestampify(msg) + "]\n");
}
}
public void updateRemote2LocalStatus(final String msg) {
remote2LocalStatus.setText(timestampify(msg));
if (LOG_TO_SCREEN) {
remote2LocalText.append("[" + timestampify(msg) + "]\n");
}
}
}