Skip to content

Commit

Permalink
Add ModbusConnection support
Browse files Browse the repository at this point in the history
  • Loading branch information
OCopping committed Mar 8, 2024
1 parent 2d9657f commit 9aff887
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 1 deletion.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies = [
"pydantic",
"pvi~=0.7.1",
"softioc",
"pymodbus",
] # Add project dependencies here, e.g. ["click", "numpy"]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down
3 changes: 2 additions & 1 deletion src/fastcs/connections/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .ip_connection import IPConnection
from .modbus_connection import ModbusSerialConnection, ModbusTcpConnection, ModbusUdpConnection

__all__ = ["IPConnection"]
__all__ = ["IPConnection", "ModbusSerialConnection", "ModbusTcpConnection", "ModbusUdpConnection"]
138 changes: 138 additions & 0 deletions src/fastcs/connections/modbus_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import asyncio

from dataclasses import dataclass
from typing import Optional

from pymodbus.client.base import ModbusBaseClient
from pymodbus.client import (
AsyncModbusSerialClient,
AsyncModbusTcpClient,
AsyncModbusUdpClient,
)
from pymodbus.exceptions import ModbusException

from pymodbus.framer import Framer
from pymodbus.pdu import ExceptionResponse


# Constants
CR = "\r"
TIMEOUT = 1.0 # Seconds
RECV_BUFFER = 4096 # Bytes


@dataclass
class ModbusConnectionSettings:
host: str = "127.0.0.1"
port: int = 7001
slave: int = 0

class ModbusConnection:

def __init__(self, settings: ModbusConnectionSettings) -> None:

self.host, self.port, self.slave = settings.host, settings.port, settings.slave
self.running: bool = False

Check warning on line 35 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L34-L35

Added lines #L34 - L35 were not covered by tests

self._client: ModbusBaseClient

Check warning on line 37 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L37

Added line #L37 was not covered by tests

async def connect(self, framer=Framer.SOCKET):
raise NotImplementedError

Check warning on line 40 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L40

Added line #L40 was not covered by tests


def disconnect(self):
self._client.close()

Check warning on line 44 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L44

Added line #L44 was not covered by tests

async def _read(self, address: int, count: int = 2) -> Optional[str]:
# address -= 1 # modbus spec starts from 0 not 1
try:

Check warning on line 48 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L48

Added line #L48 was not covered by tests
# address_hex = hex(address)
rr = await self._client.read_holding_registers(address, count=count, slave=self.slave) # type: ignore
print(f"Response: {rr}")

Check warning on line 51 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L50-L51

Added lines #L50 - L51 were not covered by tests
except ModbusException as exc: # pragma no cover
# Received ModbusException from library
self.disconnect()
return
if rr.isError(): # pragma no cover
# Received Modbus library error
self.disconnect()
return
if isinstance(rr, ExceptionResponse): # pragma no cover
# Received Modbus library exception
# THIS IS NOT A PYTHON EXCEPTION, but a valid modbus message
self.disconnect()

async def send(self, address: int, value: int) -> None:
"""Send a request.
Args:
address: The register address to write to.
value: The value to write.
"""
await self._client.write_registers(address, value, slave=self.slave)
resp = await self._read(address, 2)

Check warning on line 73 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L72-L73

Added lines #L72 - L73 were not covered by tests

class ModbusSerialConnection(ModbusConnection):

def __init__(self, settings: ModbusConnectionSettings) -> None:
super().__init__(settings)

Check warning on line 78 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L78

Added line #L78 was not covered by tests

async def connect(self, framer=Framer.SOCKET):
self._client: AsyncModbusSerialClient = AsyncModbusSerialClient(

Check warning on line 81 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L81

Added line #L81 was not covered by tests
str(self.port),
framer=framer,
timeout=10,
retries=3,
retry_on_empty=False,
close_comm_on_error=False,
strict=True,
baudrate=9600,
bytesize=8,
parity="N",
stopbits=1,
)

await self._client.connect()
assert self._client.connected

Check warning on line 96 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L95-L96

Added lines #L95 - L96 were not covered by tests

class ModbusTcpConnection(ModbusConnection):

def __init__(self, settings: ModbusConnectionSettings) -> None:
super().__init__(settings)

Check warning on line 101 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L101

Added line #L101 was not covered by tests

async def connect(self, framer=Framer.SOCKET):
self._client: AsyncModbusTcpClient = AsyncModbusTcpClient(

Check warning on line 104 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L104

Added line #L104 was not covered by tests
self.host,
self.port,
framer=framer,
timeout=10,
retries=3,
retry_on_empty=False,
close_comm_on_error=False,
strict=True,
source_address=("localhost", 0),
)

await self._client.connect()
assert self._client.connected

Check warning on line 117 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L116-L117

Added lines #L116 - L117 were not covered by tests

class ModbusUdpConnection(ModbusConnection):

def __init__(self, settings: ModbusConnectionSettings) -> None:
super().__init__(settings)

Check warning on line 122 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L122

Added line #L122 was not covered by tests

async def connect(self, framer=Framer.SOCKET):
self._client: AsyncModbusUdpClient = AsyncModbusUdpClient(

Check warning on line 125 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L125

Added line #L125 was not covered by tests
self.host,
self.port,
framer=framer,
timeout=10,
retries=3,
retry_on_empty=False,
close_comm_on_error=False,
strict=True,
source_address=("localhost", 0),
)

await self._client.connect()
assert self._client.connected

Check warning on line 138 in src/fastcs/connections/modbus_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/modbus_connection.py#L137-L138

Added lines #L137 - L138 were not covered by tests

0 comments on commit 9aff887

Please sign in to comment.