# -*- coding: utf-8 -*- """Modbus 驱动 —— PLC / 称重仪表等 RTU/TCP 从站参数化适配。 连接参数(mode / host / port / slave_id / register_map)来自模板配置; 寄存器映射约定写入点位字典 opcNode 列,格式 `{type}:{addr}`, type ∈ holding|input|coil|discrete,如 `holding:40001`。 依赖 `pymodbus`,未安装时给出明确提示。 """ from __future__ import annotations from typing import Dict, List, Optional from point_dict.loader import Point from .base import Driver, SampleValue _REG_MAP = {"holding": 3, "input": 4, "coil": 1, "discrete": 2} class ModbusDriver(Driver): """Modbus 只读采集驱动(参数化适配)。""" protocol = "modbus" def __init__(self, config: Optional[dict] = None): super().__init__(config) self.mode: str = self.config.get("mode", "tcp") # tcp | rtu self.host: str = self.config.get("host", "") self.port: int = int(self.config.get("port", 502)) self.slave_id: int = int(self.config.get("slave_id", 1)) self._client = None def connect(self) -> None: try: from pymodbus.client import ModbusTcpClient, ModbusSerialClient # type: ignore except ImportError as exc: # pragma: no cover - 依赖缺失路径 raise ConnectionError( "Modbus 驱动依赖库未安装:请 `pip install pymodbus`" ) from exc if self.mode == "tcp": if not self.host: raise ConnectionError("Modbus 驱动缺少配置: drivers.modbus.host") self._client = ModbusTcpClient(self.host, port=self.port) else: self._client = ModbusSerialClient( port=self.config.get("port", "COM1"), baudrate=self.config.get("baudrate", 9600), ) if not self._client.connect(): raise ConnectionError(f"Modbus 连接失败: {self.host or self.config.get('port')}") def read_points(self, points: List[Point]) -> Dict[str, SampleValue]: if self._client is None: raise ConnectionError("Modbus 客户端未连接,请先 connect()") result: Dict[str, SampleValue] = {} for p in points: node = (p.opc_node or "").split(":") if len(node) != 2 or node[0] not in _REG_MAP: continue reg_type, addr = node[0], int(node[1]) try: if reg_type == "coil": resp = self._client.read_coils(addr, count=1, slave=self.slave_id) elif reg_type == "discrete": resp = self._client.read_discrete_inputs(addr, count=1, slave=self.slave_id) else: resp = self._client.read_holding_registers(addr, count=1, slave=self.slave_id) \ if reg_type == "holding" else \ self._client.read_input_registers(addr, count=1, slave=self.slave_id) result[p.point_id] = resp.registers[0] if hasattr(resp, "registers") and resp.registers else None except Exception: result[p.point_id] = None return result def close(self) -> None: if self._client is not None: try: self._client.close() finally: self._client = None