package bugbeechat;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
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.bugbee.pub.IEEEAddress;
public class NetworkHandler {
/**
* maximum size of the data portion of a packet
* TODO: This should be in the API.
*/
public static final int MAX_DATA_SIZE = 84;
/*
* The various states of our state machine. The numerical values have no significance.
*/
private static final int STATE_INIT = 0;
private static final int STATE_START = 1;
private static final int STATE_JOINING = 2;
private static final int STATE_WAIT_FOR_PEER_TO_BIND_TO_US = 3;
private static final int STATE_BIND_REQUEST = 4;
private static final int STATE_WAIT_FOR_BINDING_RESPONSE = 5;
private static final int STATE_CAN_SEND_DATA = 6;
/**
* The minimum length of a command response, that we could
* receive via our command proc (1 byte data len, which would
* be set to zero plus 2 bytes for the command id)
*/
private static final int MIN_COMMAND_RESPONSE_LENGTH = 3;
/**
* The maximum length of a command response, that we could
* receive via our command proc.
* TODO: Figure out what this should really be. Around 300?
*/
private static final int MAX_MESSAGE_LENGTH = 512;
/**
* The default source and destination endpoint ID on the XBee ZBs
* (ATSE and ATDE commands).
* Used for interoperability with XBee ZBs
*/
private static final byte DIGI_DATA_ENDPOINT_ID = (byte) 0xE8;
/**
* The default cluster ID on the XBee ZBs (ATCI command).
* This is the 'send or receive plain old character data' "function"
* Used for interoperability with XBee ZBs
*/
private static final short DIGI_TRANSPARENT_DATA_CLUSTER_ID = 0x0011;
/**
* The profile id on the XBee ZBs (don't think you can change this one)
* Used for interoperability with XBee ZBs
*/
private static final short DIGI_PROFILE_ID = (short) 0xC105;
/**
* Our device id. This is arbitrary, and doesn't need to be unique.
*/
private static final short DEVICE_ID = 0x0003;
/**
* Our version. This is arbitrary, and doesn't need to be unique.
*/
private static final byte DEVICE_VERSION = 1;
/**
* When sending data, TRUE if requesting acknowledgment from the destination.
*/
private static final byte WANT_ACK = 1;
/**
* When sending data, the max number of hops the packet can travel
* through before it is dropped.
*/
private static final byte RADIUS = 7;
/**
* This is the binding address and should be used when a
* binding entry has been previously created for this particular
* CommandId/ClusterId. The destination address will be determined
* from the binding table by the ZigBee chip.
*/
private static final short BINDING_ADDRESS = (short) 0xFFFE;
/**
* the PAN ID we want to establish / join
*/
private static final short PAN_ID = 0x1111;
/**
* the channel mask to use when establishing / joining the PAN.
*/
private static final int CHANNEL_LIST = 0x00010000;
/**
* If true the COORD will wait for the ED to bind to it before
* attempting to bind to the ED.
* TODO: figure out if binding to each other simultaneously
* is causing them to drown each other out.
*/
private static final boolean WAIT_FOR_ED = true;
/*
* WARNING:
*
* With PA enabled:
* To avoid excessive RF power to the front end, the BUGbees should be placed
* about 2 feet apart. In addition, to prevent data corruption, the units should be
* operated with obstructions in between them if in a lab/office environment,
* or about 5-10 meters away from each other if in line of sight.
*
* The reason why the 100 mW device was chosen, was to compensate for the path
* losses associated with the directionality of the antenna. As a result, in
* our architecture, the PA should always be enabled when transmitting data.
* Range tests indicated that with the PA enabled, as long as we use the
* on_board printed antenna, the device will give us a range 50 to 75 feet
* indoors with obstructions, and 700 to 1000 feet line of sight.
* Any power level settings between 0 and 20 dBm will not provide any
* advantage. As a result, the settings for the PA should be enabled-full
* power or disabled.
*/
private static final boolean PA_ENABLED = true;
/**
* current state we're in; used by {@link BUGBeeChatApplication#stateMachineProc()}
*/
private int state = STATE_INIT;
private final IBUGBeeControl control;
private final IBUGBeeLEDControl led;
private final IBUGBeePacket packet;
/**
* true if we are to be the coordinator, false if we are to just be an end device.
* (we could choose to be a router instead of an end device but, since we only care
* here about peer-to-peer, being an end-device is fine).
* We set this in our ctor based on {@link BUGBeeChatApplication#COORD_FILE}
*/
private final boolean runAsCoord;
private final DataReceiver dataReceiver;
/**
* A handle used to identify the send data request; a message id if you prefer.
* We increment this for every message we send.
*/
private byte handle = 0;
private static final int MAX_BIND_ATTEMPTS = 10;
private byte failedBindCount = 0;
NetworkHandler(final IBUGBeeControl control,
final IBUGBeeLEDControl led,
final IBUGBeePacket packet,
final boolean runAsCoord,
final DataReceiver dataReceiver) {
this.control = control;
this.led = led;
this.packet = packet;
this.runAsCoord = runAsCoord;
this.dataReceiver = dataReceiver;
}
public void networkSetup() throws IOException {
if (runAsCoord) {
log("RUN: Setting up as an Coordinator");
control.setLogicalTypeConfig(IBUGBeeControl.LOGICAL_TYPE_COORDINATOR);
control.setUserDescConfig("BUGBeeChat Parent");
} else {
log("RUN: Setting up as an end device");
control.setLogicalTypeConfig(IBUGBeeControl.LOGICAL_TYPE_END_DEVICE);
control.setUserDescConfig("BUGBeeChat Child");
}
log("RUN: Setting channel list");
control.setChannelListConfig(CHANNEL_LIST);
log("RUN: Setting PANID");
control.setPANIDConfig(PAN_ID);
log("RUN: Clearing previous network state");
control.setStartUpOptionConfig(IBUGBeeControl.START_OP_CLEAR_STATE);
}
public void networkTearDown() {
/*
* If we're able, undo our changes to the configuration and reset.
*/
try {
led.LEDRedOff();
led.LEDGreenOff();
control.setStartUpOptionConfig((byte)
(IBUGBeeControl.START_OP_CLEAR_STATE |
IBUGBeeControl.START_OP_CLEAR_CONFIG));
control.reset(IBUGBeeControl.HW_RESET);
control.setStartUpOptionConfig(IBUGBeeControl.START_OP_CURRENT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void poll() {
try {
controlMessageProc();
dataMessageProc();
stateMachineProc();
} catch (IOException e) {
log("State Machine Proc Exception:");
e.printStackTrace();
}
}
/**
* Our state machine that gets us through the stages of starting up the
* stack on the ZigBee device, establishing or joining a PAN and binding
* to our peer so that we can send data to them.
*/
private void stateMachineProc() throws IOException {
switch(state)
{
case STATE_INIT:
dataReceiver.updateLocal2RemoteStatus("resetting");
log("STATE_INIT: issuing a hw reset");
control.reset(IBUGBeeControl.HW_RESET);
dataReceiver.updateLocal2RemoteStatus("starting i/f");
log("STATE_START_INTERFACE: Bringing up the ZigBee network interface");
control.setInterfaceState(true);
if (PA_ENABLED) {
// WARNING: See note where PA_ENABLED is defined, but basically
// don't have 2 BUGbees near each other if you turn on the PA
// MAX power as (again, see note) other settings don't make
// much difference.
control.configurePA(true, (byte) 0);
}
state = STATE_START;
break;
case STATE_START:
led.LEDRedOn();
// need to register our application profile so that our peer
// will be able to bind to it, and so that the ZigBee device
// will pass along the data messages for us. We use the same
// cluster id / command id for inbound and outbound since
// we can both send and receive text messages.
log("STATE_START: Registering our application profile");
control.registerAppRequest(
DIGI_DATA_ENDPOINT_ID,
DIGI_PROFILE_ID,
DEVICE_ID,
DEVICE_VERSION,
new short[] {DIGI_TRANSPARENT_DATA_CLUSTER_ID},
new short[] {DIGI_TRANSPARENT_DATA_CLUSTER_ID});
dataReceiver.updateRemote2LocalStatus("registered app profile");
log("STATE_START: Requesting that the ZigBee stack start");
control.startRequest();
state = STATE_JOINING;
break;
case STATE_JOINING:
final int deviceState = control.getState();
switch (deviceState) {
case IBUGBeeControl.DEV_ZB_COORD:
log("COORD is up");
log("Permitting joining");
control.permitJoining(IBUGBeeControl.ALL_ROUTERS_AND_COORD_ADDRESS,
IBUGBeeControl.JOINING_ON_INDEFINITELY_TIMEOUT);
dataReceiver.updateRemote2LocalStatus("COORD: permitted joining");
// arrange so ED will be able to bind to us
allowBinding();
if (WAIT_FOR_ED) {
// we know our peer can't be around yet so we just need to wait
state = STATE_WAIT_FOR_PEER_TO_BIND_TO_US;
} else {
// let's move on
state = STATE_BIND_REQUEST;
}
break;
case IBUGBeeControl.DEV_END_DEVICE:
log("ED is up");
log("Our short address is 0x" + Integer.toHexString(control.getShortAddress() & 0xFFFF));
// arrange so COORD will be able to bind to us
allowBinding();
state = STATE_BIND_REQUEST;
break;
default:
// probably not up on network yet
log("Device state: " + deviceState);
break;
}
break;
case STATE_WAIT_FOR_PEER_TO_BIND_TO_US:
dataReceiver.updateLocal2RemoteStatus("waiting for peer to bind to us");
log("STATE_WAIT_FOR_PEER_TO_BIND_TO_US");
break;
case STATE_BIND_REQUEST:
dataReceiver.updateLocal2RemoteStatus("attempting bind");
log("STATE_BIND_REQUEST");
control.bindDeviceRequest(
IBUGBeeControl.CREATE_BINDING_REQUEST,
DIGI_TRANSPARENT_DATA_CLUSTER_ID,
// all-zeros address for destination means binding will
// be established with another device that is in
// Allow Bind mode (if there is one of course).
// Replace this with the IEEE address of a peer
// device if you want to bind to a specific one
new IEEEAddress(new byte[] {
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00
/*
// address(es) of my BUGbees, possibly backwards
(byte) 0x46,
(byte) 0x81,
(byte) 0x7E,
(byte) 0x96,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) (RUN_AS_COORD ? 0x35 : 0x57)
*/
/*
// example IEEE address for one of my XBee ZB devices
(byte) 0x00,
(byte) 0x13,
(byte) 0xA2,
(byte) 0x00,
(byte) 0x40,
(byte) 0x2D,
(byte) 0x34,
(byte) 0x29
*/
}));
state = STATE_WAIT_FOR_BINDING_RESPONSE;
break;
case STATE_WAIT_FOR_BINDING_RESPONSE:
dataReceiver.updateLocal2RemoteStatus("waiting for binding response");
log("STATE_WAIT_FOR_BINDING_RESPONSE");
break;
case STATE_CAN_SEND_DATA:
//System.out.println("STATE_CAN_SEND_DATA");
break;
default:
break;
}
}
private void allowBinding() throws IOException {
// need to permit binding so our peer can bind to us to be
// able to send us messages
log("STATE_START: Permitting binding");
// timeout value is wrong (65) - should be 0xFF; this is
// an error in the CC2480 and will be corrected in IBUGBeeControl
control.allowBind((byte)0xFF /*IBUGBeeControl.BINDING_ALLOWED_INDEFINITELY_TIMEOUT*/);
dataReceiver.updateRemote2LocalStatus("permitted binding");
}
/**
* poll for a data packet coming in on the packet socket and process
* it if there's one there.
* Packet format:
* 0,1 (2 bytes) source device short address
* 2,3 (2 bytes) command id
* 4,5 (2 bytes) data length (for our expected packet type anyway)
* 6,... (x bytes) character data to display
* @throws IOException
*/
private void dataMessageProc() throws IOException
{
final byte [] buf = new byte[MAX_MESSAGE_LENGTH];
//System.out.println("DATA: waiting for message");
final int len = packet.read(buf, buf.length, IBUGBeeControl.MSG_DONTWAIT);
//System.out.println("CONTROL: received " + len + " byte message:");
for (int i = 0; i < len; ++i) {
log("datamsg[" + i + "] = 0x" + Integer.toHexString(buf[i] & 0xFF));
}
if (len <= 0) {
//System.out.println("DATA: no message");
return;
}
// use red led to indicate that we're processing a received data packet
led.LEDRedOn();
final short fromShortAddress = (short) (((buf[1] & 0xFF) << 8) | (buf[0] & 0xFF) & 0xFFFF);
log("DATA: Message received from device with short address " + fromShortAddress);
final short commandId = (short) (((buf[3] & 0xFF) << 8) | (buf[2] & 0xFF) & 0xFFFF);
log("DATA: Message received from device with command " + commandId);
if (commandId != DIGI_TRANSPARENT_DATA_CLUSTER_ID) {
log("DATA: Unexpected command id");
} else {
// it's what we wanted
final short dataLen = (short) (((buf[5] & 0xFF) << 8) | (buf[4] & 0xFF) & 0xFFFF);
final String msg = new String(buf, 6, dataLen);
log("DATA: Msg is '" + msg + "'");
dataReceiver.receiveData(msg);
dataReceiver.updateRemote2LocalStatus("rcvd data");
}
led.LEDRedOff();
}
public void sendDataMessage(final String msg) throws IOException {
final StringBuffer sb = new StringBuffer(msg);
// do it in chunks if we need to
while (sb.length() > NetworkHandler.MAX_DATA_SIZE) {
final String msgChunk = sb.substring(0, NetworkHandler.MAX_DATA_SIZE);
sendDataChunk(msgChunk);
sb.delete(0, NetworkHandler.MAX_DATA_SIZE);
}
// send the leftovers
sendDataChunk(sb.toString());
}
/**
* Send a data chunk to our peer.
* @param msg the message to send. must be no longer than {@link #MAX_DATA_SIZE}.
* @throws IOException
*/
private void sendDataChunk(final String msg) throws IOException {
if (msg.length() > MAX_DATA_SIZE) {
throw new IOException("Data request too large at " + msg.length());
}
/*
* Build our send-data-request packet.
*/
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(new byte[] {
// 16-bit short address of the destination device
(byte) ( BINDING_ADDRESS & 0xFF),
(byte) ((BINDING_ADDRESS >> 8) & 0xFF),
// 16-bit CommandID
(byte) ( DIGI_TRANSPARENT_DATA_CLUSTER_ID & 0xFF),
(byte) ((DIGI_TRANSPARENT_DATA_CLUSTER_ID >> 8) & 0xFF),
// handle used to identify the send data request
handle++,
// whether we want an acknowledgment back from the destination
WANT_ACK,
// The max number of hops the packet can travel through before it is dropped.
RADIUS});
// the size of the Data buffer in bytes
out.write(msg.length());
// the data
out.write(msg.getBytes());
final byte [] buf = out.toByteArray();
// ship it!
log("Sending message to partner");
packet.write(buf, buf.length, 0);
}
/**
* wait for and handle control messages
* TODO: Parsing the data into message objects needs to
* move down into the API.
* TODO: This polling is for the birds. Make it block for the 1-byte
* length header then block for the commandid's 2 bytes plus the msg len.
* @throws IOException
*/
private void controlMessageProc() throws IOException {
final byte [] buf = new byte[MAX_MESSAGE_LENGTH];
byte status;
//System.out.println("CONTROL: waiting for message");
final int len = control.read(buf, buf.length, IBUGBeeControl.MSG_DONTWAIT);
//System.out.println("CONTROL: received " + len + " byte message:");
for (int i = 0; i < len; ++i) {
log("controlmsg[" + i + "] = 0x" + Integer.toHexString(buf[i] & 0xFF));
}
if (len < MIN_COMMAND_RESPONSE_LENGTH) {
//System.out.println("CONTROL: message is too short; skipping it.");
return;
}
final DataInputStream in = new DataInputStream(new ByteArrayInputStream(buf, 0, len));
final int dataLen = in.readByte();
log("CONTROL: message has " + dataLen + " data bytes");
final short commandId = byteSwap(in.readShort());
log("control message command id = 0x" + Integer.toHexString(commandId & 0xFFFF));
switch (commandId)
{
case IBUGBeeControl.ZB_ALLOW_BIND_CONFIRM:
/*
* This command is issued by the ZigBee device when it responds to a bind
* request from a remote device.
* Data: (2 bytes) short address of the source device.
*/
final short sourceShortAddress = byteSwap(in.readShort());
log("CONTROL: Rcvd Bind Request from 0x" + Integer.toHexString(sourceShortAddress & 0xFFFF));
dataReceiver.updateRemote2LocalStatus("rcvd bind request from 0x" + Integer.toHexString(sourceShortAddress & 0xFFFF));
// TODO: figure out if it's okay we're turning off binding
// leave binding on for now; TODO: figure out when we've lost contact then
// we could be smarter about keeping bind off the rest of the time
//disallowBinding();
if (runAsCoord) {
if (WAIT_FOR_ED) {
// time to try binding to our peer, since we just let it bind to us
state = STATE_BIND_REQUEST;
}
}
break;
case IBUGBeeControl.ZB_BIND_CONFIRM:
/*
* This command is issued by the ZigBee device to return the results
* from a ZB_BIND_DEVICE command.
* Data: (2 bytes) command ID of the binding being confirmed.
* (1 byte) status (IBUGBeeControl.ZSUCCESS etc)
*/
if (state != STATE_WAIT_FOR_BINDING_RESPONSE) {
log("CONTROL: didn't expect to be in this state " + state +
" - ignoring bind confirm");
}
else {
final short confirmedCommandId = byteSwap(in.readShort());
log("CONTROL: Rcvd Bind Confirm for command id 0x" + Integer.toHexString(confirmedCommandId & 0xFFFF));
status = in.readByte();
if (status != IBUGBeeControl.ZSUCCESS) {
log("CONTROL: Unable to bind to peer, status is 0x" + Integer.toHexString(status & 0xFF));
dataReceiver.updateLocal2RemoteStatus("bind failed - 0x" + Integer.toHexString(status & 0xFF));
if (++failedBindCount > MAX_BIND_ATTEMPTS) {
log("CONTROL: Exceeded max bind attempts, reinit");
failedBindCount = 0;
state = STATE_INIT;
} else {
// try again
state = STATE_JOINING;
}
led.LEDGreenOff();
}
else {
log("CONTROL: Our peer confirmed that we are bound to it.");
dataReceiver.updateLocal2RemoteStatus("bind accepted");
if (!runAsCoord) {
// let coord call us now
allowBinding();
}
state = STATE_CAN_SEND_DATA;
led.LEDGreenOn();
}
}
break;
case IBUGBeeControl.ZB_SEND_DATA_CONFIRM:
case IBUGBeeControl.AF_DATA_CNF:
/*
* This command is issued by the ZigBee device to return
* the results from a SEND_DATA_REQUEST command.
* Data: (1 byte) handle
* (1 byte) status
*/
if (commandId == IBUGBeeControl.ZB_SEND_DATA_CONFIRM) {
in.skipBytes(1); // TODO: confirm throwing this byte away is ok
}
status = in.readByte();
if (status != IBUGBeeControl.ZSUCCESS) {
log("CONTROL: Unable to send data to our peer, status was 0x" + Integer.toHexString(status & 0xFF));
dataReceiver.updateLocal2RemoteStatus("send failed (0x" + Integer.toHexString(status & 0xFF) + ") rebinding");
// drop and go back to trying to bind again
state = STATE_JOINING;
} else if (state != STATE_CAN_SEND_DATA) {
log("CONTROL: Got send-data response but we were in state " + state);
}
led.LEDGreenOff();
break;
case IBUGBeeControl.ZB_RESET_IND:
/*
* This command is generated by the ZigBee device automatically
* immediately after a reset.
* Data: (1 byte) reason (0x00==Power-up, 0x01==External, 0x02==Watch-dog)
* (1 byte) transport rev
* (1 byte) product id
* (1 byte) major rel
* (1 byte) minor rel
* (1 byte) hw rev
*/
status = in.readByte();
log("CONTROL: Reset reason 0x" + Integer.toHexString(status & 0xFF));
log("transport rev " + in.readByte());
log("product id " + in.readByte());
log("major rel " + in.readByte());
log("minor rel " + in.readByte());
log("hw rev " + in.readByte());
/*
* received internal reset.
* Start over.
*/
if (status == IBUGBeeControl.ZB_RESET_BY_WATCHDOG) {
// we didn't trigger this one (on purpose anyway) so start from scratch
state = STATE_INIT;
}
break;
case IBUGBeeControl.ZB_START_CONFIRM:
/*
* This command is issued by the ZigBee device to return the results
* from a start request command.
*/
log("CONTROL: Start Confirm received");
// IBUGBeeControl.startRequest() blocks for this to come back anyway,
// so nothing to do here (and we could have just ignored it)
// TODO: does this really make it up to app-level, or does the
// driver swallow it.
break;
case IBUGBeeControl.ZDO_END_DEVICE_ANNCE_IND:
/*
* This command is issued by the ZigBee device when it has
* received an “End device announce” packet from a remote device
* Data: (2 bytes) SrcAddr - source address of the message.
* (2 bytes) NwkAddr - device's short address
* (8 bytes) IEEEAddr - 64-bit IEEE address of source device
* (1 byte) Capabilities - MAC capabilities of the device (bitmask, see docs)
*/
final short srcAddr = byteSwap(in.readShort());
final short nwkAddr = byteSwap(in.readShort());
final byte [] ieBytes = new byte[IEEEAddress.LENGTH];
in.readFully(ieBytes);
final IEEEAddress ieeeAddr = new IEEEAddress(ieBytes);
final byte capabilities = in.readByte();
log("CONTROL: Got End Device Annce Ind:");
log("srcAddr: 0x" + Integer.toHexString(srcAddr & 0xFFFF));
log("nwkAddr: 0x" + Integer.toHexString(nwkAddr & 0xFFFF));
log("IEEE Addr: " + ieeeAddr);
log("capabilities: 0x" + Integer.toHexString(capabilities & 0xFF));
break;
default:
log("CONTROL: Ignoring unrecognized command id of 0x" +
Integer.toHexString(commandId & 0xFFFF));
break;
}
}
private void disallowBinding() throws IOException {
log("CONTROL: Disallowing future binds since we've got a peer now");
control.allowBind(IBUGBeeControl.BINDING_DISALLOWED_TIMEOUT);
}
private String lastMsg = "";
private synchronized void log(final String msg) {
if (!msg.equals(lastMsg)) {
System.out.println(msg);
lastMsg = msg;
}
}
/**
* convenience function to change the endianness of a short.
* @param in the short to byte swap
* @return the byte-swapped short
*/
private static short byteSwap(final short in) {
return (short) (((in & 0xFF) << 8) | ((in >> 8) & 0xFF) & 0xFFFF);
}
public boolean isConnected() {
return state == STATE_CAN_SEND_DATA;
}
}