Pythonで複数BACnetデバイスを仮想化!ポート競合回避とYABE一括検出の決定版

同一ホスト上で複数のBACnetデバイスをシミュレートしようとして、「Address already in use」やバインドエラーに直面していませんか?

標準的なBACnet/IP通信では1つのIPアドレスと1つのUDPポート(デフォルトは47808)しか使用できません。そのため、単純に複数のBACnetアプリケーションを起動しようとすると、OSがポートを競合してしまい、初期化エラーが発生します。

しかし、Pythonとbacpypes3ライブラリを活用した「仮想VLAN」構成を実装することで、この問題を解決し、YABE (Yet Another BACnet Explorer) などのツールから全ての仮想デバイスを自動検出させることが可能になります。

BACnet/IPにおける複数デバイス構成の課題と解決策

BACnet/IPの標準では、1つのIPアドレスにつき1つのUDPポート(47808)が割り当てられます。OSの制約上、このポートは通常、1つのプロセスにしかバインドできません。

このため、BAC0.liteなどのライブラリで複数のデバイスをシミュレートしようとすると、bacpypes3.comm.ConfigurationError: unbound serverRuntimeError: already boundといったエラーが発生します。

この課題を克服する鍵は、OSレベルのポートバインドを1つに集約し、アプリケーション内部で仮想的なネットワーク(VLAN)を構築してルーティングすることです。

仮想ネットワーク構成の具体的なメリット

  • リソース効率の向上:物理的なIPアドレスを複数用意する必要がなく、単一のホスト上で数千ものデバイスノードを効率的にシミュレートできます。
  • 運用の大幅な簡素化:YABEのようなBACnetクライアントツールからは、単一のIPアドレス(例:192.168.1.54)をスキャンするだけで、配下に存在する全ての仮想デバイスが自動的に検出・認識されます。
  • テストとデバッグの容易性:全ての仮想トラフィックがアプリケーション内で管理されるため、デバイス間の通信テストや問題の切り分けが格段に容易になります。

現場での注意点と実装のポイント

最もよくある失敗は、各仮想デバイスに対して個別に物理ネットワークアダプタへのバインドを試みることです。これは前述のエラーを引き起こします。

正しいアプローチは、1つのBIPSimpleApplication(IPスタック)をフロントエンドとして設定し、その背後に仮想ルーター機能を持つVLANを配置することです。

これにより、単一のIPアドレスとポートで複数のデバイスを多重化できます。

bacpypes3 を用いた実装例

import asyncio
from bacpypes3.app import Application
from bacpypes3.local.device import LocalDeviceObject
from bacpypes3.vlan import Network, Node
from bacpypes3.ipv4.app import BIPSimpleApplication

async def create_virtual_device(vlan_network, device_id, name):
    """仮想デバイスオブジェクトを作成し、VLANネットワークに追加する"""
    # LocalDeviceObjectの初期化(デバイスID、名前、ベンダーIDを指定)
    device_object = LocalDeviceObject(
        objectIdentifier=("device", device_id),
        objectName=name,
        vendorIdentifier=15 # 例: Vendor 15
    )
    # 仮想ネットワーク内でのユニークなアドレス(仮想MACアドレス)を割り当て
    node = Node(vlan_network.next_address())
    # アプリケーションインスタンスを作成し、ノードに紐付ける
    app = Application(device_object, node)
    print(f"✅ Virtual device '{name}' (ID: {device_id}) created.")
    return app

async def main():
    # 1. 仮想ネットワーク(VLAN)のインスタンスを作成
    vlan = Network()
    print("🌐 Virtual network (VLAN) created.")

    # 2. 物理ネットワークとのブリッジ(IPスタックの共有)
    # "192.168.1.54" の UDPポート 47808 を1つだけ確保します。
    try:
        main_app = BIPSimpleApplication.from_address("192.168.1.54/24")
        vlan.add_node(main_app.node)
        print(f"✅ Bound to main IP address: {main_app.localAddress}")
    except Exception as e:
        print(f"❌ Error binding to IP address: {e}")
        return

    # 3. 複数の仮想デバイスを作成し、VLANに追加
    # 各デバイスは固有のDevice IDを持ちます。
    await create_virtual_device(vlan, 1234, "Simulated-Device-1")
    await create_virtual_device(vlan, 5678, "Simulated-Device-2")
    await create_virtual_device(vlan, 9101, "Simulated-Device-3")

    print("\n🚀 All virtual devices are running!\nYou can now scan 192.168.1.54 with YABE or other BACnet explorers.")
    print("Press Ctrl+C to stop.")

    # プログラムを永続的に実行させるための待機
    await asyncio.Future()  

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n🛑 Server stopped by user.")

よくある質問 (FAQ)

Q: なぜ「Address already in use」エラーが発生するのですか?

A: 標準的なBACnet/IP通信では、1つのIPアドレスとUDPポート(通常47808)を複数のプロセスが同時に使用することはできません。各プロセスが個別にポートへのバインドを試みるために競合が発生します。
この解決策は、OSレベルでのポートバインドを1つに集約し、アプリケーション内部で仮想的なネットワーク(VLAN)を管理することです。

Q: YABEなどのツールでデバイスを検出するにはどうすればよいですか?

A: 上記のPythonスクリプトを実行した後、YABEなどのBACnetエクスプローラーツールを開き、 192.168.1.54 (スクリプトで指定したIPアドレス)に対してネットワークスキャンを実行してください。
仮想化された全てのデバイスが、あたかも個別の物理デバイスのようにリストアップされます。

結論

Python 3.13とbacpypes3ライブラリを組み合わせることで、単一のIPアドレスとポートで複数のBACnetデバイスを効率的にシミュレートできます。

この仮想VLAN構成は、OSレベルのポート競合問題を回避し、YABEなどのツールによるデバイスの一括検出を可能にします。これにより、BACnetデバイスの開発、テスト、デバッグにおける環境構築が大幅に簡素化され、リソース効率も向上します。

BACnetシステムの開発者やテスターにとって、このテクニックは非常に価値のあるものとなるでしょう。

タイトルとURLをコピーしました