Overview

This document explains how to write a simple program that reads the NMEA data produced by a Bluetooth GPS device using Borland C++ Mobile on a Symbian Series 60 cellphone.

This code was tested on a Nokia 7610 and a Nokia 3650 with a Techway Easy-go GPS. The version of Borland C++ Mobile was 1.5 and I tried to use the visual design tools as much as possible. This document assumes you are relatively familiar with Bluetooth devices, C++ and Borland C++ Mobile.

Discovery

The first thing you need to do is 'discover' the Bluetooth GPS unit. In the 'Non-Visual' area of the designer, drag a Bluetooth 'CBtDiscovery' component onto the design surface.

You do not need to set any properties but you will need to hook up the events. In the 'Events' tab, add a 'DeviceFound' event handler. This handler will get called when the discovery object has a bluetooth device selected by the user.

Inside this handler you can call the 'BDAddr()' function on the discovery object to get the address of the discovered bluetooth device.

To kick off the actual discovery process and display the user interface to the user you call the 'FindDevices()' method on the discovery object. This will often be done as part of a menu selection. In the example below the 'OnFindDevices' method is called when the 'Connect to GPS' menu item is selected by the user.

void Cbttest1Container::OnFindDevices( TInt aCommand )
{
  iBtDiscovery1->FindDevices();
}

Service Discovery

In the 'DeviceFound' handler you'll need to start a 'service discovery' process to find what services the found device is advertising. This process is used to find the GPS unit's serial port service, the port that service is advertised on, and this is used to eventually connect to it.

To do a service discovery, drag a 'CBtSdpQuery' component onto the non-visual design surface. This component has no properties to set but a number of event handlers to connect.

Although there are no properties in the designer that you set you will need to programatically set a few things to enable it to identify which services you are interested in.

This first thing to set is the 'search pattern'. This is an object that lists the services you are interested in. In C++, create the CSdpSearchPattern object and add to it the identifiers for the services you want to look for. Then call 'SetSearchPattern' on the SdpQuery object passing it the search pattern. The ID for the serial port service is 0x1101 and our search pattern is set with:

  CSdpSearchPattern * list = CSdpSearchPattern::NewL( );
  list->AddL( 0x1101 );
  iBtSdpQuery1->SetSearchPattern(list);

We want to find the port that the service is on so we can connect to it. To do this we will have to search through the attributes of the service looking for the port number. The attribute searching is done by setting a 'match list'. This is done in C++ via:

  CSdpAttrIdMatchList * matchList = CSdpAttrIdMatchList::NewL( );
  matchList->AddL( TAttrRange( KSdpAttrIdProtocolDescriptorList ) );
  iBtSdpQuery1->SetAttrIdMatchList(matchList);

Now the actual service discovery can be kicked off by calling the 'StartNextSdpL' method on the service discovery object passing it the address of the bluetooth device we are interested in:

  iBtSdpQuery1->StartNextSdpL(iBtDiscovery1->BDAddr());

Here is the entire method that kicks off the service discovery process:

void Cbttest1Container::OniBtDiscovery1DeviceFound( CBase * aDiscovery )
{
  _LIT(KDeviceFound, "Device Found");
  iEikLabel1->SetTextL(KDeviceFound);
  this->DrawDeferred();

  CSdpSearchPattern * list = CSdpSearchPattern::NewL( );
  list->AddL( 0x1101 );
  iBtSdpQuery1->SetSearchPattern(list);
  CSdpAttrIdMatchList * matchList = CSdpAttrIdMatchList::NewL( );
  matchList->AddL( TAttrRange( KSdpAttrIdProtocolDescriptorList ) );
  iBtSdpQuery1->SetAttrIdMatchList(matchList);
  iBtSdpQuery1->StartNextSdpL(iBtDiscovery1->BDAddr());
}

The result of this will be a number of events that will be raised. Handlers will need to be installed for these events:

NextRecordRequestComplete

The first handler that gets called is the 'NextRecordRequestComplete' handler.

void Cbttest1Container::OniBtSdpQuery1NextRecordRequestComplete( 
  CBase * aQuery, TInt aError, TSdpServRecordHandle aHandle,
  TInt aTotalRecordsCount, TBool & bAttributeRequest )
{
       _LIT(KDeviceError, "SDP Next Record complete");
       iEikLabel1->SetTextL(KDeviceError);
       this->DrawDeferred();
       bAttributeRequest = 1;
}

Inside this handler we need to tell the service discovery process that we want to request the value of the attributes. This is done by setting the 'bAttributeRequest' parameter in the handler to true:

  bAttributeRequest = 1;

AttributeRequestResult

The next handler called is 'AttributeRequestResult'.

void Cbttest1Container::OniBtSdpQuery1AttributeRequestResult( 
  CBase * aQuery, TSdpServRecordHandle aHandle,
  TSdpAttributeID aAttrID, CSdpAttrValue * aAttrValue, 
  TBool & bAcceptVisitor )
{
   if( aAttrID == KSdpAttrIdProtocolDescriptorList )
     bAcceptVisitor = 1;
}

