Playing with the Sun SPOT Radio

December 18, 2008

I have recently been lucky enough to borrow a set of very cool Sun SPOTs, so I immediately began thinking of all the amazingly clever and brilliant things I could do with them. However, with the exception of making the LEDs light up like a Cylon’s eyes (or Knightrider’s Kit as per your preference), all my ideas required one spot to be able to talk to another.

So what I figured the first thing to do would be to create a simple way to do this. Basically, each SPOT you want to talk to another instantiates a Buddy object and asks it to look for another buddy (myBuddy.lookForPair()) this will then start the buddy broadcasting and looking for a fellow.

It’s probably important to note at this point that I have not tested this code with more than two SPOTs, I certainly haven’t tested any edge cases where multiple SPOTs start up and all start looking for buddies on different ports. This is code is for my own use, I’m very happy for anyone to download, modify and use it (it is GPL’d) as long as you remember that it only works for the nicest of use cases.

Once two buddies (on different SPOTs) have connected you can send a message from one to the other by typing;

myBuddy.sendMessage("my message");

Recieved messages are handled by a BuddyListener which your SPOT application should have given to it’s Buddy instance. Recieved messages come in from the following method on that interface;

void messageRecieved(String message);

There’s lots of ways this code could be improved, for example the initial broadcast port is hardcoded into the Buddy code itself, this should really be parameterised. Also, the sleeps and waits that the Buddy endures while trying to connect to another one and so on. These should probably be pulled out to fields so that they can be tweaked more easily.

The good thing about this code is that it makes paired communication very simple (although you do have to be careful with the threads that are listening to incoming messages). You can also use your own language/protocol that the SPOTs will interpret on top of this Buddy mechanism very easily.

radio.Buddy – This is the meat of the project. To easily connect two SPOTs each must have an instance of one of these. Once connected to each other sending and recieving messages between the two is very easy.

