Skip to content

Commit

Permalink
fix(plc4py): Make all read and write functions the same. No more dang…
Browse files Browse the repository at this point in the history
…ling threads.
  • Loading branch information
hutcheb committed May 26, 2024
1 parent f0f6181 commit 6f7fea8
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 96 deletions.
5 changes: 4 additions & 1 deletion plc4py/plc4py/PlcDriverManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ def __post_init__(self):
defined in the "plc4py.drivers" namespace.
"""
# Log the class loader used
logging.info("Instantiating new PLC Driver Manager with class loader %s", self.class_loader)
logging.info(
"Instantiating new PLC Driver Manager with class loader %s",
self.class_loader,
)

# Add the PlcDriverClassLoader hookspecs to the class loader
self.class_loader.add_hookspecs(PlcDriverClassLoader)
Expand Down
3 changes: 2 additions & 1 deletion plc4py/plc4py/api/messages/PlcMessage.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
class PlcMessage(Serializable):
"""
A class representing a PLC message.
Add more details about the class and its functionality here.
"""

pass
105 changes: 74 additions & 31 deletions plc4py/plc4py/drivers/mock/MockConnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@

import asyncio
import logging
from asyncio import Transport
from dataclasses import dataclass, field
from typing import Awaitable, Type, List, Dict
from typing import Awaitable, Type, List, Dict, Union

from plc4py.spi.messages.PlcWriter import PlcWriter

Expand Down Expand Up @@ -53,27 +54,35 @@
from plc4py.spi.values.PlcValues import PlcBOOL
from plc4py.spi.values.PlcValues import PlcINT

from plc4py.spi.transport.Plc4xBaseTransport import Plc4xBaseTransport


@dataclass
class MockDevice:
def read(self, tag: MockTag) -> ResponseItem[PlcValue]:
async def read(
self, request: PlcReadRequest, transport: Transport
) -> PlcReadResponse:
"""
Reads one field from the Mock Device
"""
logging.debug(f"Reading field {str(tag)} from Mock Device")
response_items = {}
for tag_name, tag in request.tags.items():
logging.debug(f"Reading field {str(tag)} from Mock Device")

if tag.data_type == "BOOL":
return ResponseItem(PlcResponseCode.OK, PlcBOOL(False))
elif tag.data_type == "INT":
return ResponseItem(PlcResponseCode.OK, PlcINT(0))
else:
raise PlcFieldParseException
if tag.data_type == "BOOL":
response_items[tag_name] = ResponseItem(PlcResponseCode.OK, PlcBOOL(False))
elif tag.data_type == "INT":
response_items[tag_name] = ResponseItem(PlcResponseCode.OK, PlcINT(0))
else:
raise PlcFieldParseException
return PlcReadResponse(PlcResponseCode.OK, response_items)


@dataclass
class MockConnection(PlcConnection, PlcReader, PlcWriter, PlcConnectionMetaData):
_is_connected: bool = False
device: MockDevice = field(default_factory=lambda: MockDevice())
_device: MockDevice = field(default_factory=lambda: MockDevice())
_transport: Union[Plc4xBaseTransport, None] = None

def _connect(self):
"""
Expand Down Expand Up @@ -123,49 +132,83 @@ async def execute(self, request: PlcRequest) -> PlcResponse:

return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

def _check_connection(self) -> bool:
"""
Checks if a ModbusDevice is set.
:return: True if no device is set, False otherwise.
"""
"""
A ModbusDevice is only set if the device was successfully connected during the constructor.
If no device is set, it's not possible to execute any read or write requests.
This method is used to prevent calling methods on the ModbusConnection which are not possible
if no device is set.
"""
return self._device is None

async def _read(self, request: PlcReadRequest) -> PlcReadResponse:
"""
Executes a PlcReadRequest
This method sends a read request to the connected modbus device and waits for a response.
The response is then returned as a PlcReadResponse.
If no device is set, an error is logged and a PlcResponseCode.NOT_CONNECTED is returned.
If an error occurs during the execution of the read request, a PlcResponseCode.INTERNAL_ERROR is
returned.
:param request: PlcReadRequest to execute
:return: PlcReadResponse
"""
if self.device is None:
logging.error("No device is set in the mock connection!")
if self._check_connection():
logging.error("No device is set in the Mock connection!")
return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

# TODO: Insert Optimizer base on data from a browse request
try:
logging.debug("Sending read request to Mock Device")
response = PlcReadResponse(
PlcResponseCode.OK,
{
tag_name: self.device.read(tag)
for tag_name, tag in request.tags.items()
},
response = await asyncio.wait_for(
self._device.read(request, self._transport), 10
)
return response
except Exception as e:
except Exception:
# TODO:- This exception is very general and probably should be replaced
return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR, {})

