second version
This commit is contained in:
166
backend/services/modbus_client.py
Normal file
166
backend/services/modbus_client.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""Persistentní Modbus TCP klient na sdílené Waveshare / RS485 bráně (jedno spojení + lock)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from pymodbus.client import AsyncModbusTcpClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModbusBatch:
|
||||
"""Více read/write pod jedním držením locku (žádný jiný task na stejném klientovi mezi nimi)."""
|
||||
|
||||
def __init__(self, owner: PersistentModbusClient) -> None:
|
||||
self._o = owner
|
||||
|
||||
async def read_register(self, address: int) -> int:
|
||||
return await self._o._read_register_locked(address)
|
||||
|
||||
async def read_register_signed(self, address: int) -> int:
|
||||
raw = await self.read_register(address)
|
||||
return raw - 65536 if raw > 32767 else raw
|
||||
|
||||
async def write_register(self, address: int, value: int) -> bool:
|
||||
return await self._o._write_register_locked(address, value)
|
||||
|
||||
async def write_registers(self, address: int, values: list[int]) -> bool:
|
||||
return await self._o._write_registers_locked(address, values)
|
||||
|
||||
|
||||
class PersistentModbusClient:
|
||||
"""
|
||||
Jedno persistentní TCP spojení na převodník.
|
||||
Serializuje všechny požadavky přes asyncio.Lock().
|
||||
Automaticky reconnectuje při výpadku.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, device_id: int = 1) -> None:
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.device_id = device_id
|
||||
self._client: AsyncModbusTcpClient | None = None
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def _ensure_connected(self) -> None:
|
||||
if self._client is not None and self._client.connected:
|
||||
return
|
||||
if self._client is not None:
|
||||
self._client.close()
|
||||
self._client = None
|
||||
logger.info("Modbus connecting %s:%s dev=%s", self.host, self.port, self.device_id)
|
||||
self._client = AsyncModbusTcpClient(
|
||||
self.host,
|
||||
port=self.port,
|
||||
timeout=5,
|
||||
retries=2,
|
||||
)
|
||||
await self._client.connect()
|
||||
if not self._client.connected:
|
||||
self._client.close()
|
||||
self._client = None
|
||||
raise ConnectionError(f"Cannot connect Modbus {self.host}:{self.port}")
|
||||
logger.info("Modbus connected %s:%s", self.host, self.port)
|
||||
|
||||
async def _read_register_locked(self, address: int) -> int:
|
||||
if self._client is None or not self._client.connected:
|
||||
await self._ensure_connected()
|
||||
assert self._client is not None
|
||||
try:
|
||||
r = await self._client.read_holding_registers(
|
||||
address, count=1, device_id=self.device_id
|
||||
)
|
||||
if r.isError() or not getattr(r, "registers", None):
|
||||
raise OSError(f"Read error 0x{address:04X}: {r!r}")
|
||||
return int(r.registers[0])
|
||||
except Exception as e:
|
||||
logger.warning("Modbus read 0x%04X failed: %s", address, e)
|
||||
self._client.close()
|
||||
self._client = None
|
||||
raise
|
||||
|
||||
async def _write_registers_locked(self, address: int, values: list[int]) -> bool:
|
||||
if self._client is None or not self._client.connected:
|
||||
await self._ensure_connected()
|
||||
assert self._client is not None
|
||||
try:
|
||||
clamped = [max(0, min(65535, int(v))) for v in values]
|
||||
r = await self._client.write_registers(
|
||||
address, clamped, device_id=self.device_id
|
||||
)
|
||||
if r.isError():
|
||||
raise OSError(f"Write error 0x{address:04X}={clamped}: {r!r}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Modbus write_registers 0x%04X failed: %s", address, e
|
||||
)
|
||||
self._client.close()
|
||||
self._client = None
|
||||
raise
|
||||
|
||||
async def _write_register_locked(self, address: int, value: int) -> bool:
|
||||
if self._client is None or not self._client.connected:
|
||||
await self._ensure_connected()
|
||||
assert self._client is not None
|
||||
try:
|
||||
v = max(0, min(65535, int(value)))
|
||||
r = await self._client.write_register(address, v, device_id=self.device_id)
|
||||
if r.isError():
|
||||
raise OSError(f"Write error 0x{address:04X}={v}: {r!r}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning("Modbus write 0x%04X=%s failed: %s", address, value, e)
|
||||
self._client.close()
|
||||
self._client = None
|
||||
raise
|
||||
|
||||
async def read_register(self, address: int) -> int:
|
||||
async with self._lock:
|
||||
await self._ensure_connected()
|
||||
return await self._read_register_locked(address)
|
||||
|
||||
async def read_register_signed(self, address: int) -> int:
|
||||
raw = await self.read_register(address)
|
||||
return raw - 65536 if raw > 32767 else raw
|
||||
|
||||
async def write_register(self, address: int, value: int) -> bool:
|
||||
async with self._lock:
|
||||
await self._ensure_connected()
|
||||
return await self._write_register_locked(address, value)
|
||||
|
||||
async def write_registers(self, address: int, values: list[int]) -> bool:
|
||||
"""FC 0x10 – povinné pro Deye registry 60–499 (jeden i více registrů)."""
|
||||
async with self._lock:
|
||||
await self._ensure_connected()
|
||||
return await self._write_registers_locked(address, values)
|
||||
|
||||
@asynccontextmanager
|
||||
async def batch(self) -> AsyncIterator[ModbusBatch]:
|
||||
"""Drží lock pro více po sobě jdoucích operací (telemetrie vs. control na stejné bráně)."""
|
||||
async with self._lock:
|
||||
await self._ensure_connected()
|
||||
yield ModbusBatch(self)
|
||||
|
||||
def close(self) -> None:
|
||||
if self._client is not None:
|
||||
self._client.close()
|
||||
self._client = None
|
||||
|
||||
|
||||
_clients: dict[str, PersistentModbusClient] = {}
|
||||
_registry_lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def get_modbus_client(
|
||||
host: str, port: int, device_id: int = 1
|
||||
) -> PersistentModbusClient:
|
||||
key = f"{host}:{port}:{device_id}"
|
||||
async with _registry_lock:
|
||||
if key not in _clients:
|
||||
_clients[key] = PersistentModbusClient(host, port, device_id)
|
||||
return _clients[key]
|
||||
Reference in New Issue
Block a user