A Bluetooth virtual COM port — vCOM — is a Windows serial port whose bytes are carried over a Bluetooth Serial Port Profile connection instead of a physical cable. The application that opens the port cannot tell the difference: it gets a port number, a baud rate, and a stream of bytes, exactly like a real UART. The Bluetooth Framework exposes the entire lifecycle of these ports through wclBluetoothRadio, so the developer never has to touch the Windows Bluetooth driver, the registry, or the system control panel.

Bluetooth vCOM bridges a legacy serial application and a remote SPP device: the app opens COM5, the vCOM driver forwards bytes to the Bluetooth stack, and the stack talks RFCOMM and SPP to the remote SPP device

Everything on the left side of the diagram is the application's world: a COM port number, a set of baud-rate settings, and a blocking or overlapped read. Everything on the right side is the Bluetooth world: RFCOMM channels, an SPP service record, and a wireless link. The vCOM driver sits between them and translates one into the other, one byte at a time.

This article covers the four members that the Bluetooth Framework exposes for this — ComPorts, EnumComPorts, CreateComPort, and DestroyComPort — using the BluetoothManager sample application as the source of the code examples.


Why use vCOM

A modern application that is being written against the Bluetooth Framework does not need vCOM: it can open an RFCOMM connection directly, using the classes described in the Classic Bluetooth guide, and get a clean byte stream with full control over the connection lifecycle. vCOM exists for a different audience, and its reason for existing is worth stating plainly.

Legacy applications

Many applications that talk to Bluetooth-enabled hardware were written before Bluetooth existed. A barcode scanner, a weighing scale, a GPS receiver, a medical sensor, a CNC controller, a label printer — each of these devices was originally a serial device, attached to a physical RS-232 cable on a specific COM port. The application opens that port, configures baud rate and flow control, and then reads and writes a stream. None of that code knows that Bluetooth exists, and none of it needs to.

With vCOM, the same application continues to work unchanged. The virtual port appears in Windows as an ordinary serial port with a number the application can be configured to use. From the application's point of view, the cable has simply become wireless. There is no DLL to swap, no API to call, no build to reproduce — the binary that ran against COM1 yesterday runs against COM5 today, and the only visible difference is the port number.

Existing legacy code

Even when the application is being actively developed, it often contains a well-tested serial transport layer — a parser, a state machine, a set of timing assumptions — that took years to get right. Rewriting that layer on top of a Bluetooth API would mean re-testing everything that goes through it: the framing, the retries, the flow-control rules, the timeouts, the behaviour at every possible buffer boundary. With vCOM, that code stays exactly as it is, and only the transport underneath changes from a UART to a Bluetooth link.

This is the second reason vCOM survives in modern codebases: it preserves the shape of the application. A serial application is typically synchronous — it calls ReadFile and blocks until data arrives, or it sets up overlapped I/O with a dedicated thread. The Windows serial driver, and the Bluetooth stack below it, handle the buffering, so the application's threading model does not have to change.

Cable replacement

This is the use case vCOM was designed for. The Serial Port Profile exists precisely to replace an RS-232 cable with a wireless link, and vCOM is the Windows-visible form of that replacement. Any device that speaks SPP — UUID 0x1101 — can be reached through a vCOM port: barcode scanners, GPS receivers, industrial controllers, point-of-sale terminals, medical instruments, and countless other devices whose firmware has not changed since the 1990s.

Devices that do not speak SPP are not reachable through vCOM. BLE GATT services, audio profiles, object exchange, and the other profiles described in the Bluetooth Classic profiles article all need the framework's dedicated classes — vCOM is specific to SPP.


Enumerating vCOM ports

A vCOM port is bound to a specific pair of values: a Bluetooth device address and a service UUID. The same device can expose more than one SPP service, and each of them can have its own virtual port; conversely, a single device with a single SPP service can have exactly one port at a time. Enumerating the current ports is therefore not just "list the connected devices" — it is "list the (device, service) pairs that Windows currently has a port for".

The wclBluetoothRadio class exposes two ways to get the current list. The ComPorts property returns the ports that the framework itself is currently tracking in this session. The EnumComPorts method returns the full set, including ports that were created in a previous session and still exist in the system. The difference matters when the application starts up: ComPorts is empty until something has been created in the current process, while EnumComPorts reports everything the system knows about.