async def _write(self, request: PlcWriteRequest) -> PlcWriteResponse:
"""
Executes a PlcReadRequest
Executes a PlcWriteRequest
This method sends a write request to the connected Modbus device and waits for a response.
The response is then returned as a PlcWriteResponse.
If no device is set, an error is logged and a PlcWriteResponse with the
PlcResponseCode.NOT_CONNECTED code is returned.
If an error occurs during the execution of the write request, a
PlcWriteResponse with the PlcResponseCode.INTERNAL_ERROR code is returned.
:param request: PlcWriteRequest to execute
:return: PlcWriteResponse
"""
if self.device is None:
logging.error("No device is set in the mock connection!")
if self._check_connection():
# If no device is set, log an error and return a response with the NOT_CONNECTED code
logging.error("No device is set in the Mock connection!")
return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

try:
logging.debug("Sending read request to MockDevice")
response = PlcWriteResponse(
PlcResponseCode.OK,
{
tag_name: self.device.write(tag)
for tag_name, tag in request.tags.items()
},
# Send the write request to the device and wait for a response
logging.debug("Sending write request to Mock Device")
response = await asyncio.wait_for(
self._device.write(request, self._transport), 5
)
# Return the response
return response
except Exception as e:
except Exception:
# If an error occurs during the execution of the write request, return a response with
# the INTERNAL_ERROR code. This exception is very general and probably should be replaced.
# TODO:- This exception is very general and probably should be replaced
return PlcWriteResponse(PlcResponseCode.INTERNAL_ERROR, request.tags)
return PlcWriteResponse(PlcResponseCode.INTERNAL_ERROR, {})

def is_read_supported(self) -> bool:
"""
Expand Down
33 changes: 15 additions & 18 deletions plc4py/plc4py/drivers/modbus/ModbusConnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def read_request_builder(self) -> ReadRequestBuilder:
"""
return DefaultReadRequestBuilder(ModbusTagBuilder)

def execute(self, request: PlcRequest) -> Awaitable[PlcResponse]:
async def execute(self, request: PlcRequest) -> PlcResponse:
"""
Executes a PlcRequest as long as it's already connected
:param request: Plc Request to execute
Expand All @@ -161,10 +161,10 @@ def execute(self, request: PlcRequest) -> Awaitable[PlcResponse]:
return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

if isinstance(request, PlcReadRequest):
return self._read(request)
return await self._read(request)

elif isinstance(request, PlcWriteRequest):
return self._write(request)
return await self._write(request)

return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

Expand All @@ -183,7 +183,7 @@ def _check_connection(self) -> bool:
"""
return self._device is None

def _read(self, request: PlcReadRequest) -> Awaitable[PlcReadResponse]:
async def _read(self, request: PlcReadRequest) -> PlcReadResponse:
"""
Executes a PlcReadRequest
Expand All @@ -197,23 +197,20 @@ def _read(self, request: PlcReadRequest) -> Awaitable[PlcReadResponse]:
:param request: PlcReadRequest to execute
:return: PlcReadResponse
"""
if self._device is None:
logging.error("No device is set in the Umas connection!")
if self._check_connection():
logging.error("No device is set in the Modbus connection!")
return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

# TODO: Insert Optimizer base on data from a browse request
async def _request(req, device) -> PlcReadResponse:
try:
response = await asyncio.wait_for(device.read(req, self._transport), 10)
return response
except Exception:
# TODO:- This exception is very general and probably should be replaced

return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR, {})

logging.debug("Sending read request to UmasDevice")
future = asyncio.ensure_future(_request(request, self._device))
return future
try:
logging.debug("Sending read request to Modbus Device")
response = await asyncio.wait_for(
self._device.read(request, self._transport), 10
)
return response
except Exception:
# TODO:- This exception is very general and probably should be replaced
return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR, {})

