feat: 完成 issue #3 边缘采集网关模板化封装

- 点位字典 CSV schema/加载/自动校验(缺失字段/量纲/重复点号,PRD 5.1)
- 协议可插拔只读驱动:OPC UA(S7-1200 适配)/S7/Modbus/称重/能源/模拟
- 周期采集引擎:只读+背压保护+健康度指标(丢失率/P99/可用性)
- Kafka 流式上行 + 本地 spool 断点续传(丢失率≤0.02% 保障)
- 模板配置外置(gateway.yaml + 点位字典 CSV),换行业零改码
- 18 个单元测试全绿;端到端运行 SLA 达标
This commit is contained in:
2026-08-04 15:32:16 +08:00
parent 21c6259739
commit f49c0920d4
27 changed files with 1806 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""OPC UA 驱动 —— 和利时 DCS 等 OPC UA 接口参数化适配(issue #25 落点)。
连接参数全部来自模板配置(gateway.yaml 的 drivers.opcua 段),不硬编码。
实际设备接入依赖第三方库 `asyncua`(异步)或 `opcua`(同步);
未安装时给出明确提示,不会静默失败。
"""
from __future__ import annotations
from typing import Dict, List, Optional
from point_dict.loader import Point
from .base import Driver, SampleValue
class OpcUaDriver(Driver):
"""OPC UA 只读采集驱动(参数化适配)。"""
protocol = "opcua"
def __init__(self, config: Optional[dict] = None):
super().__init__(config)
self.endpoint: str = self.config.get("endpoint", "")
self.security: str = self.config.get("security", "None")
self._client = None
def connect(self) -> None:
try:
from opcua import Client # type: ignore
except ImportError as exc: # pragma: no cover - 依赖缺失路径
raise ConnectionError(
"OPC UA 驱动依赖库未安装:请 `pip install opcua`(或 asyncua)"
) from exc
if not self.endpoint:
raise ConnectionError("OPC UA 驱动缺少配置: drivers.opcua.endpoint")
self._client = Client(self.endpoint, timeout=self.config.get("timeout", 10))
self._client.connect()
self._client.session_timeout = self.config.get("session_timeout", 60000)
def read_points(self, points: List[Point]) -> Dict[str, SampleValue]:
if self._client is None:
raise ConnectionError("OPC UA 客户端未连接,请先 connect()")
result: Dict[str, SampleValue] = {}
for p in points:
node_path = p.opc_node
if not node_path:
continue # 无 OPC 节点的点由其它驱动采集
try:
node = self._client.get_node(node_path)
result[p.point_id] = node.get_value()
except Exception:
# 单点读取失败不中断整批:记为未读到(计入丢失率)
result[p.point_id] = None
return result
def close(self) -> None:
if self._client is not None:
try:
self._client.disconnect()
finally:
self._client = None