In the BluetoothManager sample application, the Enum preinstalled checkbox chooses between the two. When it is checked, EnumComPorts is used; otherwise the faster ComPorts property is read directly.

procedure TfmMain.RefreshVComs;
var
  i: Integer;
  Item: TListItem;
  Radio: TwclBluetoothRadio;
  Ports: TwclVirtualComPorts;
  Port: TwclVirtualComPort;
  Res: Integer;
begin
  ClearComPorts;

  Item := lvRadios.Selected;
  Radio := TwclBluetoothRadio(Item.Data);

  if cbEnumPreinstalled.Checked then
    Res := Radio.EnumComPorts(Ports)
  else begin
    Ports := Radio.ComPorts;
    Res := WCL_E_SUCCESS;
  end;

  if Res <> WCL_E_SUCCESS then
    ShowMessage('Error enumerating vCOMs: 0x' + IntToHex(Res, 8))

  else begin
    for i := 0 to Length(Ports) - 1 do begin
      Port := Ports[i];

      Item := lvVComs.Items.Add;
      Item.Caption := Radio.ApiName;
      Item.SubItems.Add(IntToHex(Port.Address, 12));
      Item.SubItems.Add(GUIDToString(Port.Service));
      Item.SubItems.Add(IntToStr(Port.Number));

      Item.Data := Radio;
    end;
  end;
end;
void __fastcall TfmMain::RefreshVComs()
{
  ClearComPorts();

  TListItem* Item = lvRadios->Selected;
  TwclBluetoothRadio* Radio = (TwclBluetoothRadio*)(Item->Data);

  int Res;
  TwclVirtualComPorts Ports;
  if (cbEnumPreinstalled->Checked)
    Res = Radio->EnumComPorts(Ports);
  else
  {
    Ports = Radio->ComPorts;
    Res = WCL_E_SUCCESS;
  }

  if (Res != WCL_E_SUCCESS)
    ShowMessage("Error enumerating vCOMs: 0x" + IntToHex(Res, 8));

  else
  {
    for (int i = 0; i < Radio->ComPorts.Length; i++)
    {
      TwclVirtualComPort Port = Ports[i];

      Item = lvVComs->Items->Add();
      Item->Caption = Radio->ApiName;
      Item->SubItems->Add(IntToHex(Port.Address, 12));
      Item->SubItems->Add(Sysutils::GUIDToString(Port.Service));
      Item->SubItems->Add(IntToStr(Port.Number));

      Item->Data = Radio;
    }
  }
}
private void RefreshVComs()
{
    ClearComPorts();

    ListViewItem Item = lvRadios.SelectedItems[0];
    wclBluetoothRadio Radio = (wclBluetoothRadio)Item.Tag;

    Int32 Res;
    wclVirtualComPort[] Ports;
    if (cbEnumPreinstalled.Checked)
        Res = Radio.EnumComPorts(out Ports);
    else
    {
        Ports = Radio.ComPorts;
        Res = wclErrors.WCL_E_SUCCESS;
    }

    if (Res != wclErrors.WCL_E_SUCCESS)
        MessageBox.Show("Error enumerating vCOMs: 0x" + Res.ToString("X8"));
    else
    {
        if (Ports != null && Ports.Length > 0)
        {
            for (int i = 0; i < Ports.Length; i++)
            {
                wclVirtualComPort Port = Ports[i];

                Item = lvComPorts.Items.Add(Radio.ApiName);
                Item.SubItems.Add(Port.Address.ToString("X12"));
                Item.SubItems.Add(Port.Service.ToString());
                Item.SubItems.Add(Port.Number.ToString());

                Item.Tag = Radio;
            }
        }
    }
}
Private Sub RefreshVComs()
    ClearComPorts()

    Dim Item As ListViewItem = lvRadios.SelectedItems(0)
    Dim Radio As wclBluetoothRadio = CType(Item.Tag, wclBluetoothRadio)

    Dim Res As Int32
    Dim Ports As wclVirtualComPort() = Nothing
    If cbEnumPreinstalled.Checked Then
        Res = Radio.EnumComPorts(Ports)
    Else
        Ports = Radio.ComPorts
        Res = wclErrors.WCL_E_SUCCESS
    End If

    If Res <> wclErrors.WCL_E_SUCCESS Then
        MessageBox.Show("Error enumerating vCOMs: 0x" + Res.ToString("X8"))
    Else
        If Ports IsNot Nothing AndAlso Ports.Length > 0 Then
            For i As Integer = 0 To Ports.Length - 1
                Dim Port As wclVirtualComPort = Ports(i)

                Item = lvComPorts.Items.Add(Radio.ApiName)
                Item.SubItems.Add(Port.Address.ToString("X12"))
                Item.SubItems.Add(Port.Service.ToString())
                Item.SubItems.Add(Port.Number.ToString())

                Item.Tag = Radio
            Next i
        End If
    End If
