25.7. Talking to DevicesBluetooth devices are talked to via the Generic Connection Framework. If you know the address of the device you're 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:
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't 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, it's the address of the device you're 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:
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't 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, you'll need to read the technical documentation (if any), communicate with the device vendors (if they'll talk to you), or reverse engineer the protocols the devices speak. A Bluetooth protocol analyzer that can sniff packets from the air is invaluable. 25.7.1. RFCOMM ClientsRFCOMM 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't even require any commands. I'm 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't 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 whoever's interested in listening. Figure 25-4. The DeLorme Earthmate Blue LoggerThe 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:
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.
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. I've seen the Blue Logger send four sentences:
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:
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't 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't mean a lot. The next field, with the value 300106, is the date in the format DDMMYY. This date is January 30, 2006. Yes, there's 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 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).
You can control some (not all) GPS receivers by writing similar sentences over the connection's 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 that's 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 system's usual Bluetooth control panel to make sure that the host's Bluetooth controller is turned on and can see the device. However, don't actually pair with the device. If you have previously paired with it, you'll 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. You'd normally search by major class, minor class, and service classes. However, there's 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, there's no guarantee that the first one you find with that ID is actually a Blue Logger. Instead, we'll 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
The BlueLogger.find( ) method returns a RemoteDevice object for the first operating Blue Logger it sees. If it can't 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:
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:
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 there's 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, you'd 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
Here's some typical output:
The difference from one reading to the next is attributable to jitter in the GPS. It's 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. That's acceptable error for many applications. I cut this off early because I wasn't 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't 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're free to ignore most of the readings. 25.7.2. L2CAP DevicesL2CAP 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:
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:
Once you know the transmit MTU, you can send up to that amount of data at once using the send( ) method:
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:
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( ):
ready( ) returns true if and only if receive( ) can read a packet without blocking. I'll demonstrate this protocol with a dual example. First, I'll show a server that receives and prints out lines of ASCII text. Then I'll 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. I'll 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 you'd 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. You'll also need a name. L2CAPExampleServer works as well as any. With these two pieces in place, the local URL for the service is:
Use GCF's Connector class to open a connection to this URL. This returns an L2CAPConnectionNotifier object, but you'll need to cast it to restore the type:
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( ):
You then receive packets from the connection and put them into a buffer. Size the buffer to match the client's maximum transmission size:
Most protocols define some packet that indicates the end of the transaction. I'll 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
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't have to worry excessively about the sort of scaling issues that led to the new I/O API for network sockets. Now let's 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.
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't send a line longer than the transmit MTU. If that's attempted, we have to split the data into multiple packets. Example 25-9 demonstrates. Example 25-9. A very simple L2CAP client
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. (I'm not sure how useful such a chat program would be, since Bluetooth's reliable range is limited to about 10 meters, but I can think of a few uses for such short-range communications.) |
Monday, January 4, 2010
Section 25.7. Talking to Devices
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment