The Bluetooth Framework's wclRfCommClient class enables classic Bluetooth RFCOMM communication without virtual COM ports. Discover services by UUID, resolve RFCOMM channel numbers, connect to a remote device, and send or receive data using a simple event-driven API.

RFCOMM protocol stack — profiles on top of RFCOMM on top of L2CAP and baseband

The main function of Bluetooth is a Bluetooth protocol stack. It defines and provides different types of layers and functionalities. Bluetooth can run different applications over different protocol stacks, but each one of these protocol stacks uses the same Bluetooth link and physical layers. The diagram shows a complete Bluetooth protocol stack. It shows the relationship between the protocols that use the services of other protocols when there is a payload to be transferred in the air. Anyhow, the protocols have many other relationships between them — for example, some protocols (L2CAP, TCS Binary) use the LMP to control the link manager. This article describes how to use the Bluetooth Framework with the classic Bluetooth RFCOMM protocol. Should you have any questions, please do not hesitate to contact us.


Bluetooth Framework and RFCOMM

The Bluetooth RFCOMM protocol is a simple set of transport protocols, made on top of the L2CAP protocol, providing emulated RS-232 serial ports. You usually know RFCOMM as Serial Port Profile (SPP). But that is not true at all. The Serial Port Profile is just one of many profiles that are based on the Bluetooth RFCOMM protocol. The Object Push Profile (OPP), File Transfer Profile (FTP), and many other Bluetooth profiles work above RFCOMM (use RFCOMM as a transport protocol).

The Bluetooth Framework natively supports the Bluetooth classic RFCOMM protocol in client and server modes. That means that the Bluetooth Framework can act as an RFCOMM client (connects to other Bluetooth-enabled devices) or as an RFCOMM server (accepts connections from other Bluetooth-enabled devices). That also means that you do not need to create or use any virtual COM ports to be able to communicate with Bluetooth-enabled devices.


Bluetooth profiles (services)

In order to use Bluetooth technology, a device must be compatible with the subset of Bluetooth profiles (often called services). A Bluetooth profile (service) is a specification regarding an aspect of Bluetooth-based wireless communication between devices.

The way a device uses Bluetooth technology depends on its profile capabilities. The profiles provide standards that manufacturers follow to allow devices to use Bluetooth in the intended manner. Each Bluetooth profile is built on top of the Bluetooth protocol. The Bluetooth Framework can work only with profiles that are built on top of the RFCOMM protocol. There are a few exceptions, but those exceptions are not a part of this article.

For a general overview of the Bluetooth Classic profiles that exist — including the ones that do not use RFCOMM, such as A2DP and HID — see the Bluetooth Classic profiles overview.

A Bluetooth profile is defined by something known as a Universally Unique ID (UUID). UUID is an abbreviation you will see a lot in the Bluetooth world. This service's UUID tells the other device which Bluetooth features the device supports. An RFCOMM connection is usually established using the service's UUID.

A Bluetooth-enabled device may query another Bluetooth-enabled device about supported services by sending a request to its Service Discovery Protocol (SDP) service. In the Bluetooth Framework you can do it by calling the EnumRemoteServices method.

procedure TfmMain.btEnumServicesClick(Sender: TObject);
var
  Item: TListItem;
  Radio: TwclBluetoothRadio;
  Address: Int64;
  Res: Integer;
  Services: TwclBluetoothServices;
  i: Integer;
begin
  if lvDevices.Selected = nil then
    MessageDlg('Select device', mtWarning, [mbOK], 0)
  else begin
    ClearServices;
    Item := lvDevices.Selected;
    Radio := TwclBluetoothRadio(Item.Data);
    Address := StrToInt64('$' + Item.SubItems[0]);
    Res := Radio.EnumRemoteServices(Address, nil, Services);
    if Res <> WCL_E_SUCCESS then
      ShowError(Res)
    else begin
      if Services <> nil then begin
        try
          for i := 0 to Length(Services) - 1 do begin
            Item := lvServices.Items.Add;
            Item.Caption := Radio.ApiName;
            Item.SubItems.Add(IntToHex(Address, 12));
            Item.SubItems.Add(IntToHex(Services[i].Handle, 8));
            Item.SubItems.Add(GUIDToString(Services[i].Uuid));
            Item.SubItems.Add(IntToStr(Services[i].Channel));
            Item.SubItems.Add(Services[i].Name);
            Item.SubItems.Add(Services[i].Comment);
          end;
        finally
          Services := nil;
        end;
      end;
    end;
  end;