End Sub
void CBluetoothManagerDlg::RefreshVComs()
{
    ClearComPorts();

    POSITION Pos = lvRadios.GetFirstSelectedItemPosition();
    if (Pos != NULL)
    {
        int Item = lvRadios.GetNextSelectedItem(Pos);
        CwclBluetoothRadio* Radio = (CwclBluetoothRadio*)lvRadios.GetItemData(Item);

        wclVirtualComPorts Ports;
        int Res;
        if (cbEnumPreinstalled.GetCheck())
            Res = Radio->EnumComPorts(Ports);
        else
        {
            Ports = Radio->ComPorts;
            Res = WCL_E_SUCCESS;
        }

        if (Res != WCL_E_SUCCESS)
            AfxMessageBox(_T("Error enumerating vCOMs: 0x") + IntToHex(Res));
        else
        {
            int n = 0;
            for (wclVirtualComPorts::iterator i = Ports.begin(); i != Ports.end(); i++)
            {
                wclVirtualComPort Port = (*i);
                LPOLESTR Guid;
                StringFromCLSID(Port.Service, &Guid);

                lvComPorts.InsertItem(n, Radio->ApiName.c_str());
                lvComPorts.SetItemText(n, 1, IntToHex(Port.Address));
                lvComPorts.SetItemText(n, 2, Guid);
                lvComPorts.SetItemText(n, 3, IntToStr(Port.Number));
                lvComPorts.SetItemData(n, (DWORD_PTR)Radio);
                n++;
            }
        }
    }
}

Creating a vCOM port

To create a vCOM port, the application needs two things: the Bluetooth address of the target device, and the UUID of the SPP service on that device. Both come from the SDP enumeration described in the Bluetooth Framework article — a device that supports SPP advertises the service with UUID 0x1101, along with the RFCOMM channel it expects to use. In the sample application, the user first selects a device from the Devices list, then selects the SPP service from the Services list, and then clicks Create vCOM.

Once those values are known, CreateComPort does the rest: it asks Windows to register a new virtual serial port, binds it to the given device and service, and returns the Number assigned to the port. From that moment on, the port is visible to the whole system — any application can open it by number, including the legacy application that motivated the whole exercise.

procedure TfmMain.btCreateVCOMClick(Sender: TObject);
var
  Item: TListItem;
  Radio: TwclBluetoothRadio;
  Address: Int64;
  Res: Integer;
  Service: TGUID;
  Number: Word;
begin
  if lvDevices.Selected = nil then
    MessageDlg('Select device', mtWarning, [mbOK], 0)

  else begin
    if lvServices.Selected = nil then
      MessageDlg('Select service', mtWarning, [mbOK], 0)

    else begin
      Item := lvDevices.Selected;
      Radio := TwclBluetoothRadio(Item.Data);
      Address := StrToInt64('$' + Item.SubItems[0]);
      Service := StringToGUID(lvServices.Selected.SubItems[2]);

      Res := Radio.CreateComPort(Address, Service, Number);
      if Res <> WCL_E_SUCCESS then
        ShowError(Res)

      else begin
        TraceEvent(Radio, 'Create vCOM', 'Number', IntToStr(Number));

        RefreshVComs;
      end;
    end;
  end;
