Showing posts with label bluetooth. Show all posts
Showing posts with label bluetooth. Show all posts

Friday, June 03, 2011

Local data exchange with NFC and Bluetooth

One of the exciting technologies being shown off at Google's I/O conference this year was near field communication or NFC. It certainly got my interest, so I attended an excellent talk on NFC. Here's a video of the talk:

One of the things mentioned in the talk was that you did not want to use NFC for any kind of long running, verbose communication. Its range was too short and its transfer speed was too slow. Bluetooth was the way to go for such data exchange, so what you really wanted to do was an NFC-to-Bluetooth handoff. It was even mentioned that the popular Fruit Ninja game did this, or would do this in the future. Earlier this week at Bump we had our second hackathon. I decided that local communication using NFC and Bluetooth would make for an interesting hackathon project. So based on what I had learned from the I/O presentation, the examples in the Andoird SDK, and a tip from Roman Nurik, here's some code on how to do the NFC-to-Bluetooth handoff to setup a peer-to-peer connection between two phones to exchange data between them.
We'll start with the NFC pieces. You want the phone to do two things. First, it needs to broadcast an NFC "tag". This tag can have whatever information you want in it. In this case we will have it send all of the information needed to setup a Bluetooth connection: the Bluetooth MAC address for our phone plus a UUID for our app's connection. You can add more stuff to the tag as well, but these two parts are sufficient. Technically you could do without the UUID, but you'll want this in case other apps are using a similar technique. Here is some simplified code for generating an NFC text record:
public static NdefRecord newTextRecord(String msg) {
    byte[] langBytes = Locale.ENGLISH.getLanguage().getBytes(
            Charset.forName("US-ASCII"));
    byte[] textBytes = msg.getBytes(Charset.forName("UTF-8"));
    char status = (char) langBytes.length;
    byte[] data = new byte[1 + langBytes.length + textBytes.length];
    data[0] = (byte) status;
    System.arraycopy(langBytes, 0, data, 1, langBytes.length);
    System.arraycopy(textBytes, 0, data, 1 + langBytes.length,
            textBytes.length);
    return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT,
            new byte[0], data);
}
This code only handles English/ASCII characters. Take a look at the Android samples for a more generic approach. Next we need to get the Bluetooth MAC address to pass in to the above function. That is simply: BluetoothAdapter.getDefaultAdapter().getAddress(). Now we can create the text record to broadcast using NFC. To do this, you need to be inside an Android Activity:
@Override
public void onResume() {
    super.onResume();
    NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
    // code to generate a string called msg with the MAC address, UUID, etc.
    NdefMessage message = new NdefMessage(new NdefRecord[] { newTextRecord(msg) });
    adapter.enableForegroundNdefPush(this, message);
    // more code to come later
}
In this code there is a String called msg that I didn't show how it was generated. It would have the Bluetooth MAC address, as well as the UUID for your app, plus whatever else you want to include in the NFC broadcast. Now when your app loads, it will use NFC to broadcast the info needed for the Bluetooth handoff. The app needs to not only broadcast this, but also listen for this information as well:
@Override
public void onResume() {
    // see above code
    PendingIntent pi = PendingIntent.getActivity(this, 0, new Intent(this,
            getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
    IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
    try {
        ndef.addDataType("*/*");
    } catch (MalformedMimeTypeException e) {
        throw new RuntimeException("fail", e);
    }
    IntentFilter[] filters = new IntentFilter[] { ndef, };
    String[][] techLists = new String[][] { new String[] { NfcF.class.getName() } };
    adapter.enableForegroundDispatch(this, pendingIntent, filters, techLists);
}
This code configures an NFC listener using an IntentFilter and a type of NFC tag (there are many.) It uses a PendingIntent for this same Activity. So when a NFC tag that matches our criteria (based on the IntentFilter and tag type), then an Intent will be fired that will be routed to our Activity (because that's the Activity we put in the PendingIntent.) Now we just need to override the onNewIntent method of our Activity, since that is what will be invoked when an NFC tag is encountered:
@Override
public void onNewIntent(Intent intent) {
    NdefMessage[] messages = getNdefMessages(intent);
    for (NdefMessage message : messages) {
        for (NdefRecord record : message.getRecords()) {
            String msg = parse(record);
            startBluetooth(msg);
        }
    }
}
public static String parse(NdefRecord record) {
    try {
        byte[] payload = record.getPayload();
        int languageCodeLength = payload[0] & 0077;
        String text = new String(payload, languageCodeLength + 1,
                payload.length - languageCodeLength - 1, "UTF-8");
        return text;
    } catch (UnsupportedEncodingException e) {
        // should never happen unless we get a malformed tag.
        throw new IllegalArgumentException(e);
    }
}
For our example, there should only be one NdefMessage received, and it should have exactly one NdefRecord, the text record we created earlier. Once we get the message from the NFC tag, we it's time to start the Bluetooth connection. Bluetooth uses sockets and requires one device to act as a server while the other acts as a client. So if we have two devices setting up a peer-to-peer Bluetooth connection, which is one is the server and which is the client? There are a lot of ways to make this decision. What I did was have both phones include a timestamp as part of the NFC tag they broadcast. If a phone saw that it's timestamp was smaller than the other's, then it became the server. At this point you will want to spawn a thread to establish the connection. Here's the Thread I used:
public class ServerThread extends Thread {

    private final BluetoothAdapter adapter;
    private final String macAddress;
    private final UUID uuid;
    private final Handler handler;

    public ServerThread(BluetoothAdapter adapter, String macAddress, UUID uuid,
            Handler handler) {
        this.adapter = adapter;
        this.macAddress = macAddress;
        this.uuid = uuid;
        this.handler = handler;
    }

    @Override
    public void run() {
        try {
            BluetoothServerSocket server = adapter
                    .listenUsingInsecureRfcommWithServiceRecord(macAddress,
                            uuid);
            adapter.cancelDiscovery();
            BluetoothSocket socket = server.accept();
            server.close();
            CommThread comm = new CommThread(socket, handler);
            comm.start();
        } catch (IOException e) {}
    }
    
}
This Thread uses the device's BluetoothAdapter to open up an RFCOMM socket. Once you start listening, you'll want to immediately turn off Bluetooth discovery. This will allow the other device to connect much quicker. The server.accept() call will block until another devices connects (which is why this can't be in the UI thread.) Here's the client thread that will run on the other device:
public class ClientThread extends Thread {

    private final BluetoothAdapter adapter;
    private final String macAddress;
    private final UUID uuid;
    private final Handler handler;

    public ClientThread(BluetoothAdapter adapter, String macAddress, UUID uuid,
            Handler handler) {
        super();
        this.adapter = adapter;
        this.macAddress = macAddress;
        this.uuid = uuid;
        this.handler = handler;
    }

    @Override
    public void run() {
        BluetoothDevice remote = adapter.getRemoteDevice(macAddress);
        try {
            BluetoothSocket socket = remote
                    .createInsecureRfcommSocketToServiceRecord(uuid);
            adapter.cancelDiscovery();
            socket.connect();
            CommThread comm = new CommThread(socket, handler);
            comm.start();
        } catch (IOException e) {}
    }

}
On the client thread, you find the other device by using it's MAC address (not the client's.) Then you connect to it using the device using the shared UUID. On both client and server, we stated another thead for communication. From here on out this is just normal socket communication. You can write data on one end of the socket, and read it from the other.

Wednesday, July 14, 2010

A Tale of Two Mice

For a couple of years now, I have used a Logitech V470 mouse. It is a laser mouse that uses bluetooth. I've used it with a black MacBook and two MacBook Pros and it has always worked perfectly. The only negative I could ever give it is that it is a little small for my hand (but I have large hands, I wear x-large gloves.) However, years of heavy use have taken their toll on the mouse, and I decided it was time to replace it.

This picture shows the V470 in the front, and its replacement a Targus AMB09US which they call "Comfort Laser Mouse." Why did I pick the Targus? Well it is also a laser mouse that uses bluetooth. I read many a rave review, so I figured it was just as good of a mouse as my V470. It is a bigger mouse, as manufacturers have started realizing that just because I want a bluetooth mouse does not mean that I want a "travel" (small) mouse. However, after several weeks of use, here is what my desk looks like:


Yep, the old V470 is back, despite being chipped and have plastic peeling off of it. The Targus mouse is just not as good for several reasons. Its interaction with my MacBook Pro is buggy. Often the cursor on the screen does not change as it should (imagine web pages where you don't get the little hand to indicate a link). The sensitivity of its sensitivity is way too high. What I mean is that a slight adjustment in sensitivity has huge effects. It is either sluggish or spastic. I've tried adding a mousepad to the mix, but it only helped a little. Finally, it does weird things when interacting with OSX. I hide my Dock on the left of the screen. If I put the cursor over there, then the Dock appears as it should. However, if I roll over items in the Dock, they are not magnified as they should be. I can still click on them, but this messes with me and makes me think too much when I just want to launch something from the Dock. Also, I actually missed horizontal scrolling with the V470. Its wheel could move left/right to allow for horizontal scrolling. The Targus mouse does not have this feature, and I didn't think I'd miss it. However, I did not realize how often I actually used this, especially in Aperture and OmniGraffle.

Now I know that many/most of these problems may have as much to do with OSX as with the Targus mouse. But I don't care. I'm not going to switch operating systems. Instead I will switch mice. I'd really like to get a larger version of the V470, which is why I have not just bought a new V470 and continue to use my old beat-up one. At some point I will probably give in and just buy a new V470.