end;
void __fastcall TfmMain::btEnumServicesClick(TObject *Sender)
{
  if (lvDevices->Selected == NULL)
    MessageDlg("Select device", mtWarning, TMsgDlgButtons() << mbOK, 0);
  else
  {
    ClearServices();
    TListItem* Item = lvDevices->Selected;
    TwclBluetoothRadio* Radio = (TwclBluetoothRadio*)Item->Data;
    __int64 Address = StrToInt64("$" + Item->SubItems->Strings[0]);
    TwclBluetoothServices Services;
    int Res = Radio->EnumRemoteServices(Address, NULL, Services);
    if (Res != WCL_E_SUCCESS)
      ShowError(Res);
    else
    {
      for (int i = 0; i < Services.Length; i++)
      {
        Item = lvServices->Items->Add();
        Item->Caption = Radio->ApiName;
        Item->SubItems->Add(IntToHex(Address, 12));
        Item->SubItems->Add(IntToHex((int)Services[i].Handle, 8));
        Item->SubItems->Add(Sysutils::GUIDToString(Services[i].Uuid));
        Item->SubItems->Add(IntToStr(Services[i].Channel));
        Item->SubItems->Add(Services[i].Name);
        Item->SubItems->Add(Services[i].Comment);
      }
    }
  }
}
private void btEnumServices_Click(object sender, EventArgs e)
{
  if (lvDevices.SelectedItems.Count == 0)
    MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  else
  {
    ClearServices();
    ListViewItem Item = lvDevices.SelectedItems[0];
    wclBluetoothRadio Radio = (wclBluetoothRadio)Item.Tag;
    Int64 Address = Convert.ToInt64(Item.SubItems[1].Text, 16);
    wclBluetoothService[] Services;
    Guid g = Guid.Empty;
    Int32 Res = Radio.EnumRemoteServices(Address, g, out Services);
    if (Res != wclErrors.WCL_E_SUCCESS)
      ShowError(Res);
    else
    {
      if (Services != null)
      {
        try
        {
          foreach (wclBluetoothService Service in Services)
          {
            Item = lvServices.Items.Add(Radio.ApiName);
            Item.SubItems.Add(Address.ToString("X12"));
            Item.SubItems.Add(Service.Handle.ToString("X8"));
            Item.SubItems.Add(Service.Uuid.ToString());
            Item.SubItems.Add(Service.Channel.ToString());
            Item.SubItems.Add(Service.Name);
            Item.SubItems.Add(Service.Comment);
          }
        }
        finally
        {
          Services = null;
        }
      }
    }
  }
}
Private Sub btEnumServices_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btEnumServices.Click
  If lvDevices.SelectedItems.Count = 0 Then
    MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
  Else
    ClearServices()
    Dim Item As ListViewItem = lvDevices.SelectedItems(0)
    Dim Radio As wclBluetoothRadio = CType(Item.Tag, wclBluetoothRadio)
    Dim Address As Int64 = Convert.ToInt64(Item.SubItems(1).Text, 16)
    Dim Services() As wclBluetoothService = Nothing
    Dim g As Guid = Guid.Empty
    Dim Res As Int32 = Radio.EnumRemoteServices(Address, g, Services)
    If Res <> wclErrors.WCL_E_SUCCESS Then
      ShowError(Res)
    Else
      If Services IsNot Nothing Then
        Try
          For Each Service As wclBluetoothService In Services
            Item = lvServices.Items.Add(Radio.ApiName)
            Item.SubItems.Add(Address.ToString("X12"))
            Item.SubItems.Add(Service.Handle.ToString("X8"))
            Item.SubItems.Add(Service.Uuid.ToString())
            Item.SubItems.Add(Service.Channel.ToString())
            Item.SubItems.Add(Service.Name)
            Item.SubItems.Add(Service.Comment)
          Next Service
        Finally
          Services = Nothing
        End Try
      End If
    End If
  End If
End Sub
void CBluetoothManagerDlg::OnBnClickedButtonEnumServices()
{
  POSITION Pos = lvDevices.GetFirstSelectedItemPosition();
  if (Pos == NULL)
    AfxMessageBox(_T("Select device"));
  else
  {
    ClearServices();
    int Item = lvDevices.GetNextSelectedItem(Pos);
    CwclBluetoothRadio* Radio = (CwclBluetoothRadio*)lvDevices.GetItemData(Item);
    __int64 Address = StrToInt64(lvDevices.GetItemText(Item, 1));
    wclBluetoothServices Services;
    int Res = Radio->EnumRemoteServices(Address, NULL, Services);
    if (Res != WCL_E_SUCCESS)
      ShowError(Res);
    else
    {
      if (Services.size() > 0)
      {
        try
        {
          int ndx = 0;
          for (wclBluetoothServices::iterator i = Services.begin(); i != Services.end(); i++)
          {
            LPOLESTR Guid;
            StringFromCLSID((*i).Uuid, &Guid);
            lvServices.InsertItem(ndx, Radio->GetApiName().c_str());
            lvServices.SetItemText(ndx, 1, IntToHex(Address));
            lvServices.SetItemText(ndx, 2, IntToHex((int)(*i).Handle));
            lvServices.SetItemText(ndx, 3, Guid);
            lvServices.SetItemText(ndx, 4, IntToStr((*i).Channel));
            lvServices.SetItemText(ndx, 5, (*i).Name.c_str());
            lvServices.SetItemText(ndx, 6, (*i).Comment.c_str());
            CoTaskMemFree(Guid);
            ndx++;
          }
        }
        finally(
          Services.clear();
        )
      }
    }
  }
}

The Bluetooth Framework uses the wclBluetoothService structure to describe Bluetooth service information (it is also called a Bluetooth Service record or SDP record) received from a remote Bluetooth-enabled device.

TwclBluetoothService = record
  Handle: Cardinal;
  Uuid: TGuid;
  Channel: Byte;
  Name: string;
  Comment: string;
end;
struct TwclBluetoothService
{
  unsigned long Handle;
  TGUID Uuid;
  unsigned char Channel;
  String Name;
  String Comment;
};
public struct wclBluetoothService
{
  public UInt32 Handle;
  public Guid Uuid;
  public Byte Channel;
  public String Name;
  public String Comment;
};
Public Structure wclBluetoothService
    Public Handle As UInt32
    Public Uuid As Guid
    Public Channel As Byte
    Public Name As String
    Public Comment As String
End Structure
typedef struct
{
  unsigned long Handle;
  GUID Uuid;
  unsigned char Channel;
  tstring Name;
  tstring Comment;
} wclBluetoothService;
  • Handle — the Bluetooth service's handle. This is an internal field and usually means nothing for an application.
  • Uuid — the service's UUID, what we are looking for.
  • Channel — the RFCOMM channel number (see below).
  • Name — the service's name.
  • Comment — the service's comment. This field may contain additional information about service implementation details.

