ソースを参照

新增bacnet whois

Lu Xianghui 1 ヶ月 前
コミット
54c2fff4e8

+ 4 - 0
README.md

@@ -8,6 +8,7 @@
 - `modbus.point_collect_test`
 - `bacnet.point_collect_test`
 - `bacnet.point_search`
+- `bacnet.bbmd_whois`
 - `collector.modbus_device_create`
 - `collector.modbus_point_create`
 - `collector.s7_device_create`
@@ -213,11 +214,14 @@ BACnet 点位创建调用 `{data_collector_base_url}/api/collector/bacnet/point/
 - `points[].object_type`:BACnet 对象类型,如 `AnalogInput`、`analog-input`、`analogInput`;MCP 会统一规范为 `AnalogInput` 这类格式后再读取
 - `points[].object_id`:BACnet 对象实例号,范围 `0..4194303`
 
+`bacnet.bbmd_whois` 用于通过 BBMD 执行 BACnet/IP Who-Is 设备发现,调用 `{base_url}/api/dc-gateway/bacnet/bbmd/whois`。参数只有 `project_key`,MCP 不发送请求体。BBMD 地址、端口、TTL、Who-Is 等待超时、设备号范围和本机绑定地址由采集网关环境变量配置,包括 `BACNET_BBMD_IP`、`BACNET_BBMD_PORT`、`BACNET_BBMD_TTL`、`BACNET_BBMD_WHOIS_TIMEOUT`、`BACNET_BBMD_LOW_LIMIT`、`BACNET_BBMD_HIGH_LIMIT`、`BACNET_BBMD_LOCAL_DEVICE_ID`、`BACNET_LOCAL_IP`、`BACNET_LOCAL_PORT`。响应透传网关 JSON,`code=0` 表示成功,`data.bbmd` 为本次 BBMD 请求配置,`data.devices[]` 为发现的 `I-Am` 设备列表,常见字段包括 `bacnet_device_id`、`ip`、`port`、`max_apdu`、`segmentation`、`vendor_id`。
+
 调用示例:
 
 ```powershell
 & ".venv\Scripts\python.exe" tools\test_mcp_call.py --tool bacnet.point_search --args '{"project_key":"dev-01","ip":"192.168.75.240","bacnet_device_id":12345}'
 & ".venv\Scripts\python.exe" tools\test_mcp_call.py --tool bacnet.point_collect_test --args '{"project_key":"dev-01","ip":"192.168.75.240","bacnet_device_id":12345,"points":[{"object_type":"AnalogInput","object_id":1}]}'
