Talking to Devices

Bluetooth devices are talked to via the Generic Connection Framework. If you know the address of the device you e going to talk to, you may not even need to use any of the other classes in this chapter. Bluetooth URLs for the GCF look like this:

btspp://00904B2A88D6:1;authenticate=false;encrypt=false;master=false btspp://localhost:3B9FA89520078C303355AAA694238F07;authenticate=true;encrypt=true btspp://localhost:102030405060708090A1B1C1D1D1E100;name=SPPEx btl2cap://localhost:3B9FA89520078C303355AAA694238F08;name=Aserv btspp://localhost:3B9FA89520078C303355AAA694238F08 btgoep://0050C000321B:12 btgoep://localhost:3B9FA89520078C303355AAA694238F08

The URLs that begin with btspp are for devices that use the Bluetooth Serial Port Profile. These are streaming connections. URLs that begin with btl2cap are for devices that use the Bluetooth L2CAP protocol to exchange packetized data. Some higher-level protocols, such as RFCOMM, are built on top of L2CAP. Some devices use it more directly as well. URLs with the scheme btgoep are for devices that use the OBEX protocol to exchange binary data. For example, OBEX is used to synchronize contact lists between desktop computers and cell phones by exchanging binary representations of those lists.

The GCF can act as either a server or a client. The URLs that contain the word localhost are for servers. That is, they wait for incoming connections and respond to them. The URLs that don contain the word localhost are for clients. They initiate connections to the specified Bluetooth address. For a server, the long string of hex digits is the UUID of the service. For a client, its the address of the device you e talking to.

The address is sometimes followed by a colon and a channel number. This is analogous to a port in TCP protocols; that is, it is an extra number attached to each packet to help sort out which service on a given device a stream or packet is intended for. It has no particular meaning; devices that use only a single channel normally omit it.

Finally, up to five name=value optional parameters can configure the connection:

name

For server URLs only, the value for the service name attribute in the service record

master

true if this client must act as the master device; false if it can be a slave

encrypt

true if the connection is to be encrypted; false if it isn

authorize

true if the connection is to be authorized; false if it isn

authenticate

true if the connection is to be authenticated; false if it isn

Not all combinations are possible. For instance, you cannot have authenticate=false and encrypt=true.

As with USB devices or serial port devices, the details of communication are device dependent. Some devices share protocols. For example, one Bluetooth mouse is pretty much the same as another. You don need different drivers for each brand. A Bluetooth modem can more or less use the raw Bluetooth Serial Port Protocol along with the customary Hayes command set. For less standard devices, youll need to read the technical documentation (if any), communicate with the device vendors (if theyll talk to you), or reverse engineer the protocols the devices speak. A Bluetooth protocol analyzer that can sniff packets from the air is invaluable.

.7.1. RFCOMM Clients

RFCOMM devices are some of the simplest Bluetooth devices out there. Each has an output stream and an input stream. You write commands onto the output stream and read responses from the input stream. Some devices use a lockstep protocol (one command, one response). Others are asynchronous, and some don even require any commands.

Im going to demonstrate talking to the DeLorme Earthmate Blue Logger GPS receiver shown in Figure 25-4. Unlike some fancier and larger GPS units, it doesn have an LCD display. Its input is limited to a single button and its output to a couple of LEDs. This device just sends a constant stream of GPS data to whoevers interested in listening.

Figure 25-4. The DeLorme Earthmate Blue Logger

The Blue Logger formats data in the industry-standard NMEA 183 protocol supported by most GPS devices. This protocol outputs real-time position, velocity, and time information in line-by-line ASCII text that looks like this:

7.8524,W,1,07,1.1,27.2,M,-34.3,M,30.0,0000*46 $GPRMC,204449.378,A,4040.2990,N,07357.8524,W,0.00,184.22,300106,,*14 $GPVTG,184.22,T,,M,0.00,N,0.0,K*6D $GPGGA,204450.378,4040.2986,N,07357.8523,W,1,07,1.1,28.6,M,-34.3,M,30.0,0000*45 $GPGSA,A,3,10,06,05,07,04,30,02,,,,,,2.2,1.1,1.9*3A $GPGSV,3,1,09,10,67,234,44,02,64,054,43,07,34,154,37,04,33,083,31*78 $GPGSV,3,2,09,30,24,271,37,06,21,313,32,05,21,242,41,13,19,043,21*78 $GPGSV,3,3,09,29,13,175,00*4A $GPRMC,204450.378,A,4040.2986,N,07357.8523,W,0.00,184.22,300106,,*1C $GPVTG,184.22,T,,M,0.00,N,0.0,K*6D $GPGGA,204451.378,4040.2982,N,07357.8522,W,1,07,1.1,29.5,M,-34.3,M,30.0,0000*43 $GPRMC,204451.378,A,4040.2982,N,07357.8522,W,0.00,184.22,300106,,*18