end;
void __fastcall TfmMain::btCreateVCOMClick(TObject *Sender)
{
  if (lvDevices->Selected == NULL)
    MessageDlg("Select device", mtWarning, TMsgDlgButtons() << mbOK, 0);
  else
    if (lvServices->Selected == NULL)
      MessageDlg("Select service", mtWarning, TMsgDlgButtons() << mbOK, 0);
    else
    {
      TListItem* Item = lvDevices->Selected;
      TwclBluetoothRadio* Radio = (TwclBluetoothRadio*)(Item->Data);
      __int64 Address = StrToInt64("$" + Item->SubItems->Strings[0]);
      GUID Service = Sysutils::StringToGUID(lvServices->Selected->SubItems->Strings[2]);

      unsigned short Number;
      int Res = Radio->CreateComPort(Address, Service, Number);
      if (Res != WCL_E_SUCCESS)
        ShowError(Res);
      else
      {
        TraceEvent(Radio, "Create vCOM", "Number", IntToStr(Number));

        RefreshVComs();
      }
    }
}
private void btCreateVCOM_Click(object sender, EventArgs e)
{
    if (lvDevices.SelectedItems.Count == 0)
        MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    else
    {
        if (lvServices.SelectedItems.Count == 0)
            MessageBox.Show("Select service", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        else
        {
            ListViewItem Item = lvDevices.SelectedItems[0];
            wclBluetoothRadio Radio = (wclBluetoothRadio)Item.Tag;
            Int64 Address = Convert.ToInt64(Item.SubItems[1].Text, 16);
            Guid Service = new Guid(lvServices.SelectedItems[0].SubItems[3].Text);

            UInt16 Number;
            Int32 Res = Radio.CreateComPort(Address, Service, out Number);
            if (Res != wclErrors.WCL_E_SUCCESS)
                ShowError(Res);
            else
            {
                TraceEvent(Radio, "Create vCOM", "Number", Number.ToString());

                RefreshVComs();
            }
        }
    }
}
Private Sub btCreateVCOM_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btCreateVCOM.Click
    If lvDevices.SelectedItems.Count = 0 Then
        MessageBox.Show("Select device", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    Else
        If lvServices.SelectedItems.Count = 0 Then
            MessageBox.Show("Select service", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
        Else
            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 Service As Guid = New Guid(lvServices.SelectedItems(0).SubItems(3).Text)

            Dim Number As UInt16
            Dim Res As Int32 = Radio.CreateComPort(Address, Service, Number)
            If Res <> wclErrors.WCL_E_SUCCESS Then
                ShowError(Res)
            Else
                TraceEvent(Radio, "Create vCOM", "Number", Number.ToString())

                RefreshVComs()
            End If
        End If
    End If
End Sub
void CBluetoothManagerDlg::OnBnClickedButtonCreatePort()
{
    POSITION DevPos = lvDevices.GetFirstSelectedItemPosition();
    if (DevPos == NULL)
        AfxMessageBox(_T("Select device"));
    else
    {
        POSITION SrvPos = lvServices.GetFirstSelectedItemPosition();
        if (SrvPos == NULL)
            AfxMessageBox(_T("Select service"));
        else
        {
            int DevItem = lvDevices.GetNextSelectedItem(DevPos);
            CwclBluetoothRadio* Radio = (CwclBluetoothRadio*)lvDevices.GetItemData(DevItem);
            __int64 Address = StrToInt64(lvDevices.GetItemText(DevItem, 1));

            int SrvItem = lvServices.GetNextSelectedItem(SrvPos);
            CString s = lvServices.GetItemText(SrvItem, 3);
            GUID Service;
            CLSIDFromString((LPOLESTR)s.GetBuffer(), &Service);

            unsigned short Number;
            int Res = Radio->CreateComPort(Address, Service, Number);
            if (Res != WCL_E_SUCCESS)
                ShowError(Res);
            else
            {
                TraceEvent(Radio, _T("Create vCOM"), _T("Number"), IntToStr(Number));

                RefreshVComs();
            }
        }
    }
}

Destroying a vCOM port

A vCOM port is removed by calling DestroyComPort with the port number — not with the device address. This matters: a device may expose several services, and each of them may have its own port, so the device address is not enough to identify which port to remove. The Number returned by CreateComPort, or the Number field of an entry returned by EnumComPorts or ComPorts, is the only unambiguous identifier.

Destroying a vCOM port does not affect the device's pairing state and does not disconnect any active connection. It only removes the Windows-side serial port; the Bluetooth link, if one is up, stays up, and the device remains paired for future use. The legacy application that was using the port will see the port disappear, exactly as if a USB-to-serial adapter had been unplugged.

procedure TfmMain.btDestroyVCOMClick(Sender: TObject);
var
  Item: TListItem;
  Radio: TwclBluetoothRadio;
  Number: Word;
  Res: Integer;
begin
  if lvVComs.Selected = nil then
    MessageDlg('Select vCOM', mtWarning, [mbOK], 0)

  else begin
    Item := lvVComs.Selected;
    Radio := TwclBluetoothRadio(Item.Data);
    Number := StrToInt(Item.SubItems[2]);

    Res := Radio.DestroyComPort(Number);
    if Res <> WCL_E_SUCCESS then
      ShowError(Res)

    else begin
      TraceEvent(Radio, 'Destroy vCOM', 'Number', IntToStr(Number));

      RefreshVComs;
    end;
  end;
end;
void __fastcall TfmMain::btDestroyVCOMClick(TObject *Sender)
{
  if (lvVComs->Selected == NULL)
    MessageDlg("Select vCOM", mtWarning, TMsgDlgButtons() << mbOK, 0);
  else
  {
    TListItem* Item = lvVComs->Selected;
    TwclBluetoothRadio* Radio = (TwclBluetoothRadio*)(Item->Data);
    unsigned short Number = StrToInt(Item->SubItems->Strings[2]);

    int Res = Radio->DestroyComPort(Number);
    if (Res != WCL_E_SUCCESS)
      ShowError(Res);
    else
    {
      TraceEvent(Radio, "Destroy vCOM", "Number", IntToStr(Number));

      RefreshVComs();
    }
  }
}
private void btDestroyVCOM_Click(object sender, EventArgs e)
{
    if (lvComPorts.SelectedItems.Count == 0)
        MessageBox.Show("Select vCOM", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
    else
    {
        ListViewItem Item = lvComPorts.SelectedItems[0];
        wclBluetoothRadio Radio = (wclBluetoothRadio)Item.Tag;
        UInt16 Number = Convert.ToUInt16(Item.SubItems[3].Text);

        Int32 Res = Radio.DestroyComPort(Number);
        if (Res != wclErrors.WCL_E_SUCCESS)
            ShowError(Res);
        else
        {
            TraceEvent(Radio, "Destroy vCOM", "Number", Number.ToString());

            RefreshVComs();
        }
    }
}
Private Sub btDestroyVCOM_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btDestroyVCOM.Click
    If lvComPorts.SelectedItems.Count = 0 Then
        MessageBox.Show("Select vCOM", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
    Else
        Dim Item As ListViewItem = lvComPorts.SelectedItems(0)
        Dim Radio As wclBluetoothRadio = CType(Item.Tag, wclBluetoothRadio)
        Dim Number As UInt16 = Convert.ToUInt16(Item.SubItems(3).Text)

        Dim Res As Int32 = Radio.DestroyComPort(Number)
        If Res <> wclErrors.WCL_E_SUCCESS Then
            ShowError(Res)
        Else
            TraceEvent(Radio, "Destroy vCOM", "Number", Number.ToString())

            RefreshVComs()
        End If
    End If
End Sub
void CBluetoothManagerDlg::OnBnClickedButtonDestroyCom()
{
    POSITION Pos = lvComPorts.GetFirstSelectedItemPosition();
    if (Pos == NULL)
        AfxMessageBox(_T("Select vCOM"));
    else
    {
        int Item = lvComPorts.GetNextSelectedItem(Pos);
        CwclBluetoothRadio* Radio = (CwclBluetoothRadio*)lvComPorts.GetItemData(Item);
        unsigned short Number = LOWORD(StrToInt(lvComPorts.GetItemText(Item, 3)));

        int Res = Radio->DestroyComPort(Number);
        if (Res != WCL_E_SUCCESS)
            ShowError(Res);
        else
        {
            TraceEvent(Radio, _T("Destroy vCOM"), _T("Number"), IntToStr(Number));

            RefreshVComs();
        }
    }
}

The vCOM data model

Every entry in the list returned by EnumComPorts and by the ComPorts property is a TwclVirtualComPort structure (or a wclVirtualComPort in .NET). Three fields describe the binding:

  • Address — the Bluetooth device address, as a 48-bit integer. This is the same value that EnumRemoteServices uses when listing the device's SDP records, and the same value that RemotePair and RemoteDisconnect take.
  • Service — the UUID of the SPP service on that device. For SPP this is 0x1101, but the field is a full GUID because the same binding mechanism also works for vendor-specific serial services.
  • Number — the Windows COM port number, from 1 to 255. This is the value the application passes to CreateFile, and the value that DestroyComPort takes.

The BluetoothManager sample application displays these in four columns: the driver name (from Radio.ApiName), the address, the service UUID, and the port number. The number column is the one used by DestroyComPort; the address and the UUID are kept visible so the operator can tell which device a port belongs to when several devices are in range.


Practical notes

  • vCOM is Windows-specific. The framework's vCOM support relies on the Microsoft Bluetooth stack and its virtual serial port driver. On other platforms the same use case is handled through RFCOMM sockets directly, using the classes described in the Classic Bluetooth guide.
  • vCOM relies on SPP. If the remote device does not advertise SPP, creating a vCOM port has nothing to connect to. For BLE devices, use the GATT classes; for audio, use the audio classes described in the Bluetooth audio article; for OBEX, use the classes described in the Object Exchange guide.
  • Pairing is strongly recommended. A vCOM port can be created for an unpaired device in some configurations, but every new connection will then trigger a fresh authentication prompt — exactly the user-visible interruption that vCOM applications were designed to avoid. Pair the device first, with RemotePair, and the port will reconnect silently afterwards.
  • One port per (device, service) pair. Creating a second vCOM for the same pair while one already exists will fail with an error. The existing port should be reused, or destroyed first and then recreated.
  • Cleanup is important. A vCOM port survives the process that created it. If the application is uninstalled or moved, the port remains in the system and has to be removed explicitly with DestroyComPort. Applications that create vCOM ports should therefore track the numbers they created and remove them on shutdown.
  • Error codes are the standard WCL set. Every CreateComPort and DestroyComPort call returns WCL_E_SUCCESS on success and a specific error code otherwise — the same pattern used throughout the sample application and described in the error codes reference.

Frequently asked questions

What is a Bluetooth virtual COM port?
A Bluetooth virtual COM port (vCOM) is a Windows serial port that the Bluetooth Framework binds to a paired Bluetooth device and its Serial Port Profile (SPP) service. The application opens COMx and reads or writes a byte stream exactly as if a serial cable were attached; the framework routes those bytes over RFCOMM to the remote SPP device.
Why would I use vCOM instead of the Bluetooth API directly?
vCOM exists for legacy applications and for existing legacy code. Many applications — industrial, medical, retail, logistics — were written for a physical RS-232 port and cannot be rewritten. A virtual COM port lets the same binary keep working: it opens a COMx port, reads and writes a stream, and never needs to know that the transport is now Bluetooth. Existing serial parsers, state machines, and timing assumptions stay untouched.
Does creating a vCOM port pair the device?
No. CreateComPort only registers a virtual serial port in Windows and binds it to the given device address and SPP service UUID. Pairing is a separate operation that is performed with RemotePair or through the Windows Bluetooth UI.
Which profile does vCOM rely on?
vCOM relies on the Serial Port Profile (SPP), UUID 0x1101, running over RFCOMM. Devices that do not advertise SPP — BLE GATT services, audio profiles, object exchange — cannot be reached through a vCOM port and should use the framework's dedicated classes instead.
Can I use vCOM on non-Windows platforms?
No. vCOM is a Windows-specific feature that depends on the Microsoft Bluetooth stack and its virtual serial port driver. On other platforms the same use case is handled through RFCOMM sockets directly.
How do I remove a vCOM port?
Call DestroyComPort with the port number returned by CreateComPort, or with the Number field of an entry returned by EnumComPorts or ComPorts. The device address is not enough, because the same device may expose more than one SPP service, each with its own port.