RFCOMM channel number

As described above, a Bluetooth service UUID just describes some feature that is supported by a Bluetooth-enabled device. It is something like a TCP/IP service: HTTP, FTP, Telnet, or others. You can connect to any Internet service by using its IP address and service. However, Bluetooth devices may have a few services with identical UUIDs. For example, a device may have two Serial Port Profile (SPP) services: one for configuration and another for communication. It is similar to an Internet server that has two HTTP servers running: one is public and the other is for administration.

And now there is a question: how may a Bluetooth client device select which service it wants to connect to? Again, refer to the Internet: there is something known as a TCP/IP port. If an Internet server has two HTTP servers, then one may run on the standard HTTP port 80 and the other may run on port 8080. So when connecting to such a server, the browser uses the default port 80 when it specifies the HTTP service in the connection request. But we also may provide a TCP/IP port (8080), and the browser connects to the specified port.

The same appears when we connect to a Bluetooth RFCOMM service. But instead of a port in the Bluetooth world, it is called an RFCOMM channel number (the Channel field of the wclBluetoothService structure). Each RFCOMM service has its own unique RFCOMM channel number. This channel number can be hardcoded or can be dynamically assigned. But it is always unique in a device's services list.

When a Bluetooth RFCOMM client connects to a remote Bluetooth RFCOMM server, it always connects to an RFCOMM channel. If an application does not provide a channel number in the connection request, the OS queries the remote Bluetooth device about its services and tries to resolve the RFCOMM channel number by the service's UUID. If there are two or more services with the same UUID, the OS uses the first one found randomly.

However, in the case of a few services with the same UUID, an application may query the device about the supported service, find the required one (by its name or comment), and provide the RFCOMM channel number in the connection request to connect to exactly the required service. Or, if the RFCOMM channel number is hardcoded on a device's side, an application may simply provide the known RFCOMM channel number without querying device services.

When you run a Bluetooth RFCOMM server, you also provide the service's UUID and (if required) RFCOMM channel number. If (by default) an application does not provide a channel number (this is the recommended way), an OS assigns it automatically. However, an application can provide a fixed RFCOMM channel number. But in this case, the server may not start if the OS already uses this channel number. The RFCOMM channel number can be any from 1 to 31 (including both).


Bluetooth Framework as RFCOMM client

The Bluetooth Framework provides the wclRfCommClient class that allows you to connect to a remote Bluetooth-enabled device's RFCOMM-based service. Each object of the wclRfCommClient class can connect to one remote Bluetooth-enabled device's service. To connect to more than one Bluetooth device at the same time, an application must use more than one object of the wclRfCommClient class. The code below shows how to start a connection.

Connecting

procedure TfmMain.btConnectClick(Sender: TObject);
var
  Res: Integer;
  Radio: TwclBluetoothRadio;
  Services: TwclBluetoothServices;
  Connect: Boolean;
  i: Integer;
  Service: TGUID;
begin
  if lvDevices.Selected = nil then
    MessageDlg('Select device', mtWarning, [mbOK], 0)
  else begin
    Radio := GetRadio;
    if Radio <> nil then begin
      wclRfCommClient.Address := StrToInt64('$' + lvDevices.Selected.Caption);
      wclRfCommClient.Authentication := cbAuthentication.Checked;
      wclRfCommClient.Encryption := cbEncryption.Checked;
      wclRfCommClient.Timeout := StrToInt(edTimeout.Text);
      wclRfCommClient.Channel := 0;
      wclRfCommClient.Service := SerialPortServiceClass_UUID;
      Connect := False;
      if cbServiceName.Checked then begin
        Service := wclRfCommClient.Service;
        Res := Radio.EnumRemoteServices(wclRfCommClient.Address,
          @Service, // We are looking for specified services only!
          Services);
        if Res <> WCL_E_SUCCESS then
          MessageDlg('Service enumerating error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0)
        else begin
          if Length(Services) = 0 then
            ShowMessage('Services not found')
          else begin
            for i := 0 to Length(Services) - 1 do begin
              if Services[i].Name = edServiceName.Text then begin
                wclRfCommClient.Channel := Services[i].Channel;
                Connect := True;
                Break;
              end;
            end;
            if not Connect then
              ShowMessage('Service not found');
          end;
        end;
      end else
        Connect := True;
      if Connect then begin
        Res := wclRfCommClient.Connect(Radio);
        if Res <> WCL_E_SUCCESS then
          MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0)
      end;
    end;
  end;