The NMEA 0183 specification (http://www.nmea.org/pub/0183/) is published by the National Marine Electronics Association, which is stuck in the bad old days of pay-to-play specifications. You can buy the spec from them for $340 (and at that price, you don even get overnight shipping!). It is not available online. You can read more about NMEA in the NMEA FAQ at http://vancouver-webpages.com/peter/nmeafaq.txt.

In NMEA terminology, each line of text is called a sentence. The sentence begins with a dollar sign and ends with a carriage return linefeed pair. Sentences should contain no more than 82 characters (including the carriage return linefeed pair). Each sentence is self-contained and independent of the other sentences. Standard GPS sentences all begin with GP.

The first sentence in the above output looks suspect. It does not begin with a $ and an NMEA code. In fact, whats happened is that the program has hooked into the device in the middle of a sentence. NMEA devices normally send promiscuously and continuously, without considering whether anyone is listening. You should simply discard any line you receive that does not begin with a dollar sign. Similarly, if you want only some of the data, you just wait until it shows up in the output stream and ignore any sentences that aren relevant to you.

Vendor-specific sentences begin with the letter P and a three-letter manufacturer code. For instance, Garmin-specific sentences all begin with PGRM. The next three letters determine the type of the sentence. Ive seen the Blue Logger send four sentences:

GPGGA

Fix information. Essentially everything needed to determine a three-dimensional location and the accuracy thereof.

GPRMC

Recommended minimum data. This is basic time and position information.

GPVTG

Vector track and speed over ground. This includes the speed in both knots and kilometers per hour, as well as the direction of travel relative to true north and magnetic north.

GPGSA

General satellite data. This tells you which of the 28 GPS satellites the unit can currently see and how well it can see them.

A couple of dozen more sentences are emitted by various other GPS devices. For basic applications, the most interesting (and simplest) data is found in the GPRMC sentences. These give you the time, status, latitude, longitude, speed, angle, date, magnetic variation, and a checksum, in that order. Consider this GPRMC line:

$GPRMC,204449.378,A,4040.2990,N,07357.8524,W,0.00,184.22,300106,,*14

Within a sentence, commas separate the individual fields. The second field, 204449.378, is the time. Specifically, it is 20:44:49.378 seconds UTC; that is, 49.378 seconds after 8:44 PM, Greenwich Mean Time.

The third field, containing the letter A, is the status. This should be either A for Active or V for Void. Active units have found the GPS satellites. Void ones are not currently receiving GPS information, usually due to interference from buildings, canyons, and trees, and thus cannot be relied on.

4040.2990 is the latitude. Specifically, it is 40° 40.2990. Four-digit accuracy is not guaranteed, and it may not even be reported by some units. My tests suggest that a hundredth of a minute is about the best accuracy you can hope for, and that may vary depending on your location and satellite positions. The next field, the single letter N, says that this is North latitude. Similarly, the next two fields, 07357.8524,W, indicate that this is 73° 57.8524 West longitude.

The next field is the speed in knots. (Remember, this protocol was designed for boats, which still haven converted to sensible metric units.) In this case, the GPS reading was taken from a fixed location, so the speed is 0.00. The next field, with the value 184.22, is the angle of movement direction relative to true north. For a fixed location, this doesn mean a lot.

The next field, with the value 300106, is the date in the format DDMMYY. This date is January 30, 2006. Yes, theres a looming Y2K/Y2100 problem here. Most software assumes that 9099 map to 19901999 and 0089 map to 20002089. One hopes that this will be fixed sometime in the next 83 years.

The GPS satellites themselves don have a specific Y2K/Y2100 problem. Instead, they use atomic clocks accurate to within a microsecond. These clocks count time elapsed since midnight, January 6, 1980, GMT. They roll over every 1,024 weeks. The first such rollover happened on August 22, 1999. The next will happen on April 7, 2019. Its just the NMEA text format that is limited to two digits for the year.

The next field is empty in this example. If it were present it would include the magnetic variation, in the form 003.1,W.

The last field contains a checksum. This sum is formed by taking the bitwise exclusive or of all the bytes in the line between the $ and *, exclusive (that is, the $ and the * are not included when calculating the checksum).

Yes, Im skipping over a lot of technical detail here. People get PhDs in this stuff. Mapping is a lot more involved than your seventh-grade social studies teacher told you.

You can control some (not all) GPS receivers by writing similar sentences over the connections output stream. For example, this enables you to download the track logs, upload waypoints and routes, or turn off the device. However, this is all completely proprietary. Every device family has its own sentences for doing this, and some features, such as uploading maps, are completely undocumented and may even be actively hidden. Details vary from one device to the next. Sad to say, most GPS vendors have yet to catch the open source bug. For the time being, if you want to send data to a GPS unit, whether over Bluetooth, USB, or a classic serial port, you first have to reverse engineer the protocol it speaks. For many devices it may be the case that an open source Linux driver already exists in some other language, such as Python or C. Although a straight port may not be possible, this is often enough to show you what commands you need to send.

The NMEA protocol is actually designed for devices that have serial ports, but thats where the Bluetooth Serial Port Profile comes into play. You can pretend that the device is a serial port device (though you will have to use the Generic Connection Framework instead of the Java Communications API).

The first step is hardware: make sure the device is turned on, discoverable, and in range. Details vary from device to device, but to turn on the Blue Logger and make it discoverable you just hold down its one button until it starts flashing blue. You can use your systems usual Bluetooth control panel to make sure that the hosts Bluetooth controller is turned on and can see the device. However, don actually pair with the device. If you have previously paired with it, youll need to delete the pairing first so that it can be seen by Java.

The second step is to find the device. This is a little tricky. Youd normally search by major class, minor class, and service classes. However, theres no standard class for GPS devices. In such a case, the major class is set to 0x1FFF (i.e., five 1 bits in the major device class part of the class ID), and the minor class and service class bits are all 0s. Because this is a catch-all class ID for any unclassified device, theres no guarantee that the first one you find with that ID is actually a Blue Logger. Instead, well look for the friendly name "Earthmate Blue Logger." To be honest, this approach makes me a little nervous, but it seems to work. Example 25-6 demonstrates.

Example 25-6. Finding the first Blue Logger in range

import java.io.IOException; import javax.bluetooth.*; public class BlueLoggerFinder implements DiscoveryListener { private DiscoveryAgent agent; private RemoteDevice device; public static RemoteDevice find( ) throws BluetoothStateException { BlueLoggerFinder search = new BlueLoggerFinder( ); search.agent = LocalDevice.getLocalDevice().getDiscoveryAgent( ); search.agent.startInquiry(DiscoveryAgent.GIAC, search); // wait for inquiry to finish synchronized(search){ try { search.wait( ); } catch (InterruptedException ex) { // continue } } return search.device; } public void deviceDiscovered(RemoteDevice device, DeviceClass type) { int major = type.getMajorDeviceClass( ); try { if (device.getFriendlyName(false).startsWith("Earthmate Blue Logger")) { this.device = device; // stop looking for other devices agent.cancelInquiry(this); // wake up the main thread synchronized(this){ this.notify( ); } } } catch (IOException ex) { // hopefully this isn the device we e looking for } } public void inquiryCompleted(int discoveryType) {} // This search is only looking for devices and won discover any services, // but we have to implement these methods to fulfill the interface public void servicesDiscovered(int transactionID, ServiceRecord[] record) {} public void serviceSearchCompleted(int transactionID, int arg1) {} }

The BlueLogger.find( ) method returns a RemoteDevice object for the first operating Blue Logger it sees. If it can find one, it returns null. What you need from this object is the unique address of that particular Blue Logger as returned by the getBluetoothAddress( ) method. Once you have this address you can form the necessary URL to pass to the Generic Connection Framework. For example:

btspp://00904B2A88D6:1;authenticate=false;encrypt=false;master=false

Once you have the URL, you can talk to the device. This is actually quite simple. As shown in the last chapter, open a connection to the URL and get an InputStream from the connection:

StreamConnection conn = (StreamConnection) Connector.open(url); InputStream in = conn.openInputStream( );

This stream feeds you as much NMEA data as you want. Read this stream line by line. Look at the first six characters of each line. If they are $GPRMC, parse the line into individual components. Otherwise, ignore it and read the next line.

The easiest way to parse a comma-delimited line of this nature is to split the string along the commas. This is a little easier than parsing comma-delimited text normally is, because theres no possibility of a field containing the delimiter character or a line break.

Example 25-7 puts this together in a complete program that finds a Blue Logger and prints the time and location to System.out. Of course, this requires a desktop environment that has a console to write to. In a J2ME program, youd have to adjust the program to output the content using javax.microedition.lcdui, as described in Chapter 24.

Example 25-7. A Blue Logger client that monitors current position and time

import java.io.*; import javax.bluetooth.*; import javax.microedition.io.*; public class BluetoothTracker { public static void main(String[] args) throws IOException { RemoteDevice logger = BlueLoggerFinder.findBlueLogger( ); String address = logger.getBluetoothAddress( ); String url = "btspp://" + address + ":1;authenticate=false;encrypt=false;master=false"; StreamConnection conn = (StreamConnection) Connector.open(url); InputStream in = conn.openInputStream( ); BufferedReader reader = new BufferedReader( new InputStreamReader(in, "US-ASCII")); try { while (true) { String s = reader.readLine( ); if (s == null) break; if (s.startsWith("$GPRMC,")) { String[] fields = s.split(","); String time = getTime(fields[1]); String latitude = getPosition(fields[3], fields[4]); String longitude = getPosition(fields[5], fields[6]); String date = getDate(fields[9]); System.out.println(time + " " + date + " " + latitude + " " + longitude); } } } catch (IOException ex) { // device turned off or out of range } reader.close( ); } private static String getDate(String ddmmyy) { String year = "20" + ddmmyy.substring(4); String month = ddmmyy.substring(2, 4); String day = ddmmyy.substring(0, 2); return month + "-" + day + "-" + year; } // Im not sure how robust this code is. There could well be some // StringIndexOutOfBoundsExceptions waiting to trip up the unwary. // I have not tested it at every possible location on the planet. private static String getPosition(String number, String direction) { // need to handle two-digit and three-digit longitudes int point = number.indexOf(.); String degrees = number.substring(0, point-2); String minutes = number.substring(degrees.length( ), point); String seconds = String.valueOf( Double.parseDouble(number.substring(point)) * 60); return degrees + "°" + minutes + "" + seconds + """ + direction; } private static String getTime(String in) { String hours = in.substring(0, 2); String minutes = in.substring(2, 4); String seconds = in.substring(4, 6); return hours + ":" + minutes + ":" + seconds; } }

Heres some typical output:

20:42:05 01-30-2006 40°4017.832"N 073°5751.378"W 20:42:06 01-30-2006 40°4017.844"N 073°5751.312"W 20:42:07 01-30-2006 40°4017.855999999999998"N 073°5751.234"W 20:42:08 01-30-2006 40°4017.874"N 073°5751.162"W 20:42:09 01-30-2006 40°4017.898"N 073°5751.096000000000004"W 20:42:10 01-30-2006 40°4017.922"N 073°5751.036"W 20:42:11 01-30-2006 40°4017.945999999999998"N 073°5750.994"W 20:42:12 01-30-2006 40°4017.976"N 073°5750.958000000000006"W 20:42:13 01-30-2006 40°4018.006"N 073°5750.946"W 20:42:14 01-30-2006 40°4018.03"N 073°5750.952"W 20:42:15 01-30-2006 40°4018.048000000000002"N 073°5750.976"W 20:42:16 01-30-2006 40°4018.06"N 073°5751.03"W 20:42:17 01-30-2006 40°4018.06"N 073°5751.096000000000004"W 20:42:18 01-30-2006 40°4018.06"N 073°5751.150000000000006"W 20:42:19 01-30-2006 40°4018.066"N 073°5751.192"W 20:42:20 01-30-2006 40°4018.084"N 073°5751.21"W 20:42:21 01-30-2006 40°4018.108"N 073°5751.19799999999999"W 20:42:22 01-30-2006 40°4018.132"N 073°5751.168"W 20:42:23 01-30-2006 40°4018.162000000000003"N 073°5751.126"W 20:42:24 01-30-2006 40°4018.186"N 073°5751.09"W 20:42:25 01-30-2006 40°4018.21"N 073°5751.048"W

The difference from one reading to the next is attributable to jitter in the GPS. Its not accurate to more than a meter at best anyway. At this location, one second of latitude is roughly 30 meters, and a second of longitude is roughly 24 meters, so this works out to about 1.5-meter accuracy for latitude and about 10-meter accuracy for longitude. Thats acceptable error for many applications.

I cut this off early because I wasn really moving when I took these readings. If you were driving or running with a PDA, it would produce somewhat more variable output. (I don own a car, and running through the streets of Brooklyn carrying a laptop in one hand and a GPS receiver in the other did not strike me as a wise thing to try.) You could also easily set up a program to log the data once a minute or once every tenth of a mile. The device sends continuously, but you e free to ignore most of the readings.

.7.2. L2CAP Devices

L2CAP devices are a little more complex. The details of finding one, determining its URL, and opening a connection to it are essentially the same as they are for RFCOMM. However, L2CAP is based on packets rather than streams: instead of reading and writing streams, you send and receive packets. This is much like the difference between TCP and UDP on IP networks.

When given a btl2cap URL, Connector.open( ) returns an L2CAPConnection object. For example:

L2CAPConnection conn = (L2CAPConnection) Connector.open( "btl2cap:// 3B9FA89520078C303355AAA694238F08 ;ReceiveMTU=512;TransmitMTU=512");

This interface has methods to determine the Maximum Transmit Unit (MTU), tell whether the connection is ready to receive packets, and send and receive packets.

The MTU is normally set by the ReceiveMTU and transmitMTU parameters when you first open the connection. This is the maximum number of bytes you can put in each packet. Wireless connections are much less reliable and normally use smaller packet sizes than wired connections. You can check the MTU size with these two methods:

public int getTransmitMTU( ) throws IOException public int getReceiveMTU( ) throws IOException

Once you know the transmit MTU, you can send up to that amount of data at once using the send( ) method:

public void send(byte[] data) throws IOException

If you try to send more than the MTU, the extra data is discarded without warning.

To receive data coming in off the air, you pass a byte array to the receive( ) method:

public int receive(byte[] buffer) throws IOException

Data is placed in the array starting with the first component. This method blocks until data arrives off the air. To avoid that, you can first check with ready( ) before calling receive( ):

public boolean ready( ) throws IOException

ready( ) returns true if and only if receive( ) can read a packet without blocking.

Ill demonstrate this protocol with a dual example. First, Ill show a server that receives and prints out lines of ASCII text. Then Ill add a client that sends packets of ASCII text to the server. In this example, my code is controlling both ends of the connection on two different systems, so I can define any protocol I like on top of L2CAP. Ill keep it about as simple as imaginable, but this should still demonstrate the basic techniques for talking between two systems. Run this in both directions, and youd have a basic chat program.

The server listens on the local host, so first you need a service URL. You need to pick a UUID for this. I used the java.util.UUID class in Java 5 to generate a random one. The UUID it gave me was 7140b25b-7bd7-41d6-a3ad-0426002febcd. Youll also need a name. L2CAPExampleServer works as well as any. With these two pieces in place, the local URL for the service is:

btl2cap://localhost:7140b25b7bd741d6a3ad0426002febcd;name=L2CAPExampleServer

Use GCFs Connector class to open a connection to this URL. This returns an L2CAPConnectionNotifier object, but youll need to cast it to restore the type:

Connection conn = Connector.open( "btl2cap://localhost: 7140b25b7bd741d6a3ad0426002febcd;name=L2CAPExampleServer"); L2CAPConnectionNotifier notifier = (L2CAPConnectionNotifier) conn;

You now accept an incoming connection much like you would for a TCP server socket, except that for Bluetooth the method is called acceptAndOpen( ) instead of merely accept( ):

L2CAPConnection client = notifier.acceptAndOpen( );

You then receive packets from the connection and put them into a buffer. Size the buffer to match the clients maximum transmission size:

byte[] buffer = new byte[client.getTransmitMTU( )];

Most protocols define some packet that indicates the end of the transaction. Ill use a packet that contains a single null. Example 25-8 receives packets and copies them onto System.out until such a packet is seen.

Example 25-8. A very simple L2CAP server

import java.io.IOException; import javax.bluetooth.*; import javax.microedition.io.*; public class BluetoothReceiver { public final static String UUID = "7140b25b7bd741d6a3ad0426002febcd"; public static void main(String[] args) { try { LocalDevice device = LocalDevice.getLocalDevice( ); // make sure other devices can find us device.setDiscoverable(DiscoveryAgent.GIAC); String url = "btl2cap://localhost:" + UUID + ";name=L2CAPExampleServer"; L2CAPConnectionNotifier notifier = (L2CAPConnectionNotifier) Connector.open(url); L2CAPConnection client = notifier.acceptAndOpen( ); byte[] buffer = new byte[client.getTransmitMTU( )]; while (true) { int received = client.receive(buffer); if (received == 1 && buffer[0] == 0) { System.out.println("Exiting"); break; } System.out.write(buffer, 0, received); } } catch (BluetoothStateException ex) { System.err.println("Could not initialize Bluetooth." + " Please make sure Bluetooth is turned on."); } catch (IOException ex) { System.err.println("Could not start server"); } System.exit(0); } }

Obviously, this program handles only one connection at a time, but it would not be hard to extend it to handle multiple simultaneous connections by spawning a thread for each. Because the maximum number of Bluetooth devices in one piconet is eight, you don have to worry excessively about the sort of scaling issues that led to the new I/O API for network sockets.

Now lets look at the client. The first step is to discover a service with the specified UUID. Example 25-4 does this as long as we give it the necessary UUID. Of course, it would also be possible to have it return a list of all the URLs for each device with the requested service, but a single URL is all we need for the moment.

If you have trouble getting this program to work, make sure the server is discoverable. It may also help to verify that you can establish a Bluetooth connection between the client and the server for some other purpose, such as file transfer.

From there, we simply read each line read from the console into a byte array and send it over the air to the server. As is customary for console applications, if the user types a period on a line by itself, this is interpreted as the signal to stop sending and exit the program. The trickiest bit is making sure that the user can send a line longer than the transmit MTU. If thats attempted, we have to split the data into multiple packets. Example 25-9 demonstrates.

Example 25-9. A very simple L2CAP client

import java.io.*; import javax.bluetooth.*; import javax.microedition.io.*; public class BluetoothTransmitter { public static void main(String[] args) { try { String url = BluetoothServiceFinder.getConnectionURL(BluetoothReceiver.UUID); if (url == null) { System.out.println("No receiver in range"); return; } System.out.println("Connecting to " + url); L2CAPConnection conn = (L2CAPConnection) Connector.open(url); int mtu = conn.getTransmitMTU( ); // maximum packet length we can send // use safe??? BufferedReader reader = new BufferedReader( new InputStreamReader(System.in)); while (true) { String line = reader.readLine( ); if (".".equals(line)) { byte[] end = {0}; conn.send(end); break; } line += " "; // now we need to make sure this fits into the MTU byte[][] packets = segment(line, mtu); for (int i = 0; i < packets.length; i++) { conn.send(packets[i]); } } } catch (IOException ex) { ex.printStackTrace( ); } System.exit(0); } private static byte[][] segment(String line, int mtu) { int numPackets = (line.length( )-1)/mtu + 1; byte[][] packets = new byte[numPackets][mtu]; try { byte[] data = line.getBytes("UTF-8"); // the last packet will normally not fill a complete MTU for (int i = 0; i < numPackets-1; i++) { System.arraycopy(data, i*mtu, packets[i], 0, mtu ); } System.arraycopy(data, (numPackets-1)*mtu, packets[numPackets-1], 0, data.length - ((numPackets-1)*mtu) ); return packets; } catch (UnsupportedEncodingException ex) { throw new RuntimeException("Broken VM does not support UTF-8"); } } }

This combination of three classes allows one-way communication from the client to the server. Extending it to enable full bidirectional chat is not especially difficult, though, and is left as an exercise for the reader. (Im not sure how useful such a chat program would be, since Bluetooths reliable range is limited to about 10 meters, but I can think of a few uses for such short-range communications.)

Категории

© amp.flylib.com,