Bluetooth - Programming Microsoft Windows CE .NET - 3rd Edition - Douglas Boling
Bluetooth - Programming Microsoft Windows CE .NET - 3rd Edition - Douglas Boling
Bluetooth is the name of a wireless interface standard that uses radio frequency (RF) as its
medium instead of infrared frequency, as is used with IrDA. Bluetooth is designed to be a
successor to IrDA, providing the file transfer capabilities of IrDA along with a number of other
capabilities centering on cableless connections.
Bluetooth is named for Harald Blåtand (Bluetooth), who was king of Denmark from 940 to 985.
Harald was the grandson of King Ethelred of England and the grandfather of King Canute,
famous for demonstrating the limits of kingly power by commanding the tide not to come in For
those wondering, the tide came in anyway. Harald’s claim to fame is the unification of Denmark
and Norway during his rule. One thousand ten years later, following an Ericsson-initiated
feasibility study of using a low-power radio frequency network to link peripherals, a special
interest group (SIG) was formed with Ericsson, IBM, Toshiba, Nokia, and Intel to organize and
form a standard under the codename Bluetooth. That catchy code name was soon chosen as the
actual name of the standard.
Although it has taken longer than expected for Bluetooth-enabled devices to reach the
mainstream, the number of devices supporting Bluetooth has grown. Following this trend, a
number of Pocket PC and other Windows CE devices now include support for Bluetooth. Windows
CE 4.0 .NET provides integrated support for the Bluetooth protocol, which is also supported by
the Pocket PC 2003. Some Pocket PC OEMs use third-party Bluetooth software on their devices
instead of the Windows CE stack. This Bluetooth discussion covers only the Windows CE
Bluetooth API. To program third-party Bluetooth stacks, developers should contact the device
manufacturers for information.
Bluetooth functionality is centered on profiles that define services provided to the user. Profiles
include Cordless Telephony, Intercom, Headset, Fax, Dial-Up Networking, LAN Access, Object
Push, Synchronization, and File Transfer. Not all profiles are supported by all devices. In fact,
most devices support only a very few profiles relevant to the device.
Windows CE provides the Dial-up Networking, LAN Access, Object Push and File Transfer profiles
out of the box, although OEMs are free to add support for other profiles in their products. The
Pocket PC 2003 provides support for Object Push and File Transfer profiles. OEMs add support
for additional profiles, such as a headset profile for wireless headsets.
The applications, such as Pocket Inbox and Pocket Outlook, that are bundled with the devices
support Bluetooth for file transfer, business card exchange, and synchronization. Working with
these applications is preferable to writing code to work directly with the Bluetooth API because of
the complexity of that API.
For those who are interested in working directly with the Bluetooth API, the task isn’t easy, clean,
or quick. Part of the problem is the flexibility of the Bluetooth standard and the complexity of the
discovery protocol that communicates which services are available from a device. Before we can
dive into this code, a bit of background is necessary.
Stack
A diagram of the Bluetooth stack is shown in Figure 14-3. The lower three layers—Baseband, Link Manager
Protocol, and the first Host Controller Interface (HCI) layer—are implemented in the Bluetooth hardware.
The layers above the hardware and below the application are provided by Windows CE, although it’s
possible for third parties to extend the Bluetooth stack by providing additional profiles above the HCI layer.
Applications interact with the Bluetooth stack through one of two interfaces. The preferred
method is for applications to use the Winsock API to access the Bluetooth stack. Just as with
IrDA, applications use standard Winsock functions to open sockets associated with the Bluetooth
stack. Control is accomplished through various WSAxxx functions. Data transfer is accomplished
through the standard socket send and recv functions.
Winsock support for Bluetooth depends on the Winsock stack installed on the device. If the
system has Winsock 2.0 installed, such as the Pocket PC 2003, Bluetooth functionality is accessed
directly through Winsock calls such as setsockopt. For systems with Winsock 1.1 installed, the
Bluetooth stack needs to be configured through a dedicated Bluetooth API. For example, to query
the current mode of an asynchronous connection, an application can use the dedicated function
BthGetCurrentMode or, if Winsock 2.0 is on the system, a call to getsockopt with the option name
SO_BTH_GET_MODE.
The other way applications can work with Bluetooth is through virtual serial ports. With this
method, applications load a Bluetooth-dedicated serial driver. Control of the stack is
accomplished through DeviceIoControl calls to the COM driver. Calling WriteFile and ReadFile to
write and read the COM port sends and receives data across the Bluetooth connection.
Discovery
Before devices can communicate across a Bluetooth connection, devices and the services those
devices provide must be discovered. The discovery process is quite complex because of the
flexible nature of the Bluetooth feature set. Devices and services on particular devices can be
queried in a general way—all printers, for example—or they can be specifically queried—for
example, whether a particular device supports a particular service, such as the Headset-Audio-
Gateway service.
Both device discovery and service discovery are accomplished through the same series of
functions, albeit with significantly different parameters. The discovery process is accomplished
through a series of three functions: WSALookupServiceBegin, WSALookupServiceNext, and
WSALookupServiceEnd. These functions aren’t specific to Winsock 2.0, but in the discussion that
follows, I’m providing information only about using them in Bluetooth applications. A parallel
series of functions—BthNsLookupServiceBegin, BthNsLookupServiceNext, and
BthNsLookupServiceEnd—are functionally identical and can be used for systems with Winsock 1.1.
Although the function names imply a simple iterative search, the parameters required for the
search are daunting.
Device Discovery
The first parameter is a pointer to a WSAQUERYSET structure, which I’ll discuss shortly. For
T
device searches, the dwFlags parameter should contain the flag LUP_CONTAINERS. The other
allowable flags for this parameter will be covered in the upcoming discussion about service
queries. The final parameter should point to a handle value that will be filled in with a search
handle; this search handle will be used for the other calls in the search. The return value is an
HRESULT with 0, indicating success.
T
The BLOB structure pointed to by the lpBlob field is actually optional for the initial device query
call, but it’s recommended so that the time the Bluetooth stack spends looking for devices can be
defined. If the query time isn’t specified, the Bluetooth stack defaults to a rather long 15 to 20
seconds waiting for devices to respond. To define the query time, lpBlob points to a BLOB
structure that, in turn, points to a blob of a specific type. The generic BLOB structure is defined
as
The two fields are the size of the specific BLOB structure being pointed to a pointer to the
specific BLOB data. For device queries, the blob we’re interested in is an inquiry blob defined as
The first field should be set to BT_ADDR_GIAC, which is the general inquiry access code (GIAC),
defined as 0x9e8b33. The length field should be set to the time the stack should wait for devices
to respond. The unit of time for this field is a rather strange 1.28 seconds, so if you want to wait
approximately 5 seconds, the value 4 in the field will produce a wait of 4 1.28, or 5.12, seconds.
The final field, num_responses, specifies the maximum number of devices that need to respond
to end the query before the timeout value.
WSAQUERYSET structure’s lpBlob field pointing to the BLOB structure. The BLOB structure should
T
be initialized so that the cbSize field contains the size of the BTHNS_INQUIRYBLOB structure and
the pBlobData field points to the BTHNS_INQUIRYBLOB structure. The BTHNS_INQUIRYBLOB
structure should be filled in with the search criteria.
• LUP_RETURN_NAME
• LUP_RETURN_ADDRESS
• LUP_RETURN_BLOB
• BTHNS_LUP_RESET_ITERATOR
Reset the enumeration so that the next call to WSALookupServiceNext will return
information about the first device in the list.
• BTHNS_LUP_NO_ADVANCE
Return information about a device but don’t increment the device index so that the next
call to WSALookupServiceNext returns information about the same device.
The final two parameters are the address of a variable that contains the size of the output buffer
and a pointer to the output buffer. Although the output buffer pointer is cast as a pointer to a
WSAQUERYSET structure, the buffer passed to WSALookupServiceNext should be significantly
T
larger than the structure so that the function can marshal any strings into the buffer beyond the
end of the structure itself.
When the function returns without error, the WSAQUERYSET structure pointed to by pResults
T
contains information about a Bluetooth device. The name of the device, if requested with the
LUP_RETURN_NAME flag, is pointed to by the lpszServiceInstanceName field. The address of the
remote device is contained in the CSADDR_INFO structure pointed to by lpcsaBuffer.
CSADDR_INFO provides information about the local and remote device addresses and is defined
as
bt = ((SOCKADDR_BTH *)
pQueryResult->lpcsaBuffer->RemoteAddr.lpSockaddr)->btAddr;
Each call to WSALookupServiceNext returns information about a single device. The function
should be called repeatedly until it returns SOCKET_ERROR. If GetLastError returns
WSA_E_NO_MORE, there was no error; there are simply no more devices to be found.
The following routine queries the Bluetooth devices that are in range and returns their names
and addresses in an array.
// Init query
WSAQUERYSET QuerySet;
memset (&QuerySet,0,sizeof (WSAQUERYSET));
QuerySet.dwSize = sizeof (WSAQUERYSET);
QuerySet.dwNameSpace = NS_BTH;
QuerySet.lpBlob = &blob;
// Clean up
WSALookupServiceEnd (hLookup);
LocalFree (pOut);
return rc;
}
Service Discovery
Once the device of interest is found, the next task is to discover whether that device supplies the
service needed. Services are identified in a multilevel fashion. The service can publish itself under
a generic service such as printer or fax service or publish itself under a specific unique identifier,
or GUID.
If you know the specific service as well as its documented GUID, there is no need for service
discovery. Simply connect a Bluetooth socket to the specific service as discussed in the
“Bluetooth” section on page 668. If, however, you don’t know the exact service GUID, you must
take on the task of service discovery.
When you’re querying the services of another device, the WSAQUERYSET structure needs to
T
specify the target device that’s being queried. This is accomplished by referencing a restriction
blob in the WSAQUERYSET structure. The restriction blob is defined as
T
typedef struct _BTHNS_RESTRICTIONBLOB {
ULONG type;
ULONG serviceHandle;
SdpQueryUuid uuids[12];
ULONG numRange;
SdpAttributeRange pRange[1];
} BTHNS_RESTRICTIONBLOB;
The type field specifies whether the query should check for services, attributes of the services, or
both attributes and services by specifying the flags SDP_SERVICE_SEARCH_REQUEST, T
The SdpQueryUuid structure allows the service IDs to be specified as 16-, 32-, or 128-bit ID
values. The ID values for documented services are provided in the Bluetooth include file Bt_sdp.h
in the SDK.
When you’re querying attributes for a service or services, the pRange array can specify the
minimum and maximum attribute range to query. The size of the pRange array is specified in the
numRange parameter. In the following code, a specific service is queried to see whether it exists
on the device, and if it does, the query also returns the attributes associated with the service.
pQuerySet = (LPWSAQUERYSET)pQuery;
memset (pQuerySet, 0, MYBUFFSIZE);
pQuerySet->dwSize = sizeof (WSAQUERYSET);
pQuerySet->dwNameSpace = NS_BTH;
// Specify device
CSADDR_INFO csi;
memset (&csi, 0, sizeof (csi));
SOCKADDR_BTH sa;
memset (&sa, 0, sizeof (sa));
sa.btAddr = bta;
sa.addressFamily = AF_BT;
// Clean up records
for (i = 0; i < cRecordArg; i++)
pRecordArg[i]->Release();
CoTaskMemFree(pRecordArg);
}
dwLen = MYBUFFSIZE;
i++;
}
rc = WSALookupServiceEnd (hLookup);
LocalFree (pQuery);
return rc;
}
Notice that in this code, the Service Discovery Protocol (SDP) data for the service is returned in
the buffer pointed to by the lpBlob structure. This data isn’t parsed in the routine. Instead, a
routine named ParseBlobToRecs is called to parse the data. The routine ParseBlobToRecs, shown
here, returns a series of ISdpRecord interface pointers, one for each record in the SDP data.
//
// ParseBlobToRecs - Use ISdpStream object to parse the response from the
// SDP server.
//
HRESULT ParseBlobToRecs (UCHAR *pbData, DWORD cbStream,
ISdpRecord ***pppSdpRecords, ULONG *pcbRec) {
HRESULT hr;
ULONG ulError;
ISdpStream *pIStream = NULL;
*pppSdpRecords = NULL;
*pcbRec = 0;
if (SUCCEEDED(hr)) {
hr = pIStream->VerifySequenceOf (pbData, cbStream,
SDP_TYPE_SEQUENCE, NULL, pcbRec);
if (SUCCEEDED(hr) && *pcbRec > 0) {
*pppSdpRecords = (ISdpRecord **)CoTaskMemAlloc (
sizeof (ISdpRecord*) *
(*pcbRec));
if (pppSdpRecords != NULL) {
hr = pIStream->RetrieveRecords (pbData, cbStream,
*pppSdpRecords, pcbRec);
if (!SUCCEEDED(hr)) {
CoTaskMemFree (*pppSdpRecords);
*pppSdpRecords = NULL;
*pcbRec = 0;
}
}
else
hr = E_OUTOFMEMORY;
}
}
if (pIStream != NULL) {
pIStream->Release();
pIStream = NULL;
}
return hr;
}
The routine returns the data in an array of ISdpRecord pointers. It’s left to the reader to parse
the record data using the other interfaces provided in the Bluetooth API.
Publishing a Service
The other side of service discovery is service publication. Bluetooth applications that want to
provide a service to other applications must do more than simply create a Bluetooth socket, bind
the socket, and call accept as would an IrDA service. In addition to the socket work, the service
must publish the details of the service through the SDP API.
The actual publication of a service is actually quite simple. All that’s necessary is to call
WSASetService, which is prototyped as
If only registration were that simple. The problem isn’t calling the function; it’s composing the
SDP data that’s placed in the WSAQUERYSET structure. The dwNameSpace field should be set to
NS_BTH. And, as with the discovery process, the blobs are involved. The blob used in setting the
service is a BTHNS_SETBLOB structure defined as
The first parameter points to a ULONG that will receive a handle for the SDP record being created.
The fSecurity and fOptions fields are reserved and should be set to 0. The ulRecordLength
parameter should be set to the length of the SDP record to publish, whereas pRecord is the
starting byte of the byte array that is the SDP record to publish.
The following code demonstrates publishing an SDP record. The routine is passed an SDP record
and its size. It then initializes the proper structures and calls WSASetService to publish the record.
int PublishRecord (HWND hWnd, PBYTE pSDPRec, int nRecSize, ULONG *pRecord) {
BTHNS_SETBLOB *pSetBlob;
ULONG ulSdpVersion = BTH_SDP_VERSION;
int rc;
// Zero out the record handle that will be returned by the call
*pRecord = 0;
pSetBlob->pRecordHandle = pRecord;
pSetBlob->pSdpVersion = &ulSdpVersion;
pSetBlob->fSecurity = 0;
pSetBlob->fOptions = 0;
pSetBlob->ulRecordLength = nRecSize;
memcpy (pSetBlob->pRecord, pSDPRec, nRecSize);
When the application no longer wants to support the service, it needs to remove the record from
the SDP database. Removing the record is accomplished by using WSASetService, specifying the
record handle of the service and the flag RNRSERVICE_DELETE. The record handle is passed in
the BTHNS_SETBLOB structure. The other fields of this structure are ignored. The following code
shows a routine that unregisters a service.
BTHNS_SETBLOB SetBlob;
memset (&SetBlob, 0, sizeof (SetBlob));
SetBlob.pRecordHandle = &hRecord;
SetBlob.pSdpVersion = &ulSdpVersion;
SDP Records
The format of the SDP information that’s published is so complex that Windows CE provides a
special COM control to construct and deconstruct SDP records. Even with the control, parsing
SDP records isn’t easy. The first problem is knowing what’s required in the SDP record. The
information in the SDP record is defined by the Bluetooth specification, and a complete
explanation of this data far exceeds the space available for such an explanation.
The following code shows a routine that uses a canned SDP record with the GUID of the service
and the channel stuffed into the appropriate places in the record.
int RegisterService (HWND hWnd, GUID *pguid, byte bChannel, ULONG *pRecord) {
// SDP dummy record
// GUID goes at offset 8
// Channel goes in last byte of record.
static BYTE bSDPRecord[] = {
0x35, 0x27, 0x09, 0x00, 0x01, 0x35, 0x11, 0x1C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x09, 0x00, 0x04, 0x35, 0x0C, 0x35, 0x03, 0x19, 0x01,
0x00, 0x35, 0x05, 0x19, 0x00, 0x03, 0x08, 0x00};
The hard part of Bluetooth communication is the setup. Once a service is published, the
communication with remote devices is simple regardless of the method, Winsock or virtual COM
port, used by the application.
Server Side
A Bluetooth application providing a service first must set up a server routine that creates a socket
and performs all the necessary calls to support the server side of a socket communication. The
task starts with creating a socket with the standard socket call. The address format of the socket
should be set to AF_BT, indicating a socket bound to the Bluetooth transport.
Once created, the socket needs to be bound with a call to bind. The following code shows a
socket being created followed by a call to bind the socket. The address the socket is bound to is
left blank, indicating that the system will provide the proper settings. The address format for the
Bluetooth address used in the bind call is set to AF_BT.
// Bind to socket
rc = bind (s_sock, (struct sockaddr *)&btaddr, sizeof (btaddr));
if (rc) {
closesocket (s_sock);
return -2;
}
// Get information on the port assigned
len = sizeof (btaddr);
rc = getsockname (s_sock, (SOCKADDR *)&btaddr, &len);
if (rc) {
closesocket (s_sock);
return 0;
}
// Tell the world what we've bound to.
printf ("Addr %04x.%08x, port %d", GET_NAP(btaddr.btAddr),
GET_SAP(btaddr.btAddr), btaddr.port)
Once the call to bind succeeds, the code calls getsockname, which fills in the details of the
address of the device and, more important, the Bluetooth RFCOMM channel the socket was
bound to. This RFCOMM channel is important since it will need to be published with the SDP
record so that other devices will know which port to connect to when connecting to the service.
The macros in the printf statement in the preceding code demonstrate the division of the
Bluetooth device address into its two parts: the NAP, or nonsignificant address portion, and the
SAP, or significant address portion.
Once the RFCOMM channel is known, the SDP record can be constructed and published as shown
earlier in this section. The socket is then placed in listen mode, and a call to accept is made,
which blocks until a client application socket connects to the address. When the client does
connect, the accept call returns with the handle of a new socket that’s connected with the client.
This new socket is then used to communicate with the client device.
Client Side
On the client side, the task of connecting starts with device discovery. Once the Bluetooth
address of the client is determined, the client can create a thread that will communicate with the
server. The process mirrors any socket-based client with calls to create the socket, and the client
connects the socket to the remote server by specifying the address of the server. In the case of a
Bluetooth client, the address of the server must include either the RFCOMM channel or the GUID
of the service being connected to. In the following code, a client connects to a remote service
knowing the remote device’s Bluetooth address and the GUID of the client.
If using Winsock for communication isn’t to your liking, the Windows CE Bluetooth stack can also
be accessed by using a serial driver that can be loaded. This method has a number of
shortcomings, but some developers prefer it to using Winsock because of the familiarity of using
a simple serial port compared with the complexity of Winsock. In any case, before I show you
how to use the virtual serial port method, a few of the problems should be discussed.
The first problem is that the Bluetooth driver name is already the most used driver name in
Windows CE. The Windows CE stream driver architecture is such that the operating system is
limited to 10 instances of a given driver name, such as COM or WAV. Since typically 2 to 4
instances of serial drivers are already in a Windows CE system, the available number of virtual
COM ports is limited. Also, since the Bluetooth stack typically exposes some of its profiles through
COM ports, the 2 to 4 number quickly increases to 6 to 8 ports, leaving only 2 to 4 available COM
driver instances for Bluetooth applications that want to use virtual COM ports. An intrepid
programmer could register the Bluetooth driver under a different name, such as BTC for
Bluetooth COM, but this nonstandard name wouldn’t be expected if it were to be passed on to
other applications.
The second problem is that although the virtual COM port method is used on a number of
platforms, the implementation on Windows CE is unique. At least with the Winsock method, an
application can be written to be fairly source code compatible with Windows XP. That isn’t the
case with the virtual COM port method.
Finally, creating COM ports using this method is accomplished using the RegisterDevice function.
Although perfectly functional, this function has been deprecated for quite a while under newer
versions of Windows CE. Drivers loaded with RegisterDevice aren’t listed in the active device list
maintained in the registry by the system. RegisterDevice requires that the application provide the
index value for the driver being loaded. Because there’s no simple method for determining which
instance values are in use, the application must try all 10 instance values until one doesn’t fail
because it’s used by another COM driver. Still, in some circumstances—when legacy support is
needed, for example—using a virtual COM port is necessary.
Creating a virtual COM port is accomplished with the function RegisterDevice, which is prototyped
as
The first parameter is a three-character name of the driver, such as COM or WAV. The second
parameter is the instance value from 1 through 9, or 0 for instance 10. This value can’t already
be in use by another driver of the same name. The third parameter is the name of the DLL that
implements the driver. The final parameter is a DWORD that’s passed to the Init entry point of
the driver.
When used to load a Bluetooth virtual COM port, RegisterDevice is used as follows:
The first field is the RFCOMM channel to be used for this port. If the channel is to be assigned
automatically, the field can be set to RFCOMM_CHANNEL_MULTIPLE. The fLocal field should be
set to TRUE for the server application and FALSE for the client application. The device field is
used by client applications to specify the Bluetooth address of the remote server. This field must
be 0 for server applications.
The next three parameters allow the application to specify the maximum transaction unit (MTU).
The first field in this series, imtu, is the suggested value, while iminmtu is the minimum
acceptable MTU and imaxmtu is the maximum acceptable MTU. If all three of these fields are 0,
the driver uses default values for the MTU. The isendquota and irecvquota fields set the buffer
sizes for send and receive operations. Setting these fields to 0 indicates that the driver should
use the default values.
The uuidService field is used by the client application to specify the service being connected to on
the server. If the channel field is 0, this field must be set. If the uuidService is nonzero, the
Bluetooth stack will perform an SDP search to determine the proper channel for the service. The
actual SDP search will take place when the COM port is opened, not when it’s loaded with
RegisterDevice.
• RFCOMM_PORT_FLAGS_AUTHENTICATE
• RFCOMM_PORT_FLAGS_ENCRYPT
• RFCOMM_PORT_FLAGS_REMOTE_DCB
When this flag is specified, changing the DCB settings of the port results in a negation
with the peer device DCB settings.
• RFCOMM_PORT_FLAGS_KEEP_DCD
If this flag is set, the emulated DCD line will always be set.
Server Side
As when using Winsock to talk to the Bluetooth stack, using virtual COM ports requires that one
device be the server and the other the client. The server’s responsibility includes loading the
driver, opening the driver, determining the RFCOMM channel assigned to the port, and
advertising the port using the SDP process discussed earlier.
The following code fragment demonstrates a server registering a virtual COM port driver. Notice
that the routine makes multiple attempts at registering the driver, starting with instance value 9
and going down. Since the upper instance values are typically less used, this results in a quicker
registration process. Notice that as soon as the registration loop completes, the code saves the
instance value because that value forms the name of the driver. The driver name is then used to
open the driver with CreateFile. Once the driver is opened, the server uses one of the two special
I/O Control (IOCTL) commands available on a virtual COM port to query the RFCOMM channel.
The server then calls its RegisterService routine to advertise the service through an SDP record.
//
// Server process for opening a virtual COM port
//
int i, rc;
PORTEMUPortParams pp;
TCHAR szDrvName[6];
// Find free instance number and load Bluetooth virt serial driver
for (i = 9; i >= 0; i--) {
hDev = RegisterDevice (L"COM", i, L"btd.dll", (DWORD)&pp);
if (hDev)
break;
}
// See if driver registered
if (hDev == 0) return -1;
Client Side
The client side of the process is similar to the server side, with the exception that the client
needs to know the Bluetooth address of the server and the GUID of the service on the server.
Both of these parameters are specified in the PORTEMUPortParams structure when the device is
registered. The following code shows the COM port initialization process from the client
perspective.
//
// Client side
//
int i, rc;
PORTEMUPortParams pp;
TCHAR szDrvName[6];
// Find free instance number and load Bluetooth virt serial driver
for (i = 9; i >= 0; i--) {
hDev = RegisterDevice (L"COM", i, L"btd.dll", (DWORD)&pp);
if (hDev)
break;
}
// See if driver registered
if (hDev == 0) return -1;
Communication between the client and the server is accomplished through the standard Win32
file functions ReadFile and WriteFile. When the conversation has been concluded, the driver
should be closed with a call to CloseHandle and the driver unloaded with a call to
DeregisterDevice, prototyped here:
The BtHello example demonstrates a fairly complete Bluetooth application that can act as both a
client and a server. BtHello must be running on two Windows CE devices that use the Windows
CE Bluetooth stack for it to work. When started, BtHello searches for other Bluetooth devices in
the area and lists them in the output window. When the user taps the “Say Hello” button, BtHello
connects to the bthello service on the other device. Once connected, the client sends the server a
short string and then closes the connection. The server reads the text and displays it in its
window. Figure 14-4 shows the BtHello example after it has received the message from the other
device.
Figure 14-4. The BtHello example after it has received a message from another device
The source code for BtHello is shown in Listing 14-2. The application is a simple dialog-based
application. The source code is divided into two .cpp files and their associated include files:
BtHello.cpp, which contains the majority of the source code; and MyBtUtil.cpp, which contains
handy Bluetooth routines for finding devices and for registering service GUIDs with the SDP
service.
MyBtUtil.h
//======================================================================
// Header file
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//======================================================================
#ifndef _MYBTUTIL_H_
#define _MYBTUTIL_H_
// Registers a BT service
int RegisterBtService (GUID *pguid, byte bChannel,
ULONG *pRecord);
#include <windows.h>
#include <winsock2.h>
#include <ws2bth.h>
#include <bt_sdp.h>
#include <bthapi.h>
#include <bt_api.h>
#include "MyBtUtil.h"
// Init query
WSAQUERYSET QuerySet;
memset(&QuerySet,0,sizeof(WSAQUERYSET));
QuerySet.dwSize = sizeof(WSAQUERYSET);
QuerySet.dwNameSpace = NS_BTH;
QuerySet.lpBlob = &blob;
// Zero out the record handle that will be returned by the call
*pRecord = 0;
pSetBlob->pRecordHandle = pRecord;
pSetBlob->pSdpVersion = &ulSdpVersion;
pSetBlob->fSecurity = 0;
pSetBlob->fOptions = 0;
pSetBlob->ulRecordLength = nRecSize;
memcpy (pSetBlob->pRecord, pSDPRec, nRecSize);
// Clean up
LocalFree ((PBYTE)pSetBlob);
return rc;
}
//----------------------------------------------------------------------
// UnregisterBtService - Remove service from SDP database
//
int UnregisterBtService (HWND hWnd, ULONG hRecord) {
ULONG ulSdpVersion = BTH_SDP_VERSION;
int rc;
BTHNS_SETBLOB SetBlob;
memset (&SetBlob, 0, sizeof (SetBlob));
SetBlob.pRecordHandle = &hRecord;
SetBlob.pSdpVersion = &ulSdpVersion;
//----------------------------------------------------------------------
// Generic defines and data types
//
struct decodeUINT { // Structure associates
UINT Code; // messages
// with a function.
LRESULT (*Fxn)(HWND, UINT, WPARAM, LPARAM);
};
struct decodeCMD { // Structure associates
UINT Code; // menu IDs with a
LRESULT (*Fxn)(HWND, WORD, HWND, WORD); // function.
};
//----------------------------------------------------------------------
// Defines used by application
#define ID_ICON 1
// Window procedures
LRESULT CALLBACK MainWndProc (HWND, UINT, WPARAM, LPARAM);
// Message handlers
LRESULT DoCreateMain (HWND, UINT, WPARAM, LPARAM);
LRESULT DoSizeMain (HWND, UINT, WPARAM, LPARAM);
LRESULT DoCommandMain (HWND, UINT, WPARAM, LPARAM);
LRESULT DoPocketPCShell (HWND, UINT, WPARAM, LPARAM);
LRESULT DoDestroyMain (HWND, UINT, WPARAM, LPARAM);
LRESULT DoEnableSendMain (HWND, UINT, WPARAM, LPARAM);
LRESULT DoPrintfNotifyMain (HWND, UINT, WPARAM, LPARAM);
// Command functions
LPARAM DoMainCommandSend (HWND, WORD, HWND, WORD);
LPARAM DoMainCommandExit (HWND, WORD, HWND, WORD);
LPARAM DoMainCommandScan (HWND, WORD, HWND, WORD);
// Thread functions
DWORD WINAPI SearchThread (PVOID pArg);
DWORD WINAPI ServerThread (PVOID pArg);
DWORD WINAPI ReceiveThread (PVOID pArg);
DWORD WINAPI SayHelloThread (PVOID pArg);
BtHello.cpp
//======================================================================
// BtHello - A demonstration of a Bluetooth application
//
// Written for the book Programming Windows CE
// Copyright (C) 2003 Douglas Boling
//======================================================================
#include <windows.h> // For all that Windows stuff
#include <winsock2.h>
#include <ws2bth.h>
#include <Msgqueue.h>
#if defined(WIN32_PLATFORM_PSPC)
#include <aygshell.h> // Add Pocket PC includes
#pragma comment( lib, "aygshell" ) // Link Pocket PC lib for menubar
#endif
//----------------------------------------------------------------------
// Global data
//
const TCHAR szAppName[] = TEXT ("bthello");
// {26CECFEC-D255-4a5d-AF7C-9CCF840E7A42}
GUID guidbthello =
{ 0x26cecfec, 0xd255, 0x4a5d, { 0xaf, 0x7c, 0x9c, 0xcf,
0x84, 0xe, 0x7a, 0x42} };
#define MAX_DEVICES 16
MYBTDEVICE btd[MAX_DEVICES]; // List of BT devices
int nDevs = 0; // Count of BT devices
mqo.bReadAccess = FALSE;
hQWrite = CreateMsgQueue (TEXT ("MSGQUEUE\\ThTead"), &mqo);
// For Pocket PC, make dialog box full screen with PPC
// specific call.
shidi.dwMask = SHIDIM_FLAGS;
shidi.dwFlags = SHIDIF_DONEBUTTON │ SHIDIF_SIZEDLG │ SHIDIF_SIPDOWN;
shidi.hDlg = hWnd;
SHInitDialog(&shidi);
sai.cbSize = sizeof (sai);
SHHandleWMSettingChange(hWnd, wParam, lParam, &sai);
#endif
return 0;
}
//----------------------------------------------------------------------
// DoSizeMain - Process WM_SIZE message for window.
//
LRESULT DoSizeMain (HWND hWnd, UINT wMsg, WPARAM wParam,
LPARAM lParam) {
EnterCriticalSection (&csPrintf);
va_start(args, lpszFormat);
nBuf = _vstprintf(szBuffer, lpszFormat, args);
va_end(args);
EnterCriticalSection (&csPrintf);
va_start(args, lpszFormat);
nBuf = _vstprintf(szBuffer, lpszFormat, args);
va_end(args);
// List them.
for (i = 0; i < nDevs; i++)
Add2List (hWnd, TEXT("%d. dev:>%s< "), i, btd[i].szName);
// Bind to socket
rc = bind (s_sock, (struct sockaddr *)&btaddr, sizeof (btaddr));
if (rc) {
Add2List (hWnd, TEXT("bind failed"));
closesocket (s_sock);
return 0;
}
// Get information on the port assigned
len = sizeof (btaddr);
rc = getsockname (s_sock, (SOCKADDR *)&btaddr, &len);
if (rc) {
Add2List (hWnd, TEXT("getsockname failed"));
closesocket (s_sock);
return 0;
}
Add2List (hWnd, TEXT("Addr %04x.%08x, port %d"),
GET_NAP(btaddr.btAddr), GET_SAP(btaddr.btAddr), btaddr.port);
// Send ack
nBytes = send (r_sock, (char *)&rc, sizeof (rc), 0);
if (nBytes == SOCKET_ERROR) {
Add2List (hWnd, TEXT("Error %d receiving text length"), GetLastError())
;
closesocket (r_sock);
return 0;
}
if (rc == 0) {
// Send text name
if (nBytes != SOCKET_ERROR) {
nBytes = send (t_sock, (LPSTR)szText, nCnt, 0);
}
// Recv ack of text send.
if (recv (t_sock, (char *)&rc, sizeof (rc), 0) == SOCKET_ERROR)
rc = SOCKET_ERROR;
}
// Send close code.
if (rc != BAD_SOCKET)
send (t_sock, (LPSTR)&rc, sizeof (rc), 0);
closesocket (t_sock);
if (rc)
Add2List (hWnd, TEXT("SayHello Exit rc = %d"), rc);
else
Add2List (hWnd, TEXT("Text sent successfully"));
return 0;
}
The interesting routines are the search thread routine SearchThread and the server thread
routine ServerThread. The SearchThread calls the FindDevice routine to enumerate the Bluetooth
devices in the immediate area. The search is set to take approximately 5 seconds. Once found,
the device names and addresses are listed in the output window. The names and the addresses
of all the devices are saved in an array. The search can be restarted by tapping on the Scan
button.
The server routine, ServerThread, creates a socket and binds it to an address. The routine then
queries Winsock for the RFCOMM channel assigned to the socket. The RegisterBtService routine
is then called to advertise the bthello service. The RegisterBtService routine uses a prebuilt SDP
record and inserts the GUID for the service and the RFCOMM channel in the appropriate parts of
the record. Once constructed, the SDP packet is registered in the PublishRecord routine.
When the user taps the “Say Hello” button, an attempt is made to connect to the bthello server
on each of the devices found. If one of the connections is successful, the text is sent to the other
device.
Accessing Bluetooth through either Winsock or virtual COM ports provides the most flexible way
to wirelessly communicate with another device. The problem is that with either of these methods
the custom application, such as BtHello, has to be on both machines unless the application
communicates through one of the public services.
If you use one of the public services, the application must implement the proper protocol.
Although directly talking to Bluetooth is the most flexible path, it’s also the most complex. How
about a higher-level standard that will inform the application when devices come in range, that
will work over Bluetooth and IrDA, and that will provide a simple method for transferring files?
There is such a standard. It’s called the Object Exchange (OBEX) standard, and it too is
supported by the Pocket PC and other Windows CE devices.