+& ".venv\Scripts\python.exe" tools\test_mcp_call.py --tool bacnet.bbmd_whois --args '{"project_key":"dev-01"}'
 ```
 
 调用其他 MCP URL:

+ 20 - 0
data_collector_mcp/bacnet_server.py

@@ -11,6 +11,7 @@ from .collector_api import (
     edit_bacnet_point as api_edit_bacnet_point,
 )
 from .gateway_api import (
+    bacnet_bbmd_whois as api_bacnet_bbmd_whois,
     bacnet_point_collect_test as api_bacnet_point_collect_test,
     bacnet_point_search as api_bacnet_point_search,
 )
@@ -109,6 +110,25 @@ def bacnet_point_search(
     )
 
 
+@mcp.tool(
+    name="bacnet.bbmd_whois",
+    description=(
+        "通过采集网关执行 BACnet/IP BBMD Who-Is 设备发现。调用 "
+        "{base_url}/api/dc-gateway/bacnet/bbmd/whois,MCP 不发送请求体;"
+        "BBMD 地址、端口、TTL、Who-Is 等待超时、设备号范围、本机绑定地址等参数由采集网关环境变量配置,"
+        "包括 BACNET_BBMD_IP、BACNET_BBMD_PORT、BACNET_BBMD_TTL、"
+        "BACNET_BBMD_WHOIS_TIMEOUT、BACNET_BBMD_LOW_LIMIT、BACNET_BBMD_HIGH_LIMIT、"
+        "BACNET_BBMD_LOCAL_DEVICE_ID、BACNET_LOCAL_IP、BACNET_LOCAL_PORT。"
+        "网关会注册 BBMD Foreign Device,分发 Who-Is,收集 I-Am 响应并在结束前注销;"
+        "响应 data.bbmd 为本次 BBMD 请求配置,data.devices[] 为发现的 BACnet 设备,"
+        "常见字段包括 bacnet_device_id、ip、port、max_apdu、segmentation、vendor_id;"
+        "响应透传上游 JSON,code=0 表示业务成功。"
+    ),
+)
+def bacnet_bbmd_whois(project_key: str) -> dict[str, Any]:
+    return api_bacnet_bbmd_whois(project_key)
+
+
 @mcp.tool(
     name="collector.bacnet_device_create",
     description=(

+ 5 - 1
data_collector_mcp/gateway_api.py

@@ -8,7 +8,7 @@ from .protocols import BACNET_SPEC, MODBUS_SPEC, S7_SPEC
 from .protocols.bacnet import normalize_bacnet_object_type
 
 
-def _request_gateway(project_key: str, path: str | None, payload: dict[str, Any], protocol: str) -> dict[str, Any]:
+def _request_gateway(project_key: str, path: str | None, payload: dict[str, Any] | None, protocol: str) -> dict[str, Any]:
     project = find_project_config(project_key)
     if not path:
         raise ValueError(f"{protocol} gateway path is not configured")
@@ -83,6 +83,10 @@ def bacnet_point_search(
     return _request_gateway(project_key, BACNET_SPEC.search_points_path, payload, "bacnet point search")
 
 
+def bacnet_bbmd_whois(project_key: str) -> dict[str, Any]:
+    return _request_gateway(project_key, BACNET_SPEC.bbmd_whois_path, None, "bacnet bbmd whois")
+
+
 def s7_raw_read(
     project_key: str,
     *,

+ 1 - 0
data_collector_mcp/protocols/bacnet.py

@@ -32,6 +32,7 @@ BACNET_SPEC = ProtocolSpec(
     create_point_path="/api/collector/bacnet/point/add_collect_point",
     point_test_path="/api/dc-gateway/bacnet/read_points",
     search_points_path="/api/dc-gateway/bacnet/search_points",
+    bbmd_whois_path="/api/dc-gateway/bacnet/bbmd/whois",
     device_defaults={
         "type": "bacnet",
         "device_type": 1,

+ 1 - 0
data_collector_mcp/protocols/base.py

@@ -13,5 +13,6 @@ class ProtocolSpec:
     raw_read_path: str | None = None
     connect_scan_path: str | None = None
     search_points_path: str | None = None
+    bbmd_whois_path: str | None = None
     device_defaults: dict[str, Any] = field(default_factory=dict)
     point_defaults: dict[str, Any] = field(default_factory=dict)

+ 90 - 2
docs/接口汇总.md

@@ -77,6 +77,7 @@ AI 调用接口时应按以下规则判断结果:
 | S7 连接扫描 | `POST` | `/api/dc-gateway/s7/connect_scan` | 只传 IP,扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。 |
 | BACnet 点位读取 | `POST` | `/api/dc-gateway/bacnet/read_points` | 按 BACnet 对象读取 `present-value`。 |
 | BACnet 点位搜索 | `POST` | `/api/dc-gateway/bacnet/search_points` | 读取设备 `object-list`,返回常见点位对象信息和值。 |
+| BACnet BBMD Who-Is | `POST` | `/api/dc-gateway/bacnet/bbmd/whois` | 通过 BBMD 分发 `Who-Is`,返回收到的 `I-Am` 设备列表。 |
 
 ## 网关接口目录
 
@@ -85,7 +86,7 @@ AI 调用接口时应按以下规则判断结果:
 | 通用部分 | 请求格式、返回格式、HTTP 状态码、健康检查。 |
 | Modbus 网关接口 | Modbus TCP 通用设备字段、地址规则、功能码、原始读取、点位读取并转换。 |
 | S7 网关接口 | S7 TCP 通用设备字段、地址字段、语义化通信记录、原始读取、点位读取并转换、连接扫描。 |
-| BACnet 网关接口 | BACnet/IP 通用设备字段、对象类型、点位读取、点位搜索。 |
+| BACnet 网关接口 | BACnet/IP 通用设备字段、对象类型、点位读取、点位搜索、BBMD Who-Is。 |
 
 ## Modbus 通用设备字段
 
@@ -993,7 +994,7 @@ curl -X POST http://127.0.0.1:8000/api/dc-gateway/s7/connect_scan \
 
 ### BACnet 网关目标
 
-BACnet 网关用于通过 HTTP 直连 BACnet/IP 设备,当前支持点位读取和点位搜索底层使用 `BAC0`,读取点位时固定读取对象的 `present-value`。
+BACnet 网关用于通过 HTTP 直连 BACnet/IP 设备,当前支持点位读取、点位搜索和通过 BBMD 执行 Who-Is 设备发现。点位读取和点位搜索底层使用 `BAC0`,读取点位时固定读取对象的 `present-value`;BBMD Who-Is 设备发现通过 BACnet/IP BVLC 报文实现
 
 ### BACnet 通用设备字段
 
@@ -1186,6 +1187,91 @@ curl -X POST http://127.0.0.1:8000/api/dc-gateway/bacnet/search_points \
   }'
 ```
 
