SPP(Serial Port Profile)によるBluetooth通信を行う際に デバイス名からCOMポート番号を取得する

# はじめに

PCと自作デバイス間などで、Bluetoothを使った無線通信をする場合は、SPPを利用して
従来の有線のような感覚でシリアル通信を行うのが簡単です。

SPPはBluetooth搭載のデバイスをWindowsとペアリングすると、仮想COMポートが割り振られ、このCOMポートに対してプログラムやターミナルからアクセスできるというものです。

しかし実際にやってみると、対象のCOMポートがどれなのかすぐにはわかりません。
デバイスマネージャーで見ると「BluetoothデバイスのCOMポート」みたいなざっくりとした名前で複数個並んでいたりします。
これらのどれかが目的のポートなので、接続できるまで試してみればいいのですが、めんどうくさいですし
ユーザーにそんなことをさせてはいけません。開発者自身もやりたくありません。
そこで、設定したデバイス名から、このCOMポート番号を求める方法について書きます。
デバイス名さえ決まっていれば、実行時動的にCOMポートが判明するので、手動での設定が必要なくなります。  

ちなみに、C#での実現方法は下記リンク先が詳しいです。
https://www.softech.co.jp/mm_170705_tr.htm

ここでは、C++での実現方法をご紹介します。  


# 手順

### レジストリから、デバイス名をキーにしてフォルダ名を取得
レジストリを検索します。COMデバイスが登録されているところは、次の

```
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\BTHPORT\Parameters\Devices
```

になります。ここのサブキーのパラメータNameに、デバイス名が入っているので、目的のデバイス名を探します。

見つかったら、ここのフォルダパスの一部が次のCOMポートを探すためのキー（デバイスインスタンスID）となるので保持しておきます。

### デバイスインスタンスIDをキーとしてデバイス情報を探す
SetupDiGetClassDevs()にGUID_DEVINTERFACE_COMPORTを指定してハンドルを取得し、
SetupDiEnumDeviceInfo()で、デバイス情報の列挙ができます。

SetupDiGetDeviceInstanceId()で、デバイスインスタンスIDが得られるので、先程のものを探します。
見つかったら、SetupDiEnumDeviceInfo()のときに第二引数にセットされてくる deviceInfoData をおぼえておきます。

### レジストリからCOMポートを取得
先程のdeviceInfoDataを、SetupDiOpenDevRegKey()に食わせてあげるとレジストリのキーを返してくれます。
この場所のパラメータ"PortName"に、目的のCOM番号があります。やっと見つけました！  


# 説明はいいからコードを

サンプルコードです。

SearchPort( const std::string& searchname, std::string& portname )

第一引数に、デバイス名を入れてあげると、第二引数でCOMポート名を返します。
※一部、安全でないコードも含まれるので各自修正してください。

```c++

#include <setupapi.h> // Setupapi.lib

// 指定した名前のポート名を見つける
bool SearchPort( const std::string& searchname, std::string& portname )
{
  // COMデバイスが登録されているところ
  const std::string findKey = "SYSTEM\\CurrentControlSet\\services\\BTHPORT\\Parameters\\Devices";

  DWORD dwResult;

  std::string strFolderID;

  ////// レジストリから指定した名前を持つポートのフォルダ名を取得する

  // フォルダキー（サブキー）を開く
  HKEY hFolderKey;
  dwResult = RegOpenKeyExA( HKEY_LOCAL_MACHINE, findKey.c_str(), 0, KEY_READ, &hFolderKey );
  if ( dwResult != ERROR_SUCCESS ) {
    return false;
  }

  FILETIME ftLastWriteTime;
  char folder[1024];

  // フォルダーの列挙
  for ( DWORD dwIndex = 0; ; dwIndex++ ) {
    DWORD folderSz = sizeof( folder ) / sizeof( char );
    // 列挙
    dwResult = RegEnumKeyExA( hFolderKey, dwIndex, folder, &folderSz, NULL, NULL, NULL, &ftLastWriteTime );
    if ( dwResult == ERROR_NO_MORE_ITEMS ) break;
    if ( dwResult != ERROR_SUCCESS ) continue;

    // 取得された→folder

    // その下のNameエンティティを調べる
    char namePath[1024];
    DWORD namePathSz = sizeof( namePath ) / sizeof( char );

    strcpy_s( namePath, namePathSz, folder );

    HKEY hNameEntKey;
    dwResult = RegOpenKeyExA( hFolderKey, namePath, 0, KEY_READ, &hNameEntKey );
    if ( dwResult != ERROR_SUCCESS ) {
      continue;
    }

    DWORD dwType;
    char lpData[1024];
    DWORD dwDataSize = sizeof( lpData ) / sizeof( char );
    dwResult = RegQueryValueEx( hNameEntKey, TEXT( "Name" ), 0, &dwType, (LPBYTE)lpData, &dwDataSize );
    RegCloseKey( hNameEntKey );

    if ( dwResult != ERROR_SUCCESS ) {
      continue;
    }
    lpData[dwDataSize] = '\0';

    // 値のチェック
    if ( strcmp( lpData, searchname.c_str() ) == 0 ) { /// ここ文字数見たほうがいいかも TODO:
      char searchID[512];
      sprintf_s( searchID, 512, "&%s_", folder );
      strFolderID = std::string( searchID );
      std::transform( strFolderID.begin(), strFolderID.end(), strFolderID.begin(), std::toupper );
      break;
    }
  }
  RegCloseKey( hFolderKey );

  if ( strFolderID == "" ) return false;

  ////// デバイス情報から、フォルダ名をキーにCOMポート名をひっぱる

  // デバイス情報取得 COMPORT
  HDEVINFO hDevInfo = SetupDiGetClassDevs( &GUID_DEVINTERFACE_COMPORT, NULL, NULL, ( DIGCF_PRESENT | DIGCF_DEVICEINTERFACE ) );

  SP_DEVINFO_DATA deviceInfoData = { sizeof( SP_DEVINFO_DATA ) };
  for ( DWORD i = 0; SetupDiEnumDeviceInfo( hDevInfo, i, &deviceInfoData ); ++i ) {
    char buffer[512];
    DWORD buflen = 0;
    SetupDiGetDeviceInstanceIdA( hDevInfo, &deviceInfoData, (char*)buffer, sizeof( buffer ), &buflen );
    std::string strBuffer = std::string( buffer );
    if ( strBuffer.find( strFolderID ) == std::string::npos ) continue;

    // 発見
    //// COMポート番号を取得
    HKEY tmp_key = SetupDiOpenDevRegKey( hDevInfo, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE );
    if ( !tmp_key ) {
      return false;
    }
    char name[256];
    DWORD type = 0;
    DWORD size = sizeof( name );
    RegQueryValueExA( tmp_key, "PortName", NULL, &type, (LPBYTE)name, &size );
    portname = std::string( name );
    return true;
  }
  return false;
}
```


# おわりに
これで、ペアリングされたBluetoothデバイスとすぐにシリアル通信ができるようになりました。
お疲れ様でした。