async def _write(self, request: PlcWriteRequest) -> PlcWriteResponse:
"""
Expand Down
77 changes: 49 additions & 28 deletions plc4py/plc4py/drivers/umas/UmasConnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,47 +160,68 @@ def execute(self, request: PlcRequest) -> Awaitable[PlcResponse]:

return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

def _read(self, request: PlcReadRequest) -> Awaitable[PlcReadResponse]:
async def _read(self, request: PlcReadRequest) -> PlcReadResponse:
"""
Executes a PlcReadRequest
This method sends a read request to the connected modbus device and waits for a response.
The response is then returned as a PlcReadResponse.
If no device is set, an error is logged and a PlcResponseCode.NOT_CONNECTED is returned.
If an error occurs during the execution of the read request, a PlcResponseCode.INTERNAL_ERROR is
returned.
:param request: PlcReadRequest to execute
:return: PlcReadResponse
"""
if self._device is None:
if self._check_connection():
logging.error("No device is set in the Umas connection!")
return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

# TODO: Insert Optimizer base on data from a browse request
async def _request(req, device) -> PlcReadResponse:
try:
response = await asyncio.wait_for(device.read(req, self._transport), 10)
return response
except Exception:
# TODO:- This exception is very general and probably should be replaced
self.log.exception("Caught an exception while executing a read request")
return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR, {})

logging.debug("Sending read request to UmasDevice")
future = asyncio.ensure_future(_request(request, self._device))
return future
try:
logging.debug("Sending read request to Umas Device")
response = await asyncio.wait_for(
self._device.read(request, self._transport), 10
)
return response
except Exception:
# TODO:- This exception is very general and probably should be replaced
return PlcReadResponse(PlcResponseCode.INTERNAL_ERROR, {})

def _write(self, request: PlcWriteRequest) -> Awaitable[PlcTagResponse]:
async def _write(self, request: PlcWriteRequest) -> PlcWriteResponse:
"""
Executes a PlcWriteRequest
This method sends a write request to the connected Modbus device and waits for a response.
The response is then returned as a PlcWriteResponse.
If no device is set, an error is logged and a PlcWriteResponse with the
PlcResponseCode.NOT_CONNECTED code is returned.
If an error occurs during the execution of the write request, a
PlcWriteResponse with the PlcResponseCode.INTERNAL_ERROR code is returned.
:param request: PlcWriteRequest to execute
:return: PlcWriteResponse
"""
if self._device is None:
logging.error("No device is set in the umas connection!")
if self._check_connection():
# If no device is set, log an error and return a response with the NOT_CONNECTED code
logging.error("No device is set in the Umas connection!")
return self._default_failed_request(PlcResponseCode.NOT_CONNECTED)

async def _request(req, device) -> PlcWriteResponse:
try:
response = await asyncio.wait_for(device.write(req, self._transport), 5)
return response
except Exception as e:
# TODO:- This exception is very general and probably should be replaced
return PlcWriteResponse(PlcResponseCode.INTERNAL_ERROR, {})

logging.debug("Sending write request to ModbusDevice")
future = asyncio.ensure_future(_request(request, self._device))
return future
try:
# Send the write request to the device and wait for a response
logging.debug("Sending write request to Umas Device")
response = await asyncio.wait_for(
self._device.write(request, self._transport), 5
)
# Return the response
return response
except Exception:
# If an error occurs during the execution of the write request, return a response with
# the INTERNAL_ERROR code. This exception is very general and probably should be replaced.
# TODO:- This exception is very general and probably should be replaced
return PlcWriteResponse(PlcResponseCode.INTERNAL_ERROR, {})

def _browse(self, request: PlcBrowseRequest) -> Awaitable[PlcBrowseResponse]:
"""
Expand Down
4 changes: 1 addition & 3 deletions plc4py/tests/test_plc4py.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,7 @@ async def test_plc_driver_manager_init_mock_read_request():
# Build the request
request: PlcTagRequest = builder.build()
# Execute the request
response: PlcReadResponse = cast(
PlcReadResponse, await connection.execute(request)
)
response = await connection.execute(request)

# Verify that the request has one field
assert response.response_code == PlcResponseCode.OK
12 changes: 6 additions & 6 deletions plc4py/tests/unit/plc4py/drivers/modbus/test_modbus_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async def test_modbus_discrete_inputs_request_standardized():
"""
# Create a Modbus PDU Read Discrete Inputs Request with address 0 and quantity 10
discrete_inputs_request = ModbusPDUReadDiscreteInputsRequestBuilder(0, 10).build()

# Ensure the request object is not None
assert discrete_inputs_request is not None

Expand Down Expand Up @@ -64,19 +64,19 @@ async def test_modbus_ModbusTcpADUBuilder_serialize():
"""
# Create a Modbus PDU Read Discrete Inputs
pdu = ModbusPDUReadDiscreteInputsRequestBuilder(5, 2).build()

# Build Modbus TCP ADU
request = ModbusTcpADUBuilder(10, 5, pdu).build(False)

# Get the size of the request
size = request.length_in_bytes()

# Create a write buffer
write_buffer = WriteBufferByteBased(size, ByteOrder.BIG_ENDIAN)

# Serialize the request
serialize = request.serialize(write_buffer)

# Get the serialized bytes
bytes_array = write_buffer.get_bytes().tobytes()

Expand Down
Loading

0 comments on commit 6f7fea8

Please sign in to comment.