+### 接口:BACnet BBMD Who-Is 设备发现
+
+#### 基本信息
+
+| 项 | 值 |
+|---|---|
+| 方法 | `POST` |
+| 路径 | `/api/dc-gateway/bacnet/bbmd/whois` |
+| 完整地址 | `http://127.0.0.1:8000/api/dc-gateway/bacnet/bbmd/whois` |
+| Content-Type | 不需要请求体 |
+| 用途 | 注册 BBMD Foreign Device,通过 BBMD 分发 `Who-Is`,收集 `I-Am` 响应并返回 BACnet 设备列表。 |
+
+MCP 工具 `bacnet.bbmd_whois` 会调用该接口并透传响应。该工具只需要传 `project_key`,不发送请求体;BBMD 地址、端口、TTL、Who-Is 等待超时、设备号范围、本机绑定地址等参数由采集网关环境变量配置。
+
+#### 请求体结构
+
+不需要请求体,直接 `POST` 即可:
+
+```bash
+curl -X POST http://127.0.0.1:8000/api/dc-gateway/bacnet/bbmd/whois
+```
+
+#### 采集网关环境变量
+
+| 环境变量 | 是否必填 | 默认值 | 说明 |
+|---|---:|---|---|
+| `BACNET_BBMD_IP` | 是 | 无 | BBMD 地址。 |
+| `BACNET_BBMD_PORT` | 否 | `47808` | BBMD UDP 端口。 |
+| `BACNET_BBMD_TTL` | 否 | `60` | Foreign Device 注册 TTL,单位秒。 |
+| `BACNET_BBMD_WHOIS_TIMEOUT` | 否 | `5` | 收集 `I-Am` 的等待时间,单位秒。 |
+| `BACNET_BBMD_LOW_LIMIT` | 否 | `0` | Who-Is 设备号下限。 |
+| `BACNET_BBMD_HIGH_LIMIT` | 否 | `4194303` | Who-Is 设备号上限。 |
+| `BACNET_BBMD_LOCAL_DEVICE_ID` | 否 | 无 | 若收到同设备号的 `I-Am`,会作为本机设备忽略。 |
+| `BACNET_LOCAL_IP` | 否 | 自动按到 BBMD 的路由推断 | 本机 BACnet 源地址。 |
+| `BACNET_LOCAL_PORT` | 否 | 自动选择空闲端口 | 本机 UDP 绑定端口。 |
+
+#### 成功返回示例
+
+```json
+{
+  "code": 0,
+  "msg": "success",
+  "data": {
+    "bbmd": {
+      "bbmd_ip": "192.168.1.1",
+      "bbmd_port": 47808,
+      "ttl": 60,
+      "timeout": 5,
+      "low_limit": 0,
+      "high_limit": 4194303
+    },
+    "devices": [
+      {
+        "bacnet_device_id": 12345,
+        "ip": "192.168.10.20",
+        "port": 47808,
+        "max_apdu": 1476,
+        "segmentation": "noSegmentation",
+        "vendor_id": 842
+      }
+    ]
+  }
+}
+```
+
+#### 失败返回示例
+
+```json
+{
+  "code": 1,
+  "msg": "BBMD foreign device registration timeout",
+  "data": {
+    "bbmd": {
+      "bbmd_ip": "192.168.1.1",
+      "bbmd_port": 47808,
+      "ttl": 60,
+      "timeout": 5,
+      "low_limit": 0,
+      "high_limit": 4194303
+    },
+    "devices": []
+  }
+}
+```
+
 ## AI 调用决策指南
 
 AI 或自动化程序选择接口时遵循以下规则:
