はじめに (対象読者・この記事でわかること)
この記事は、Bluetooth対応デバイスと連携するアプリケーションの開発者、特にNode.jsやPythonでシリアル通信を扱うエンジニアの方を対象としています。工場ラインのIoT化、スマートホームデバイスの制御、組み込みシステムとの連携など、Bluetoothシリアルポート(SPP)を利用したシステム構築を検討している方にとって役立つ情報を提供します。
この記事を読むことで、一般的なCOMポート名(例: WindowsのCOM3、Linuxの/dev/rfcomm0)だけでは判断しづらいBluetoothデバイスの「フレンドリー名」をプログラムから取得する方法がわかります。これにより、ユーザーインターフェース上でわかりやすいデバイス名を表示したり、特定のデバイスを識別して接続したりする実装が可能になります。単なるポート名ではなく、"MySensor01"や"SmartPrinter"といった具体的なデバイス名を扱えるようになるでしょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 * Node.jsまたはPythonの基本的なプログラミング知識 * シリアル通信(COMポート、UARTなど)の基本的な概念と利用経験 * BluetoothデバイスのペアリングとSPPプロファイルでの接続経験(PCとBluetoothデバイスがペアリング済みであることが前提です)
Bluetoothシリアルポートとデバイス名識別の重要性
Bluetoothシリアルポートプロファイル(SPP: Serial Port Profile)は、Bluetoothを介して仮想的なシリアルポート接続を提供する標準規格です。これにより、従来のRS-232Cシリアルケーブルを使っていた機器が無線化され、様々なデバイス間でのデータ通信が可能になります。例えば、PCとBluetooth対応の組み込みボード、測定機器、POSプリンターなどがSPPを使って接続されます。
しかし、これらのBluetoothシリアルポートは、OS上では一般的なシリアルポートとして認識されます。WindowsではCOMx(例: COM3、COM4)、Linuxでは/dev/ttySxや/dev/rfcommxといったデバイスパスが割り当てられます。複数のBluetoothデバイスを接続した場合、これらのポート名だけでは「どのポートがどのデバイスに対応しているのか」をユーザーが直感的に判断するのは困難です。例えば、COM3が温度センサーでCOM4が湿度センサーなのか、あるいはどちらも同じ種類のセンサーなのかを区別できません。
そこで重要になるのが、Bluetoothデバイスが持つ「フレンドリー名(Friendly Name)」です。これは、デバイスを識別するための人間が理解しやすい名前で、例えば"ESP32-TempSensor"や"Arduino-BTPrinter"のような名称です。このフレンドリー名をプログラムから取得し、ユーザーインターフェースに表示したり、特定のデバイスを識別するロジックに組み込んだりすることで、アプリケーションの利便性と信頼性が大幅に向上します。本記事では、このフレンドリー名を取得するための具体的な方法に焦点を当てて解説します。
プログラムからBluetoothデバイス名を取得する具体的な方法
Bluetoothシリアルポートのデバイス名を取得する方法は、OSによってアプローチが異なります。ここでは、WindowsとLinuxのそれぞれで、PythonとNode.jsを使った具体的な実装例を見ていきましょう。
基本的なアプローチとしては、まず利用可能なシリアルポートを列挙し、次にOS固有のAPIやコマンドを利用して、列挙されたポートに関連付けられたBluetoothデバイスのフレンドリー名を取得します。
Windowsでの取得方法
Windowsでは、Bluetoothシリアルポートは通常のCOMポートとして扱われます。デバイスマネージャーで見ると、"Bluetooth Link on COMx"のように表示され、その詳細プロパティにはBluetoothデバイス名が含まれています。
Pythonでの実装例
Pythonではpyserialライブラリのserial.tools.list_portsモジュールを使ってCOMポートを列挙し、そのdescription属性からBluetoothデバイス名を取得できる場合があります。
Pythonimport serial.tools.list_ports def get_bluetooth_com_ports_windows(): """ Windows上でBluetoothシリアルポートのデバイス名を取得します。 """ bluetooth_devices = [] ports = serial.tools.list_ports.comports() print("--- 利用可能なシリアルポート一覧 ---") for port in ports: print(f"Path: {port.device}, Description: {port.description}, HWID: {port.hwid}") # Descriptionに"Bluetooth"または"BT"が含まれるものをBluetoothシリアルポートと仮定 if "bluetooth" in port.description.lower() or "bt" in port.description.lower(): # Descriptionからフレンドリー名を抽出する(例:`Bluetooth リンク上の標準シリアル (COMx)`から`Bluetooth リンク`を取得) # これはあくまでDescriptionの文字列に依存するため、環境によって調整が必要です。 # 通常、COMポートのDescriptionがデバイス名そのものか、それを特定できる情報を含んでいます。 friendly_name = port.description.split(" (COM")[0].strip() if friendly_name.endswith("上の標準シリアル"): # 日本語環境での調整 friendly_name = friendly_name.replace("上の標準シリアル", "").strip() bluetooth_devices.append({ "port": port.device, "friendly_name": friendly_name if friendly_name else "Unknown Bluetooth Device" }) print("-----------------------------------") return bluetooth_devices if __name__ == "__main__": # pyserialがインストールされていない場合は 'pip install pyserial' でインストールしてください。 devices = get_bluetooth_com_ports_windows() if devices: print("\n--- 検出されたBluetoothシリアルポート ---") for dev in devices: print(f"COM Port: {dev['port']}, Device Name: {dev['friendly_name']}") else: print("\nBluetoothシリアルポートは検出されませんでした。")
解説:
serial.tools.list_ports.comports() は、システム上のすべてのシリアルポートに関する情報(デバイスパス、説明、ハードウェアIDなど)を提供します。Windowsの場合、Bluetoothシリアルポートのdescriptionには「Bluetooth」というキーワードや、そのデバイスのフレンドリー名が含まれることが多いです。上記のコードでは、descriptionをパースしてフレンドリー名を取得しています。
Node.jsでの実装例
Node.jsではserialportライブラリを使用します。このライブラリも、Windows環境でシリアルポートのfriendlyNameなどの情報を提供することがあります。より確実に情報を取得するには、PowerShellコマンドをchild_processモジュールで実行し、結果をパースする方法が有効です。
Javascriptconst { SerialPort } = require('serialport'); const { exec } = require('child_process'); async function getBluetoothComPortsWindows() { const bluetoothDevices = []; console.log("--- 利用可能なシリアルポート一覧 ---"); const ports = await SerialPort.list(); ports.forEach(port => { console.log(`Path: ${port.path}, PnP ID: ${port.pnpId}, Manufacturer: ${port.manufacturer}, Friendly Name: ${port.friendlyName || 'N/A'}`); }); console.log("-----------------------------------"); // PowerShellを使ってBluetoothシリアルポートの情報を取得 // Get-PnpDevice -Class Ports はCOMポートなどのデバイス情報を取得 // Where-Object で Bluetooth関連のポートをフィルタリング // Select-Object FriendlyName, Name, InstanceId で必要な情報を抽出 const powershellCommand = `Get-PnpDevice -Class Ports | Where-Object { $_.FriendlyName -like "*Bluetooth*" } | Select-Object FriendlyName, Name, InstanceId | ConvertTo-Json`; return new Promise((resolve, reject) => { exec(powershellCommand, { 'shell': 'powershell.exe' }, (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); return reject(error); } if (stderr) { console.error(`stderr: ${stderr}`); // エラーではないが、情報出力の場合もあるので続行 } try { const pnpDevices = JSON.parse(stdout); if (Array.isArray(pnpDevices)) { pnpDevices.forEach(device => { // FriendlyNameからCOMポート番号を抽出(例: "Bluetooth リンク上の標準シリアル (COM3)" から "COM3") const comMatch = device.FriendlyName.match(/\((COM\d+)\)/); if (comMatch && comMatch[1]) { const comPort = comMatch[1]; const friendlyName = device.FriendlyName.replace(/\s*\((COM\d+)\)/, '').trim(); bluetoothDevices.push({ port: comPort, friendly_name: friendlyName }); } }); } else if (pnpDevices) { // 単一オブジェクトの場合 const comMatch = pnpDevices.FriendlyName.match(/\((COM\d+)\)/); if (comMatch && comMatch[1]) { const comPort = comMatch[1]; const friendlyName = pnpDevices.FriendlyName.replace(/\s*\((COM\d+)\)/, '').trim(); bluetoothDevices.push({ port: comPort, friendly_name: friendlyName }); } } resolve(bluetoothDevices); } catch (jsonError) { console.error(`JSON parsing error: ${jsonError}`); resolve([]); // パースエラーの場合は空配列を返す } }); }); } if (require.main === module) { // serialportがインストールされていない場合は 'npm install serialport' でインストールしてください。 getBluetoothComPortsWindows().then(devices => { if (devices.length > 0) { console.log("\n--- 検出されたBluetoothシリアルポート ---"); devices.forEach(dev => { console.log(`COM Port: ${dev.port}, Device Name: ${dev.friendly_name}`); }); } else { console.log("\nBluetoothシリアルポートは検出されませんでした。"); } }).catch(err => { console.error("Bluetoothシリアルポートの取得中にエラーが発生しました:", err); }); }
解説:
serialport.list() は、Node.jsで利用可能なシリアルポートのリストを返しますが、提供される情報(pnpIdやmanufacturerなど)だけではデバイスのフレンドリー名に直接結びつかない場合があります。
そこで、child_process.execを使ってPowerShellコマンドを実行し、より詳細なデバイス情報を取得します。Get-PnpDevice -Class Portsでポートデバイスを列挙し、FriendlyNameに"Bluetooth"を含むものをフィルタリングして、JSON形式で出力させています。このJSONをパースすることで、COMポート番号とフレンドリー名を関連付けます。
Linuxでの取得方法
Linuxでは、Bluetoothシリアルポートは通常/dev/rfcommXのようなデバイスパスで表されます。これらのパスとBluetoothデバイスのフレンドリー名を関連付けるには、BlueZユーティリティ(bluetoothctlやrfcomm)の情報を利用するのが一般的です。
Pythonでの実装例
Pythonではpyserialで/dev/rfcommXポートを列挙し、subprocessモジュールを使ってbluetoothctlやrfcommコマンドの出力をパースします。
Pythonimport serial.tools.list_ports import subprocess import re def get_bluetooth_com_ports_linux(): """ Linux上でBluetoothシリアルポートのデバイス名を取得します。 """ bluetooth_devices = [] # 1. /dev/rfcommX ポートを列挙 rfcomm_ports = [] ports = serial.tools.list_ports.comports() print("--- 利用可能なシリアルポート一覧 ---") for port in ports: print(f"Path: {port.device}, Description: {port.description}, HWID: {port.hwid}") if "rfcomm" in port.device.lower(): rfcomm_ports.append(port.device) print("-----------------------------------") if not rfcomm_ports: return [] # 2. rfcomm show コマンドで rfcommX と MACアドレスの対応を取得 rfcomm_mac_map = {} try: # root権限が必要な場合があるため、sudoを付けます。 # 環境によっては'sudo'が不要、またはパスワード入力が必要になることがあります。 output = subprocess.check_output(["sudo", "rfcomm", "show"], text=True) for line in output.splitlines(): mac_match = re.search(r'rfcomm(\d+):\s+([\da-fA-F:]+)\s+channel\s+\d+\s+(.+)$', line) if mac_match: device_path = f"/dev/rfcomm{mac_match.group(1)}" mac_address = mac_match.group(2).upper() rfcomm_mac_map[device_path] = mac_address except subprocess.CalledProcessError as e: print(f"rfcomm show コマンドの実行に失敗しました: {e}") print("rfcommコマンドがインストールされているか、またはsudo権限があるか確認してください。") return [] except FileNotFoundError: print("rfcommコマンドが見つかりません。bluezユーティリティがインストールされているか確認してください。") return [] # 3. bluetoothctl devices コマンドで MACアドレスとデバイス名の対応を取得 mac_name_map = {} try: output = subprocess.check_output(["bluetoothctl", "devices"], text=True) for line in output.splitlines(): # Device XX:XX:XX:XX:XX:XX DeviceName device_match = re.search(r'Device\s+([\da-fA-F:]+)\s+(.+)', line) if device_match: mac_address = device_match.group(1).upper() device_name = device_match.group(2).strip() mac_name_map[mac_address] = device_name except subprocess.CalledProcessError as e: print(f"bluetoothctl devices コマンドの実行に失敗しました: {e}") print("bluetoothctlがインストールされているか確認してください。") return [] except FileNotFoundError: print("bluetoothctlコマンドが見つかりません。bluezユーティリティがインストールされているか確認してください。") return [] # 4. 取得した情報を組み合わせてBluetoothデバイスリストを作成 for port_path in rfcomm_ports: mac_address = rfcomm_mac_map.get(port_path) if mac_address: friendly_name = mac_name_map.get(mac_address, f"Unknown Device ({mac_address})") bluetooth_devices.append({ "port": port_path, "friendly_name": friendly_name }) return bluetooth_devices if __name__ == "__main__": # pyserialがインストールされていない場合は 'pip install pyserial' でインストールしてください。 # Linux環境では 'sudo apt-get install bluez' などでbluezユーティリティをインストールしてください。 devices = get_bluetooth_com_ports_linux() if devices: print("\n--- 検出されたBluetoothシリアルポート ---") for dev in devices: print(f"COM Port: {dev['port']}, Device Name: {dev['friendly_name']}") else: print("\nBluetoothシリアルポートは検出されませんでした。") print("\n注意: rfcommコマンドの実行には通常sudo権限が必要です。")
解説:
1. /dev/rfcommXの列挙: pyserial.tools.list_ports.comports() でシステム上のシリアルポートを列挙し、/dev/rfcommで始まるパスを持つポートを特定します。
2. rfcomm showでMACアドレスを取得: rfcomm showコマンドを実行し、/dev/rfcommXポートに対応するBluetoothデバイスのMACアドレスを取得します。このコマンドは通常、sudo権限が必要です。
3. bluetoothctl devicesでデバイス名を取得: bluetoothctl devicesコマンドを実行し、ペアリング済みのBluetoothデバイスのMACアドレスとそのフレンドリー名の一覧を取得します。
4. 情報の結合: 1で取得したポートパス、2で取得したMACアドレス、3で取得したフレンドリー名を組み合わせて、目的の情報を得ます。
Node.jsでの実装例
Node.jsでも、Pythonと同様にserialportで/dev/rfcommXポートを列挙し、child_processでbluetoothctlやrfcommコマンドの出力をパースします。
Javascriptconst { SerialPort } = require('serialport'); const { exec } = require('child_process'); async function getBluetoothComPortsLinux() { const bluetoothDevices = []; // 1. /dev/rfcommX ポートを列挙 const rfcommPorts = []; console.log("--- 利用可能なシリアルポート一覧 ---"); const ports = await SerialPort.list(); ports.forEach(port => { console.log(`Path: ${port.path}, PnP ID: ${port.pnpId || 'N/A'}, Manufacturer: ${port.manufacturer || 'N/A'}`); if (port.path.includes('/dev/rfcomm')) { rfcommPorts.push(port.path); } }); console.log("-----------------------------------"); if (rfcommPorts.length === 0) { return []; } // 2. rfcomm show コマンドで rfcommX と MACアドレスの対応を取得 const rfcommMacMap = {}; try { const { stdout: rfcommShowOutput } = await execCommand('sudo rfcomm show'); rfcommShowOutput.split('\n').forEach(line => { const macMatch = line.match(/rfcomm(\d+):\s+([\da-fA-F:]+)\s+channel\s+\d+\s+(.+)$/); if (macMatch) { const devicePath = `/dev/rfcomm${macMatch[1]}`; const macAddress = macMatch[2].toUpperCase(); rfcommMacMap[devicePath] = macAddress; } }); } catch (e) { console.error(`rfcomm show コマンドの実行に失敗しました: ${e.message}`); console.error("rfcommコマンドがインストールされているか、またはsudo権限があるか確認してください。"); return []; } // 3. bluetoothctl devices コマンドで MACアドレスとデバイス名の対応を取得 const macNameMap = {}; try { const { stdout: bluetoothctlOutput } = await execCommand('bluetoothctl devices'); bluetoothctlOutput.split('\n').forEach(line => { const deviceMatch = line.match(/Device\s+([\da-fA-F:]+)\s+(.+)/); if (deviceMatch) { const macAddress = deviceMatch[1].toUpperCase(); const deviceName = deviceMatch[2].trim(); macNameMap[macAddress] = deviceName; } }); } catch (e) { console.error(`bluetoothctl devices コマンドの実行に失敗しました: ${e.message}`); console.error("bluetoothctlがインストールされているか確認してください。"); return []; } // 4. 取得した情報を組み合わせてBluetoothデバイスリストを作成 for (const portPath of rfcommPorts) { const macAddress = rfcommMacMap[portPath]; if (macAddress) { const friendlyName = macNameMap[macAddress] || `Unknown Device (${macAddress})`; bluetoothDevices.push({ port: portPath, friendly_name: friendlyName }); } } return bluetoothDevices; } // execのPromiseラッパー function execCommand(command) { return new Promise((resolve, reject) => { exec(command, (error, stdout, stderr) => { if (error) { return reject(error); } if (stderr) { console.warn(`stderr: ${stderr}`); // 警告として出力し、続行 } resolve({ stdout, stderr }); }); }); } if (require.main === module) { // serialportがインストールされていない場合は 'npm install serialport' でインストールしてください。 // Linux環境では 'sudo apt-get install bluez' などでbluezユーティリティをインストールしてください。 getBluetoothComPortsLinux().then(devices => { if (devices.length > 0) { console.log("\n--- 検出されたBluetoothシリアルポート ---"); devices.forEach(dev => { console.log(`COM Port: ${dev.port}, Device Name: ${dev.friendly_name}`); }); } else { console.log("\nBluetoothシリアルポートは検出されませんでした。"); } }).catch(err => { console.error("Bluetoothシリアルポートの取得中にエラーが発生しました:", err); }); console.log("\n注意: rfcommコマンドの実行には通常sudo権限が必要です。"); }
解説:
Node.jsでも、child_process.execを使用してrfcomm showとbluetoothctl devicesコマンドを実行し、その出力を正規表現でパースしています。非同期処理の管理のためにasync/awaitとPromiseでexecCommandをラップしています。
ハマった点やエラー解決
Bluetoothシリアルポートのデバイス名取得では、以下のような点に注意が必要です。
- OS間の互換性: WindowsとLinuxではBluetoothスタックやAPIが異なるため、コードも大きく異なります。クロスプラットフォームで動作する汎用的なライブラリは限られており、多くの場合OS固有の実装が必要です。
- 管理者権限:
rfcommコマンドの実行や、WindowsのCOMポート情報への詳細なアクセスには、管理者権限(sudoなど)が必要な場合があります。スクリプトを実行する際には、適切な権限で実行されているか確認してください。 - Bluetoothデバイスの状態: ターゲットのBluetoothデバイスがPCとペアリング済みであること、そしてSPPプロファイルが有効になっていることが前提です。デバイスが接続されていない場合や、Bluetoothアダプターが無効な場合は検出されません。
- コマンド出力のパース:
bluetoothctlやrfcommのようなコマンドラインツールの出力は、バージョンやOSの locale によってフォーマットが変わる可能性があります。堅牢なパースロジック(正規表現など)が必要ですが、それでも将来的な変更には対応しきれない場合もあります。 pyserial/node-serialportの情報の限界: これらのライブラリはシリアルポートの列挙には優れていますが、Bluetoothデバイスの詳細なフレンドリー名を直接提供するわけではありません。OS固有のシステム情報と連携して初めて、目的の情報を取得できます。- 特定のBluetoothスタック: LinuxではBlueZが一般的ですが、他のBluetoothスタックが使われている場合は異なるコマンドやAPIが必要になることがあります。
解決策
- プラットフォームごとの実装: ターゲットとするOSに合わせてコードを分岐させ、それぞれのOSで最も適したAPIやコマンドを利用する。
- 堅牢なエラーハンドリング: コマンドの実行失敗、出力のパースエラー、デバイス未検出などの状況を想定し、適切なエラー処理やフォールバックを用意する。
- 管理者権限の明示: 必要であれば、スクリプトの実行前に管理者権限が必要であることをユーザーに伝えたり、sudoコマンドを利用したりする。
- コマンド出力の安定性: 正規表現は強力ですが、コマンド出力のフォーマットが変更されると壊れる可能性があります。可能であれば、より安定したプログラマティックなAPI(例: Pythonの
bluetoothモジュールなど)の利用も検討する。しかし、これらのモジュールもOS固有の依存関係が多いことに注意が必要です。 - ユーザーへの情報提供: デバイスが検出されない場合に、Bluetoothのペアリング状況やSPPプロファイルの有効性、管理者権限の確認などを促すメッセージを表示することで、デバッグの助けになります。
まとめ
本記事では、Bluetoothで接続されたシリアルポートデバイスのフレンドリー名を、PythonとNode.jsを使ってプログラムから取得する方法 を解説しました。
- OSごとのアプローチ: Windowsでは
pyserialのdescriptionやPowerShellコマンド、Linuxではrfcommとbluetoothctlコマンドの組み合わせで情報を取得する必要があります。 - ライブラリとコマンドの連携:
pyserialやnode-serialportでシリアルポートを列挙し、OS固有のコマンドラインツール(PowerShell, bluetoothctl, rfcomm)の出力をパースすることで、目的のフレンドリー名を特定しました。 - 実装上の注意点: 管理者権限、Bluetoothデバイスの状態、コマンド出力の変動性など、いくつかの「ハマりどころ」とその解決策についても触れました。
この記事を通して、シリアルポート名だけではわかりづらかったBluetoothデバイスを、ユーザーが直感的に理解できるフレンドリー名で識別し、アプリケーション開発における利便性と堅牢性を高めることができるようになったことと思います。
今後は、取得したデバイス名を利用して特定のデバイスに自動接続する方法や、Bluetooth Low Energy (BLE) デバイスのフレンドリー名取得、クロスプラットフォームに対応するための抽象化ライブラリの検討など、発展的な内容についても記事にする予定です。
参考資料
- pyserial 公式ドキュメント
- serialport (Node.js) 公式ドキュメント
- Microsoft Docs: Get-PnpDevice
- BlueZ (Linux Bluetooth stack) 公式サイト
- ArchWiki: Bluetooth (rfcomm, bluetoothctlなどの解説)