end;
void __fastcall TfmMain::btConnectClick(TObject *Sender)
{
  if (lvDevices->Selected == NULL)
    MessageDlg("Select device", mtWarning, TMsgDlgButtons() << mbOK, 0);
  else
  {
    TwclBluetoothRadio* Radio = GetRadio();
    if (Radio != NULL)
    {
      wclRfCommClient->Address = StrToInt64("$" + lvDevices->Selected->Caption);
      wclRfCommClient->Authentication = cbAuthentication->Checked;
      wclRfCommClient->Encryption = cbEncryption->Checked;
      wclRfCommClient->Timeout = StrToInt(edTimeout->Text);
      wclRfCommClient->Channel = 0;
      wclRfCommClient->Service = SerialPortServiceClass_UUID;
      bool Connect = false;
      if (cbServiceName->Checked)
      {
        TwclBluetoothServices Services;
        TGUID Service = wclRfCommClient->Service;
        int Res = Radio->EnumRemoteServices(wclRfCommClient->Address,
          &Service, // We are looking for specified services only!
          Services);
        if (Res != WCL_E_SUCCESS)
          MessageDlg("Service enumerating error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
        else
        {
          if (Services.Length == 0)
            ShowMessage("Services not found");
          else
          {
            for (int i = 0; i < Services.Length; i++)
            {
              if (Services[i].Name == edServiceName->Text)
              {
                wclRfCommClient->Channel = Services[i].Channel;
                Connect = true;
                break;
              }
            }
            if (!Connect)
              ShowMessage("Service not found");
          }
        }
      }
      else
        Connect = true;
      if (Connect)
      {
        int Res = wclRfCommClient->Connect(Radio);
        if (Res != WCL_E_SUCCESS)
          MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
      }
    }
  }
}
private void btConnect_Click(object sender, EventArgs e)
{
  if (lvDevices.SelectedItems.Count == 0)
    MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  else
  {
    wclBluetoothRadio Radio = GetRadio();
    if (Radio != null)
    {
      Client.Address = Convert.ToInt64(lvDevices.SelectedItems[0].Text, 16);
      Client.Authentication = cbAuthentication.Checked;
      Client.Encryption = cbEncryption.Checked;
      Client.Timeout = (UInt32)Convert.ToInt32(edTimeout.Text);
      Client.Channel = 0;
      Client.Service = wclUUIDs.SerialPortServiceClass_UUID;
      Boolean Connect = false;
      if (cbServiceName.Checked)
      {
        Guid Service = Client.Service;
        wclBluetoothService[] Services;
        Int32 Res = Radio.EnumRemoteServices(Client.Address,
          Service, // We are looking for specified services only!
          out Services);
        if (Res != wclErrors.WCL_E_SUCCESS)
          MessageBox.Show("Service enumerating error: 0x" + Res.ToString("X8"));
        else
        {
          if (Services == null || Services.Length == 0)
            MessageBox.Show("Services not found");
          else
          {
            foreach (wclBluetoothService s in Services)
            {
              if (s.Name == edServiceName.Text)
              {
                Client.Channel = s.Channel;
                Connect = true;
                break;
              }
            }
            if (!Connect)
              MessageBox.Show("Service not found");
          }
        }
      }
      else
        Connect = true;
      if (Connect)
      {
        Int32 Res = Client.Connect(Radio);
        if (Res != wclErrors.WCL_E_SUCCESS)
          MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
      }
    }
  }
}
Private Sub btConnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btConnect.Click
  If lvDevices.SelectedItems.Count = 0 Then
    MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
  Else
    Dim Radio As wclBluetoothRadio = GetRadio()
    If Radio IsNot Nothing Then
      Client.Address = Convert.ToInt64(lvDevices.SelectedItems(0).Text, 16)
      Client.Authentication = cbAuthentication.Checked
      Client.Encryption = cbEncryption.Checked
      Client.Timeout = Convert.ToInt32(edTimeout.Text)
      Client.Channel = 0
      Client.Service = wclUUIDs.SerialPortServiceClass_UUID
      Dim Connect As Boolean = False
      If cbServiceName.Checked Then
        Dim Service As Guid = Client.Service
        Dim Services As wclBluetoothService()
        Dim Res As Int32 = Radio.EnumRemoteServices(Client.Address, Service, Services)
        If Res <> wclErrors.WCL_E_SUCCESS Then
          MessageBox.Show("Service enumerating error: 0x" + Res.ToString("X8"))
        Else
          If Services Is Nothing Or Services.Length = 0 Then
            MessageBox.Show("Services not found")
          Else
            Dim s As wclBluetoothService
            For Each s In Services
              If s.Name = edServiceName.Text Then
                Client.Channel = s.Channel
                Connect = True
                Exit For
              End If
            Next s
            If Not Connect Then
              MessageBox.Show("Service not found")
            End If
          End If
        End If
      Else
        Connect = True
      End If
      If Connect Then
        Dim Res As Int32 = Client.Connect(Radio)
        If Res <> wclErrors.WCL_E_SUCCESS Then
          MessageBox.Show("Error: 0x" + Res.ToString("X8"), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End If
      End If
    End If
  End If
End Sub
void CRfCommClientDlg::OnBnClickedButtonConnect()
{
  POSITION Pos = lvDevices.GetFirstSelectedItemPosition();
  if (Pos == NULL)
    AfxMessageBox(_T("Select device"));
  else
  {
    int Item = lvDevices.GetNextSelectedItem(Pos);

    CwclBluetoothRadio* Radio = GetRadio();
    if (Radio != NULL)
    {
      wclRfCommClient.SetAddress(StrToInt64(lvDevices.GetItemText(Item, 0)));
      wclRfCommClient.SetAuthentication(cbAuthentication.GetCheck() != 0);
      wclRfCommClient.SetEncryption(cbEncryption.GetCheck() != 0);
      CString s;
      edTimeout.GetWindowText(s);
      wclRfCommClient.SetTimeout(StrToInt(s));

      wclRfCommClient.SetChannel(0);
      wclRfCommClient.SetService(SerialPortServiceClass_UUID);

      bool Connect = false;
      if (cbServiceName.GetCheck())
      {
        GUID Service = wclRfCommClient.GetService();
        wclBluetoothServices Services;
        int Res = Radio->EnumRemoteServices(wclRfCommClient.GetAddress(), &Service, Services);
        if (Res != WCL_E_SUCCESS)
          AfxMessageBox(_T("Service enumerating error: 0x") + IntToHex(Res));
        else
        {
          if (Services.size() == 0)
            AfxMessageBox(_T("Services not found"));
          else
          {
            for (wclBluetoothServices::iterator s = Services.begin(); s != Services.end(); s++)
            {
              CString str;
              edServiceName.GetWindowText(str);
              tstring strs(str);
              if ((*s).Name == strs)
              {
                wclRfCommClient.SetChannel((*s).Channel);
                Connect = true;
                break;
              }
            }
            if (!Connect)
              AfxMessageBox(_T("Service not found"));
          }
        }
      }
      else
        Connect = true;

      if (Connect)
      {
        int Res = wclRfCommClient.Connect(Radio);
        if (Res != WCL_E_SUCCESS)
          AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
      }
    }
  }
}

Once an application sets all required parameters (the target device's address, the service's UUID, the RFCOMM channel number, and the authentication and encryption mode), the application calls the Connect method that starts the connection procedure. The only required parameter is a target device's MAC address. By default, the wclRfCommClient connects to the Serial Port Profile (SPP) with authentication enabled and encryption disabled. If the RFCOMM channel number (the Channel property) is not set or set to zero (the default value), the OS tries to resolve it using the service's UUID as described above in the RFCOMM Channel Number section.

The Connect method just starts the connection procedure. The returning value just indicates (if successful) that the connection procedure has been started, or that the connection procedure has not been started (in case of error). When the connection procedure is completed (the connection is established or is not established), the OnConnect event is called with the connection result code.

procedure TfmMain.wclRfCommClientConnect(Sender: TObject; const Error: Integer);
begin
  if Error = WCL_E_SUCCESS then begin
    lbEvents.Items.Add('Connected');
    GetBuffers;
  end else
    lbEvents.Items.Add('Connect error: 0x' + IntToHex(Error, 8));
end;
void __fastcall TfmMain::wclRfCommClientConnect(TObject *Sender, const int Error)
{
  if (Error == WCL_E_SUCCESS)
  {
    lbEvents->Items->Add("Connected");
    GetBuffers();
  }
  else
    lbEvents->Items->Add("Connect error: 0x" + IntToHex(Error, 8));
}
void Client_OnConnect(object Sender, int Error)
{
  if (Error == wclErrors.WCL_E_SUCCESS)
  {
    lbEvents.Items.Add("Connected");
    GetBuffers();
  }
  else
    lbEvents.Items.Add("Connect error: 0x" + Error.ToString("X8"));
}
Private Sub Client_OnConnect(ByVal Sender As Object, ByVal [Error] As Integer) Handles Client.OnConnect
  If [Error] = wclErrors.WCL_E_SUCCESS Then
    lbEvents.Items.Add("Connected")
    GetBuffers()
  Else
    lbEvents.Items.Add("Connect error: 0x" + [Error].ToString("X8"))
  End If
End Sub
void CRfCommClientDlg::wclRfCommClientConnect(void* Sender, int Error)
{
  if (Error == WCL_E_SUCCESS)
  {
    lbEvents.AddString(_T("Connected"));
    GetBuffers();
  }
  else
    lbEvents.AddString(_T("Connect error: 0x") + IntToHex(Error));
}

As you can see in the code above, the real connection result is passed as the Error parameter into the OnConnect event handler. If the Error parameter value is WCL_E_SUCCESS, the connection to a remote device has been established successfully. If the Error parameter has another value, it indicates one of the connection error codes (you can find a list of all Wireless Communication Library error codes by this link).

Sending data

When the connection is established, an application can start communication with a connected Bluetooth device. To send data to the connected device, an application calls the Write method of the wclRfCommClient class.

procedure TfmMain.btSendClick(Sender: TObject);
var
  Ansi: AnsiString;
  Res: Integer;
  Sent: Cardinal;
begin
  Ansi := AnsiString(edText.Text);
  Res := wclRfCommClient.Write(PByte(Ansi), Length(Ansi), Sent);
  if Res <> WCL_E_SUCCESS then
    MessageDlg('Error: 0x' + IntToHex(Res, 8), mtError, [mbOK], 0)
  else
    MessageDlg('Sent: ' + IntToStr(Sent) + ' from ' + IntToStr(Length(Ansi)), mtInformation, [mbOK], 0);
end;
void __fastcall TfmMain::btSendClick(TObject *Sender)
{
  AnsiString Ansi = AnsiString(edText->Text);
  unsigned int Sent = 0;
  int Res = wclRfCommClient->Write(Ansi.c_str(), Ansi.Length(), Sent);
  if (Res != WCL_E_SUCCESS)
    MessageDlg("Error: 0x" + IntToHex(Res, 8), mtError, TMsgDlgButtons() << mbOK, 0);
  else
    MessageDlg("Sent: " + IntToStr((int)Sent) + " from " + IntToStr(Ansi.Length()), mtInformation, TMsgDlgButtons() << mbOK, 0);
}
private void btSend_Click(object sender, EventArgs e)
{
  Byte[] Ansi = Encoding.ASCII.GetBytes(edText.Text);
  UInt32 Sent = 0;
  Int32 Res = Client.Write(Ansi, out Sent);
  if (Res != wclErrors.WCL_E_SUCCESS)
    MessageBox.Show("Error: 0x" + Res.ToString("X8"));
  else
    MessageBox.Show("Sent: " + Sent.ToString() + " from " + Ansi.Length.ToString());
}
Private Sub btSend_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btSend.Click
  Dim Ansi As Byte() = System.Text.Encoding.ASCII.GetBytes(edText.Text)
  Dim Sent As UInt32 = 0
  Dim Res As Int32 = Client.Write(Ansi, Sent)
  If Res <> wclErrors.WCL_E_SUCCESS Then
    MessageBox.Show("Error: 0x" + Res.ToString("X8"))
  Else
    MessageBox.Show("Sent: " + Sent.ToString() + " from " + Ansi.Length.ToString())
  End If
End Sub
void CRfCommClientDlg::OnBnClickedButtonSend()
{
  CString s;
  edText.GetWindowText(s);
  CStringA Ansi(s);
  unsigned long Sent;
  int Res = wclRfCommClient.Write(Ansi.GetBuffer(), Ansi.GetLength(), Sent);
  if (Res != WCL_E_SUCCESS)
    AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
  else
    AfxMessageBox(_T("Sent: ") + IntToStr(Sent) + _T(" from ") + IntToStr(Ansi.GetLength()));
}

Receiving data

When data is received from the connected device, the OnData event is called. That was the basic information you need to know about an RFCOMM connection.

procedure TfmMain.wclRfCommClientData(Sender: TObject; const Data: Pointer; const Size: Cardinal);
var
  Str: AnsiString;
begin
  if Size > 0 then begin
    SetLength(Str, Size);
    CopyMemory(Pointer(Str), Data, Size);
    lbEvents.Items.Add('Received: ' + string(Str));
  end;
end;
void __fastcall TfmMain::wclRfCommClientData(TObject *Sender, const Pointer Data, const DWORD Size)
{
  if (Size > 0)
  {
    AnsiString Str = AnsiString((AnsiChar*)Data, Size);
    lbEvents->Items->Add("Received: " + String(Str));
  }
}
void Client_OnData(object Sender, byte[] Data)
{
  if (Data != null && Data.Length > 0)
  {
    String Str = Encoding.ASCII.GetString(Data);
    lbEvents.Items.Add("Received " + Data.Length.ToString() + " bytes: " + Str);
  }
}
Private Sub Client_OnData(ByVal Sender As Object, ByVal Data() As Byte) Handles Client.OnData
  If Data IsNot Nothing And Data.Length > 0 Then
    Dim Str As String = System.Text.Encoding.ASCII.GetString(Data)
    lbEvents.Items.Add("Received: " + Str)
  End If
End Sub
void CRfCommClientDlg::wclRfCommClientData(void* Sender, void* Data, unsigned long Size)
{
  if (Size > 0)
  {
    CStringA Str((PCHAR)Data, (int)Size);
    CString s(Str);
    lbEvents.AddString(_T("Received: ") + s);
  }
}

Bluetooth Framework as RFCOMM server

The Bluetooth Framework also provides the wclRfCommServer class, allowing your application to act as an RFCOMM server (acceptor). The server registers a service with a specific UUID (and optionally a channel number) in the local SDP database, listens for incoming connections, and manages multiple connected clients.

Listening for connections

To start the server, configure the service UUID, the authentication and encryption requirements, and call the Listen method. The OS will automatically assign an RFCOMM channel unless you specify one explicitly. The OnListen event fires when the server is successfully started and ready to accept connections.

procedure TfmMain.btListenClick(Sender: TObject);
var
  Res: Integer;
  Radio: TwclBluetoothRadio;
begin
  Radio := GetRadio;
  if Radio <> nil then begin
    // Switch radio in discoverable and connectable mode.
    Radio.SetDiscoverable(True);
    Radio.SetConnectable(True);

    wclRfCommServer.Authentication := cbAuthentication.Checked;
    wclRfCommServer.Encryption := cbEcryption.Checked;
    
    wclRfCommServer.Service := SerialPortServiceClass_UUID;
    wclRfCommServer.ServiceName := edServiceName.Text;

    Res := wclRfCommServer.Listen(Radio);
    if Res <> WCL_E_SUCCESS then
      ShowMessage('Error: 0x' + IntToHex(Res, 8));
  end;
end;

procedure TfmMain.wclRfCommServerListen(Sender: TObject);
begin
  // AssignedChannel gives the channel number assigned by the OS
  lbEvents.Items.Add('Server listening on radio ' +
    TwclRfCommServer(Sender).Radio.ApiName + ' on channel: ' +
    IntToStr(TwclRfCommServer(Sender).AssignedChannel));
end;
void __fastcall TfmMain::btListenClick(TObject *Sender)
{
  TwclBluetoothRadio* Radio = GetRadio();
  if (Radio != NULL)
  {
    Radio->SetDiscoverable(true);
    Radio->SetConnectable(true);

    wclRfCommServer->Authentication = cbAuthentication->Checked;
    wclRfCommServer->Encryption = cbEcryption->Checked;

    wclRfCommServer->Service = SerialPortServiceClass_UUID;
    wclRfCommServer->ServiceName = edServiceName->Text;

    int Res = wclRfCommServer->Listen(Radio);
    if (Res != WCL_E_SUCCESS)
      ShowMessage("Error: 0x" + IntToHex(Res, 8));
  }
}

void __fastcall TfmMain::wclRfCommServerListen(TObject *Sender)
{
  lbEvents->Items->Add("Server listening on radio " +
    ((TwclRfCommServer*)Sender)->Radio->ApiName + " on channel: " +
    IntToStr(((TwclRfCommServer*)Sender)->AssignedChannel));
}
private void btListen_Click(object sender, EventArgs e)
{
  wclBluetoothRadio Radio = GetRadio();
  if (Radio != null)
  {
    Radio.SetDiscoverable(true);
    Radio.SetConnectable(true);

    Server.Authentication = cbAuthentication.Checked;
    Server.Encryption = cbEncryption.Checked;

    Server.Service = wclUUIDs.SerialPortServiceClass_UUID;
    Server.ServiceName = edServiceName.Text;

    Int32 Res = Server.Listen(Radio);
    if (Res != wclErrors.WCL_E_SUCCESS)
      MessageBox.Show("Error: 0x" + Res.ToString("X8"));
  }
}

void Server_OnListen(object sender, EventArgs e)
{
  lbEvents.Items.Add("Server listening on radio " +
    ((wclRfCommServer)sender).Radio.ApiName + " on channel: " +
    ((wclRfCommServer)sender).AssignedChannel.ToString());
}
Private Sub btListen_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btListen.Click
  Dim Radio As wclBluetoothRadio = GetRadio()
  If Radio IsNot Nothing Then
    Radio.SetDiscoverable(True)
    Radio.SetConnectable(True)

    Server.Authentication = cbAuthentication.Checked
    Server.Encryption = cbEncryption.Checked

    Server.Service = wclUUIDs.SerialPortServiceClass_UUID
    Server.ServiceName = edServiceName.Text

    Dim Res As Int32 = Server.Listen(Radio)
    If Res <> wclErrors.WCL_E_SUCCESS Then
      MessageBox.Show("Error: 0x" + Res.ToString("X8"))
    End If
  End If
End Sub

Private Sub Server_OnListen(ByVal sender As Object, ByVal e As System.EventArgs) Handles Server.OnListen
  Dim Server As wclRfCommServer = CType(sender, wclRfCommServer)
  lbEvents.Items.Add("Server listening on radio " + Server.Radio.ApiName + " on channel: " + Server.AssignedChannel.ToString())
End Sub
void CRfCommServerDlg::OnBnClickedButtonListen()
{
  CwclBluetoothRadio* Radio = GetRadio();
  if (Radio != NULL)
  {
    Radio->SetDiscoverable(true);
    Radio->SetConnectable(true);

    CString s;
    edServiceName.GetWindowText(s);

    wclRfCommServer.Authentication = (cbAuthentication.GetCheck() != 0);
    wclRfCommServer.Encryption = (cbEncryption.GetCheck() != 0);

    wclRfCommServer.Service = SerialPortServiceClass_UUID;
    wclRfCommServer.ServiceName = tstring(s);
    
    int Res = wclRfCommServer.Listen(Radio);
    if (Res != WCL_E_SUCCESS)
      AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
  }
}

void CRfCommServerDlg::wclRfCommServerListen(void* Sender)
{
  lbEvents.AddString(_T("Server listening on radio ") +
    CString(((CwclRfCommServer*)Sender)->Radio->ApiName.c_str()) +
    _T(" on channel: ") + IntToStr(((CwclRfCommServer*)Sender)->AssignedChannel));
}

Handling client connections

When a client connects, the OnConnect event fires, providing a Client object that represents the specific connection. You use this object to track the client, read incoming data in OnData, or disconnect the client later.

procedure TfmMain.wclRfCommServerConnect(Sender: TObject;
  const Client: TwclRfCommServerClientConnection; const Error: Integer);
var
  Item: TListItem;
begin
  lbEvents.Items.Add('Client connected: ' + IntToHex(Client.Address, 12) +
    ' error: 0x' + IntToHex(Error, 8));

  Item := lvClients.Items.Add;
  Item.Caption := IntToHex(Client.Address, 12);
  Item.Data := Client;
end;

procedure TfmMain.wclRfCommServerData(Sender: TObject;
  const Client: TwclRfCommServerClientConnection; const Data: Pointer;
  const Size: Cardinal);
var
  Str: AnsiString;
begin
  if Size > 0 then begin
    SetLength(Str, Size);
    CopyMemory(Pointer(Str), Data, Size);
    lbEvents.Items.Add('Received: ' + string(Str) + ' from ' +
      IntToHex(Client.Address, 12));
  end;
end;
void __fastcall TfmMain::wclRfCommServerConnect(TObject *Sender,
      const TwclRfCommServerClientConnection *Client, const int Error)
{
  lbEvents->Items->Add("Client connected: " + IntToHex(Client->Address, 12) +
    " error: 0x" + IntToHex(Error, 8));

  TListItem* Item = lvClients->Items->Add();
  Item->Caption = IntToHex(Client->Address, 12);
  Item->Data = (void*)Client;
}

void __fastcall TfmMain::wclRfCommServerData(TObject *Sender,
      const TwclRfCommServerClientConnection *Client, const Pointer Data,
      const DWORD Size)
{
  if (Size > 0)
  {
    AnsiString Str = AnsiString((PAnsiChar)Data, Size);
    lbEvents->Items->Add("Received: " + String(Str) + " from " +
      IntToHex(Client->Address, 12));
  }
}
void Server_OnConnect(object Sender, wclRfCommServerClientConnection Client, int Error)
{
  lbEvents.Items.Add("Client connected: " + Client.Address.ToString("X12") +
    " error: 0x" + Error.ToString("X8"));

  ListViewItem Item = lvClients.Items.Add(Client.Address.ToString("X12"));
  Item.Tag = Client;
}

void Server_OnData(object Sender, wclRfCommServerClientConnection Client, byte[] Data)
{
  if (Data != null && Data.Length > 0)
  {
    String Str = Encoding.ASCII.GetString(Data);
    lbEvents.Items.Add("Received " + Data.Length.ToString() + " bytes: " + Str +
      " from " + Client.Address.ToString("X12"));
  }
}
Private Sub Server_OnConnect(ByVal Sender As Object, ByVal Client As wclBluetooth.wclRfCommServerClientConnection, ByVal [Error] As Integer) Handles Server.OnConnect
  lbEvents.Items.Add("Client connected: " + Client.Address.ToString("X12") + " error: 0x" + [Error].ToString("X8"))

  Dim Item As ListViewItem = lvClients.Items.Add(Client.Address.ToString("X12"))
  Item.Tag = Client
End Sub

Private Sub Server_OnData(ByVal Sender As Object, ByVal Client As wclBluetooth.wclRfCommServerClientConnection, ByVal Data() As Byte) Handles Server.OnData
  If Data IsNot Nothing AndAlso Data.Length > 0 Then
    Dim Str As String = Encoding.ASCII.GetString(Data)
    lbEvents.Items.Add("Received: " + Str + " from " + Client.Address.ToString("X12"))
  End If
End Sub
void CRfCommServerDlg::wclRfCommServerConnect(void* Sender,
  CwclRfCommServerClientConnection* const Client, const int Error)
{
  lbEvents.AddString(_T("Client connected: ") + IntToHex(Client->Address) +
    _T(" error: 0x") + IntToHex(Error));

  int Item = lvClients.GetItemCount();
  lvClients.InsertItem(Item, IntToHex(Client->Address));
  lvClients.SetItemData(Item, (DWORD_PTR)Client);
}

void CRfCommServerDlg::wclRfCommServerData(void* Sender, CwclRfCommServerClientConnection* const Client,
  const void* const Data, const unsigned long Size)
{
  if (Size > 0)
  {
    CStringA a((PCHAR)Data, Size);
    CString s(a);
    lbEvents.AddString(_T("Received: ") + s + _T(" from ") + IntToHex(Client->Address));
  }
}

Sending data to clients

To send data to a specific connected client, use the Write method of the TwclRfCommServerClientConnection object associated with that client.

procedure TfmMain.btSendClick(Sender: TObject);
var
  Res: Integer;
  Ansi: AnsiString;
  Sent: Cardinal;
begin
  if lvClients.Selected = nil then
    ShowMessage('Select client')
  else begin
    Ansi := AnsiString(edText.Text);
    Res := TwclRfCommServerClientConnection(
      lvClients.Selected.Data).Write(PByte(Ansi), Length(Ansi), Sent);
    if Res <> WCL_E_SUCCESS then
      ShowMessage('Error: 0x' + IntToHex(Res, 8))
    else
      ShowMessage('Sent: ' + IntToStr(Sent) + ' from ' + IntToStr(Length(Ansi)));
  end;
end;
void __fastcall TfmMain::btSendClick(TObject *Sender)
{
  if (lvClients->Selected == NULL)
    ShowMessage("Select client");
  else
  {
    AnsiString Ansi = AnsiString(edText->Text);
    unsigned int Sent = 0;
    int Res = ((TwclRfCommServerClientConnection*)
      lvClients->Selected->Data)->Write(Ansi.c_str(), Ansi.Length(), Sent);
    if (Res != WCL_E_SUCCESS)
      ShowMessage("Error: 0x" + IntToHex(Res, 8));
    else
      ShowMessage("Sent: " + IntToStr((int)Sent) + " from " + IntToStr(Ansi.Length()));
  }
}
private void btSend_Click(object sender, EventArgs e)
{
  if (lvClients.SelectedItems.Count == 0)
    MessageBox.Show("Select client");
  else
  {
    Byte[] Ansi = Encoding.ASCII.GetBytes(edText.Text);
    UInt32 Sent = 0;
    Int32 Res = ((wclRfCommServerClientConnection)lvClients.SelectedItems[0].Tag).Write(Ansi, out Sent);
    if (Res != wclErrors.WCL_E_SUCCESS)
      MessageBox.Show("Error: 0x" + Res.ToString("X8"));
    else
      MessageBox.Show("Sent: " + Sent.ToString() + " from " + Ansi.Length.ToString());
  }
}
Private Sub btSend_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btSend.Click
  If lvClients.SelectedItems.Count = 0 Then
    MessageBox.Show("Select client")
  Else
    Dim Ansi As Byte() = Encoding.ASCII.GetBytes(edText.Text)
    Dim Sent As UInt32 = 0
    Dim Res As Int32 = CType(lvClients.SelectedItems(0).Tag, wclRfCommServerClientConnection).Write(Ansi, Sent)
    If Res <> wclErrors.WCL_E_SUCCESS Then
      MessageBox.Show("Error: 0x" + Res.ToString("X8"))
    Else
      MessageBox.Show("Sent: " + Sent.ToString() + " from " + Ansi.Length.ToString())
    End If
  End If
End Sub
void CRfCommServerDlg::OnBnClickedButtonSend()
{
  POSITION Pos = lvClients.GetFirstSelectedItemPosition();
  if (Pos == NULL)
    AfxMessageBox(_T("Select client"));
  else
  {
    int Item = lvClients.GetNextSelectedItem(Pos);
    CString s;
    edText.GetWindowText(s);
    CStringA Ansi(s);
    unsigned long Sent;
    int Res = ((CwclRfCommServerClientConnection*)lvClients.GetItemData(Item))->Write(Ansi.GetBuffer(), Ansi.GetLength(), Sent);
    if (Res != WCL_E_SUCCESS)
      AfxMessageBox(_T("Error: 0x") + IntToHex(Res));
    else
      AfxMessageBox(_T("Sent: ") + IntToStr(Sent) + _T(" from ") + IntToStr(Ansi.GetLength()));
  }
}

Frequently asked questions

What is Bluetooth RFCOMM?
RFCOMM is a Bluetooth protocol that emulates RS-232 serial ports over L2CAP. It serves as the transport for many classic Bluetooth profiles, including Serial Port Profile (SPP), Object Push Profile (OPP), and File Transfer Profile (FTP).
How do I connect to a classic Bluetooth device with the Bluetooth Framework?
Use the wclRfCommClient class. Set the target device's MAC address and service UUID, then call the Connect method. The connection result is delivered via the OnConnect event. You can optionally specify an RFCOMM channel number if multiple instances of the same service exist.
What is the RFCOMM channel number and why is it important?
An RFCOMM channel number (1-31) uniquely identifies a specific service instance on a Bluetooth device. If a device offers multiple services with the same UUID, you can use the channel number to connect to the exact one you need.