@@ -1200,6 +1286,7 @@ AI 或自动化程序选择接口时遵循以下规则:
 | 不确定 S7 连接参数 | `POST /api/dc-gateway/s7/connect_scan` | 扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。 |
 | 需要搜索 BACnet 设备点位 | `POST /api/dc-gateway/bacnet/search_points` | 读取设备 `object-list`,返回常见点位对象。 |
 | 需要读取 BACnet 点位当前值 | `POST /api/dc-gateway/bacnet/read_points` | 固定读取 BACnet 对象的 `present-value`。 |
+| 需要跨网段发现 BACnet 设备 | `POST /api/dc-gateway/bacnet/bbmd/whois` | 通过采集网关环境变量配置的 BBMD 分发 `Who-Is`,返回 `I-Am` 设备列表。 |
 
 AI 构造请求时必须区分两个地址:
 
@@ -1209,6 +1296,7 @@ AI 构造请求时必须区分两个地址:
 | Modbus 设备地址 | 请求体 `ip` 和 `port` | 实际被读取的 Modbus TCP 设备地址。 |
 | S7 设备地址 | 请求体 `ip`、`port`、`rock`、`slot`、`tsap_conn_type` | 实际被读取的 S7 TCP 设备连接参数。 |
 | BACnet 设备地址 | 请求体 `ip`、`port`、`bacnet_device_id` | 实际被读取的 BACnet/IP 设备参数。 |
+| BACnet BBMD 地址 | 采集网关环境变量 `BACNET_BBMD_IP`、`BACNET_BBMD_PORT` | `bacnet.bbmd_whois` 使用的 BBMD 目标地址,不通过 MCP 入参传递。 |
 
 AI 构造 `/modbus/read_points` 点位时应按以下步骤:
 

+ 18 - 0
tests/test_gateway_api.py

@@ -134,6 +134,24 @@ class GatewayApiTests(unittest.TestCase):
             },
         )
 
+    def test_bacnet_bbmd_whois_posts_without_body(self) -> None:
+        response = {"code": 0, "msg": "success", "data": {"bbmd": {}, "devices": []}}
+        with patch(
+            "data_collector_mcp.gateway_api.find_project_config",
+            return_value={"project_key": "dev-01", "base_url": "http://gateway.test"},
+        ), patch(
+            "data_collector_mcp.gateway_api.request_json",
+            return_value=response,
+        ) as request_json:
+            result = gateway_api.bacnet_bbmd_whois("dev-01")
+
+        self.assertEqual(result, response)
+        request_json.assert_called_once_with(
+            "POST",
+            "http://gateway.test/api/dc-gateway/bacnet/bbmd/whois",
+            json_payload=None,
+        )
+
     def test_s7_raw_read_posts_gateway_payload(self) -> None:
         response = {"code": 0, "msg": "success", "data": {"communication": []}}
         with patch(

+ 8 - 0
tests/test_server_tools.py

@@ -30,6 +30,7 @@ class ServerToolTests(unittest.TestCase):
                 "modbus.point_collect_test",
                 "bacnet.point_collect_test",
                 "bacnet.point_search",
+                "bacnet.bbmd_whois",
                 "s7.raw_read",
                 "s7.point_collect_test",
                 "s7.connect_scan",
@@ -429,6 +430,13 @@ class ServerToolTests(unittest.TestCase):
             port=47808,
         )
 
+        with patch(
+            "data_collector_mcp.bacnet_server.api_bacnet_bbmd_whois",
+            return_value={"code": 0},
+        ) as bbmd_whois:
+            self.assertEqual(bacnet_server.bacnet_bbmd_whois("dev-01"), {"code": 0})
+        bbmd_whois.assert_called_once_with("dev-01")
+
     def test_bacnet_collector_tools_forward_to_api(self) -> None:
         devices = [{"name": "bacnet_1", "ip": "192.168.1.20", "bacnet_device_id": 12345}]
         with patch(