This indicates the attributes have been requested and we have to tell the system that we want to visit each attribute value. This is done by setting the 'bAcceptVisitor' parameter in the handler to true. We only want to visit the values of the 'KSdpAttrIdProtocolDescriptorList' attribute in this instance.

  if( aAttrID == KSdpAttrIdProtocolDescriptorList )
          bAcceptVisitor = 1;

VisitAttributeValue

For each attribute value in that attribute the 'VisitAttributeValue' handler will now be called.

void Cbttest1Container::OniBtSdpQuery1VisitAttributeValue( 
  CBase * aQuery, 
  CSdpAttrValue & aValue, 
  TSdpElementType aType )
{
  if(aType == ETypeUUID) {
    iPreviousUUID = aValue.UUID();
  } else if(aType == ETypeUint) {
    if(iPreviousUUID == KRFCOMM) {
      iRemoteChannel = aValue.Uint();
    }
  }
}

Here we examine the type of the attribute and store its value for later use. The following code in this handler will extract the port value we are interested in:

  if(aType == ETypeUUID) {
      iPreviousUUID = aValue.UUID();
    } else if(aType == ETypeUint) {
      if(iPreviousUUID == KRFCOMM) {
        iRemoteChannel = aValue.Uint();
      }
    }
  }

In this example the port is stored in the 'iRemoteChannel' class variable.

AttributeRequestComplete

The final handler to be called is 'AttributeRequestComplete'.

void Cbttest1Container::OniBtSdpQuery1AttributeRequestComplete( 
  CBase * aQuery, 
  TSdpServRecordHandle aHandle, 
  TInt aError )
{
  _LIT(KDeviceError, "attribute request completed");
  iEikLabel1->SetTextL(KDeviceError);
  this->DrawDeferred();
  if(iRemoteChannel != 255) {
    iBtSocket1->OpenL(EFalse);
    iBtSocket1->SetPort(iRemoteChannel);
    iBtSocket1->SetAddr(iBtDiscovery1->BDAddr());
    iBtSocket1->ConnectL();
  }
  else {
    _LIT(KDeviceError, "Could not find remote port");
    iEikLabel1->SetTextL(KDeviceError);
    this->DrawDeferred();
  }
}

In this handler we have the port value and we can connect via a Bluetooth socket to the GPS unit. This method is explained in the next section.

Socket Connection

To start the Bluetooth socket connection drag a 'CBtSocket' component and a 'CBtParameters' component to the non-visual design surface.

In the 'CBtParameters' object set the 'protocol' property to 'KSolBtRFCOMM'.

In the 'CBtSocket' object, set the 'parameters' property to the 'CBtParameters' object you created above. No other properties need to be changed.

In C++, probably in the 'AttributeRequestComplete' handler from the service discovery process, you'll now set the other required values of the socket object:

 iBtSocket1->OpenL(EFalse);
 iBtSocket1->SetPort(iRemoteChannel);
 iBtSocket1->SetAddr(iBtDiscovery1->BDAddr());
 iBtSocket1->ConnectL();

We open the socket and set the port and device address to those we found previously. Then we connect to the device. The connection is complete when we get a 'Connect' event which we need to create a handler for.

Receiving Data

Once connected we can start to receive data.

void Cbttest1Container::OniBtSocket1Connect( CBase * aSocket )
{
  _LIT(KDeviceError, "Socket Connected");
  iEikLabel1->SetTextL(KDeviceError);
  this->DrawDeferred();

  iBtSocket1->RecvOneOrMore();
}

Inside the 'Connect' event we can call the 'RecvOneOrMore()' method on the socket object to receive data:

  iBtSocket1->RecvOneOrMore();

When the data is received from the device a 'RecvComplete' event is raised on the socket object.

void Cbttest1Container::OniBtSocket1RecvComplete( 
  CBase * aSocket, 
  TInt aErrorCode, 
  const TDesC8 & aMsg )
{
  _LIT(KDeviceError, "Data received");
  iEikLabel1->SetTextL(KDeviceError);
  iDataBuffer.Copy(aMsg);
  iEikLabel2->SetTextL(iDataBuffer);
  this->DrawDeferred();
}

Installing a handler for this allows us to process the data. The data itself is in the 'aMsg' parameter passed to the handler. In this example we set the value of a label component to the data. Since the data is in 8 bit format we copy it into the 16 bit format that the label requires. The 'iDataBuffer' is a TBuf16:

	
 iDataBuffer.Copy(aMsg);
 iEikLabel2->SetTextL(iDataBuffer);

Disconnecting

To gracefully disconnect from the device we can call the 'Disconnect' method of the socket object from a suitable menu handler.

void Cbttest1Container::OniMenuItem2ViewCommand( TInt aCommand )
{
  iBtSocket1->Disconnect();
}

Resources

Some of the places I got help or sample code for using bluetooth from Symbian C++ were:

Conclusion

There are quite a few event handlers to set and functions to write but overall the process is quite easy. Once you start receiving the NMEA string you can then process it as required by the application.

I initially found the Borland environment a bit difficult to get to grips with. Especially as there was limited documentation available for the various designer components. But once I used it a bit, and examined the source code for the components, I began to understand how things worked and working through this simple example will hopefully help you learn how to use the components and the design environment.

I welcome any questions, comments, or suggestions to improve this article. I can be reached via email at chris.double@double.co.nz.