/*
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package radio;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import javax.microedition.io.Connector;
import javax.microedition.io.Datagram;
import javax.microedition.io.DatagramConnection;

import com.sun.spot.io.j2me.radiogram.RadiogramConnection;
import com.sun.spot.io.j2me.radiostream.RadiostreamConnection;
import com.sun.spot.peripheral.radio.RadioFactory;
import com.sun.spot.util.IEEEAddress;
import com.sun.spot.util.Utils;
import com.sun.squawk.util.StringTokenizer;

public class Buddy {

	private static final String NEED_BUDDY = "BUDDY_REQ";
	private static final String I_WILL_BE_YOUR_BUDDY = "BUDDY_ACK";

	private long thisAddressAsNumber = RadioFactory.getRadioPolicyManager().getIEEEAddress();
	private String addyStr;

	private RadiostreamConnection listenOn;
	private RadiostreamConnection sendOn;

	private DataInputStream listenOnInputStream;
	private DataOutputStream sendOnOutputStream;

	private int portOut;
	private int portIn;

	private BuddyListener buddyListener = new NullBuddyListener();

	private int initialBroadCastPort;

	private boolean foundBuddy = false;

	private RadiogramConnection broadcastConnectionIn;
	private DatagramConnection broadcastConnectionOut;

	private int MAX_BROADCAST_SIZE;

	public Buddy(int broadcastPort) throws IOException {
		this.initialBroadCastPort = broadcastPort;
	}

	public void lookForPair() throws IOException {
		IEEEAddress addy = new IEEEAddress(RadioFactory.getRadioPolicyManager().getIEEEAddress());
		addyStr = addy.asDottedHex();

		System.out.println("I am " + addyStr);

		broadcastConnectionIn = (RadiogramConnection) Connector.open("radiogram://:" + initialBroadCastPort);
		MAX_BROADCAST_SIZE = broadcastConnectionIn.getMaximumLength();
		broadcastConnectionOut = (DatagramConnection) Connector.open("radiogram://broadcast:" + initialBroadCastPort);
		startListenForPartnerResponseThread();
		startLookForBuddyThread();
	}

	public void setBuddyListener(BuddyListener bl) {

		if(null != bl) {
			buddyListener = bl;
		} else {
			buddyListener = new NullBuddyListener(); //allows us to reset the currently loaded BuddyListener
		}

	}

	public void sendMessage(String message) {
		if (null != sendOn) {
			try {
				if (null == sendOnOutputStream) {
					sendOnOutputStream = sendOn.openDataOutputStream();
				}
				sendOnOutputStream.writeUTF(message);
			} catch (IOException ioe) {
				System.out.println("IOException sending message on partner stream");
				ioe.printStackTrace();
			}
		} else {
			throw new IllegalStateException(
					"Cannot send message.  No buddy found yet.");
		}
	}

	private void startLookForBuddyThread() {
		Thread t = new Thread(new Runnable() {
			public void run() {
				lookForBuddy();
			}
		});
		t.start();
	}

	private void lookForBuddy() {
		try {
			while (!foundBuddy) {
				Datagram dg = broadcastConnectionOut.newDatagram(broadcastConnectionOut.getMaximumLength());
				dg.writeUTF(Buddy.NEED_BUDDY + " " + initialBroadCastPort);
				broadcastConnectionOut.send(dg);

				Utils.sleep(1000);
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	private void startListenForPartnerResponseThread() {
		Thread t = new Thread(new Runnable() {
			public void run() {
				try {
					while (true) {
						Datagram dg = broadcastConnectionIn.newDatagram(MAX_BROADCAST_SIZE);
						broadcastConnectionIn.receive(dg); // blocking call

						String message = dg.readUTF();

						String otherAddress = dg.getAddress();
						long otherAddressAsNumber = IEEEAddress.toLong(otherAddress);

						if (thisAddressAsNumber != otherAddressAsNumber) {
							handleIncomingBroadcastMessage(dg, message,	otherAddress);
						}
						if (sendOn != null && listenOn != null) {

							System.out.println("Buddy status achieved, no longer going to listen to broadcasts");
							System.out.println("Partner port to send on: " + sendOn.getLocalPort());
							System.out.println("Partner port to listen on: " + listenOn.getLocalPort());

							startListenOnPartnerStreamThread();
							break;
						}
					}

				} catch (IOException ioe) {
					System.out.println("Exception when listening for buddy response");
					ioe.printStackTrace();
				}
			}
		});
		t.start();

	}

	private void handleIncomingBroadcastMessage(Datagram dg, String message, String otherAddress) throws IOException {
		if (message.startsWith(Buddy.NEED_BUDDY)) {
			if (null == sendOn) {
				int otherPort = Integer.valueOf(separateStrings(message)[1]).intValue();
				Datagram response = offerToBeBuddy(otherPort, otherAddress);

				response.setAddress(dg);
				broadcastConnectionIn.send(response);
			}
		} else if (message.startsWith(Buddy.I_WILL_BE_YOUR_BUDDY)) {
			buddyInviteAccepted(message, otherAddress);
		}
	}

	private void buddyInviteAccepted(String message, String otherAddress)	throws IOException {
		foundBuddy = true;

		portOut = Integer.valueOf(separateStrings(message)[1]).intValue();
		sendOn = (RadiostreamConnection) Connector.open("radiostream://" + otherAddress + ":" + portOut);

		portIn = portOut - 1;
		listenOn = (RadiostreamConnection) Connector.open("radiostream://" + otherAddress + ":" + portIn);
	}

	private Datagram offerToBeBuddy(int otherPort, String otherAddress) throws IOException {
		foundBuddy = true;

		portOut = otherPort;
		portIn = portOut + 1;

		System.out.println("Offering to be a buddy to " + otherAddress + "\nSending on " + portOut + "\nListening on " + portIn);

		sendOn = (RadiostreamConnection) Connector.open("radiostream://" + otherAddress + ":" + portOut);
		Datagram response = broadcastConnectionIn.newDatagram(MAX_BROADCAST_SIZE);

		response.writeUTF(Buddy.I_WILL_BE_YOUR_BUDDY + " " + portIn);
		listenOn = (RadiostreamConnection) Connector.open("radiostream://" + otherAddress + ":" + portIn);
		return response;
	}

	private void startListenOnPartnerStreamThread() {
		Thread t = new Thread(new Runnable() {
			public void run() {
				while (true) {
					listenForIncomingMessage();
				}
			}
		});
		t.start();
	}

	private void listenForIncomingMessage() {
		if (null != listenOn) {

			try {
				if (null == listenOnInputStream) {
					listenOnInputStream = listenOn.openDataInputStream();
				}

				String message = listenOnInputStream.readUTF();
				buddyListener.messageRecieved(message);

			} catch (IOException ioe) {
				System.out.println("IOException recieving message on partner stream");
				ioe.printStackTrace();
			}
		} else {
			// Maybe we haven't found a partner yet, so don't spin the processor
			Utils.sleep(100);
		}
	}

	private String[] separateStrings(String msg) {
		StringTokenizer stk = new StringTokenizer(msg, " ");
		String[] result = new String[stk.countTokens()];
		for (int i = 0; stk.hasMoreTokens(); i++) {
			result[i] = stk.nextToken();
		}
		return result;
	}
}

radio.BuddyListener – Classes wishing to have a buddy must have (or be) one of these and give it to the buddy instance

/*
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package radio;

public interface BuddyListener {
	void messageRecieved(String message);
}

radio.NullBuddyListener – It might be that two buddies are connected and one could be chatting to the other which doesn’t have a BuddyListener attached, in which case this is used instead.

/*
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package radio;

public class NullBuddyListener implements BuddyListener {

	public void messageRecieved(String message) {
		System.out.println("Warning!  NullBuddyListener being used and messages are getting recieved");
	}
}

radio.RadioFun – This is included for completeness. It’s the app that got run when I was testing the Buddy connections. I don’t think it makes a lot of sense for any one else to use this class except to see how to talk/listen to the buddy.

/*
 * 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 2 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, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package radio;

import java.io.IOException;

import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

import com.sun.spot.peripheral.radio.RadioFactory;
import com.sun.spot.util.IEEEAddress;
import com.sun.spot.util.Utils;

public class RadioFun extends MIDlet {

	private Buddy buddy;

	public RadioFun() {
	}

	void start() {
		try {
			buddy = new Buddy(60);
			buddy.setBuddyListener(new BuddyListener() {
				public void messageRecieved(String message) {
					System.out.println("recieved=["+message+"]");
				}
			});
			buddy.lookForPair();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}

		IEEEAddress addy = new IEEEAddress(RadioFactory.getRadioPolicyManager().getIEEEAddress());
		String addyStr = addy.asDottedHex();

		while (true) {
			try {
				buddy.sendMessage("Hello from "+addyStr);
			} catch (IllegalStateException ise) {
				System.out.println("Failed to send message ["+ise.getMessage()+"]");
			}

			Utils.sleep(500);
		}
	}

	protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
	}

	protected void pauseApp() {
	}

	protected void startApp() throws MIDletStateChangeException {
		RadioFun rf = new RadioFun();
		rf.start();
	}
}